diff --git a/.github/workflows/helm-e2e.yaml b/.github/workflows/helm-e2e.yaml index 314e143542..bb812cbbdd 100644 --- a/.github/workflows/helm-e2e.yaml +++ b/.github/workflows/helm-e2e.yaml @@ -36,6 +36,10 @@ jobs: go-version-file: go.mod - name: Setup Helm uses: azure/setup-helm@v4 + - name: Test Helm chart + run: | + helm plugin install https://github.com/helm-unittest/helm-unittest.git --version 1.0.3 --verify=false + make helm-test - name: Cache micro-VM assets uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: diff --git a/Makefile b/Makefile index 8dd64c7fd4..7930319bd7 100644 --- a/Makefile +++ b/Makefile @@ -146,6 +146,10 @@ clean: helm-template: @./hack/render-manifests.sh +.PHONY: helm-test +helm-test: + @helm unittest charts/substrate + # Verify that manifests/ate-install/ matches the chart output. Used in CI. .PHONY: verify-helm-template verify-helm-template: diff --git a/charts/substrate/README.md b/charts/substrate/README.md index 7fd7b09ed1..07bbfa2ac8 100644 --- a/charts/substrate/README.md +++ b/charts/substrate/README.md @@ -41,8 +41,14 @@ See `values.yaml` for the full set; the important keys: | Key | Default | Notes | |-----|---------|-------| | `postgres.enabled` | `true` | Deploy the bundled PostgreSQL instance | -| `postgres.connectionString` | `""` (in-cluster) | Override to use external PostgreSQL | -| `postgres.schema` | `public` | Store the Substrate tables in this PostgreSQL schema | +| `postgres.readWriteConnectionStringSecretRef` | managed Secret | Read the read/write connection from a Secret | +| `postgres.ownerConnectionStringSecretRef` | managed Secret | Read the owner connection from a Secret | +| `postgres.bootstrap` | `true` | Create missing fixed users, roles, schema, and grants on startup; the direct `ateapi` binary defaults to `false` | +| `postgres.readWriteRole` | `substrate_readwrite` | Role assumed by read/write connections; configurable when bootstrap is disabled | +| `postgres.ownerRole` | `substrate_owner` | Role assumed by owner connections; configurable when bootstrap is disabled | +| `postgres.adminSecretRef` | `postgres-admin` | Select the administrator Secret | +| `postgres.pool.maxConnLifetime` | `""` (pgx default) | Maximum physical connection lifetime; bounds Secret credential turnover | +| `postgres.schema` | `substrate` | Store the Substrate tables in this PostgreSQL schema | | `postgres.storageSize` | `1Gi` | In-cluster PostgreSQL PVC size | | `rustfs.enabled` | `true` | Deploy an in-cluster S3-compatible RustFS bucket for snapshots | | `atelet.storageBackend` | `s3` | Default snapshot backend, wired to RustFS when `rustfs.enabled=true` | @@ -57,3 +63,45 @@ See `values.yaml` for the full set; the important keys: | `otel.metrics.endpoint` | `""` | OTLP endpoint for metrics, overriding `otel.endpoint` | | `otel.logs.enabled` | `true` | Set to `false` to export no logs. Gates both OTLP log sources: ateapi's actor lifecycle events and the router access log | | `otel.logs.endpoint` | `""` | OTLP endpoint for logs, overriding `otel.endpoint` | + +## PostgreSQL credential rotation + +The bundled PostgreSQL pod creates `postgres.database` (`atepg` by default). The chart uses the same database name in both managed connection Secrets. Under Kagent, the umbrella chart supplies its bundled PostgreSQL Service address and `kagent` database name to Substrate's fixed credential templates. + +The Substrate binary also has these fixed development usernames and passwords. During bootstrap it checks both connection Secrets against those constants, then uses the constants to create missing users. Bootstrap runs on every `ateapi` pod start while `postgres.bootstrap=true`. It never changes an existing user's password. To use different credentials, create the users yourself and disable bootstrap. + +The chart's default administrator and application passwords are fixed, published values. This bundled bootstrap setup is for development and evaluation, not production. For production, provision unique users and permissions outside Substrate, supply connection Secrets, and set `postgres.bootstrap=false`. + +The Substrate binary executes `cmd/ateapi/internal/store/atepg/identity.sql` from the Substrate repository during bootstrap. Operators can run that same file after supplying its transaction-local settings; it is separate from table migrations. +For manual provisioning with custom chart role names, set the optional +`substrate.bootstrap_owner_role` and `substrate.bootstrap_readwrite_role` +transaction-local settings before running the file. They default to the fixed +development names; bundled binary bootstrap passes those names explicitly. + +Substrate mounts connection Secrets as projected files. Kubernetes updates +these files when the Secret changes. Substrate reads the current value for +each new physical connection. + +`postgres.pool.maxConnLifetime` bounds how long established connections may +continue using an old credential; rotation is not immediate. Keep old and new +credentials valid long enough for Kubernetes projection and connection +turnover. + +When rotation changes a login username, grant the applicable configured group +role. Bootstrap uses the fixed `substrate_readwrite` and `substrate_owner` roles. + +For bundled PostgreSQL, first create replacement logins and connection Secrets. +Then set `postgres.bootstrap=false` and set both connection Secret references +in the same Helm upgrade. PostgreSQL remains bundled, but Substrate stops +creating or verifying the fixed login users. The bundled PostgreSQL pod still +uses its administrator Secret; the API server no longer mounts it. Bootstrap +never changes the password of an existing login. + +Substrate runs `SET ROLE` for each new connection. It rejects a login without +the required membership. + +Set `postgres.bootstrap=false` for a BYO database. Create both group roles and +all grants before installation, then set `postgres.readWriteRole` and +`postgres.ownerRole` to those names. Use distinct roles and table schemas for +separate installs sharing one database. Give each install separate logins and +grant each login membership only in its install's roles. diff --git a/charts/substrate/templates/_helpers.tpl b/charts/substrate/templates/_helpers.tpl index 45184413fb..20c427b625 100644 --- a/charts/substrate/templates/_helpers.tpl +++ b/charts/substrate/templates/_helpers.tpl @@ -78,6 +78,27 @@ Plaintext HTTP URL that clients use to reach atenet-router. {{- printf "http://%s.%s.svc:80" (include "substrate.fullname" (list "atenet-router" .)) .Release.Namespace -}} {{- end -}} +{{- define "substrate.postgres.adminSecretName" -}} +{{- .Values.postgres.adminSecretRef.name | default "postgres-admin" -}} +{{- end -}} + +{{- define "substrate.postgres.readWriteSecretName" -}} +{{- .Values.postgres.readWriteConnectionStringSecretRef.name | default (include "substrate.fullname" (list "postgres-readwrite" .)) -}} +{{- end -}} + +{{- define "substrate.postgres.ownerSecretName" -}} +{{- .Values.postgres.ownerConnectionStringSecretRef.name | default (include "substrate.fullname" (list "postgres-owner" .)) -}} +{{- end -}} + +{{/* Fixed bundled identities. Callers supply only the database endpoint. */}} +{{- define "substrate.postgres.readWriteConnectionString" -}} +{{- printf "postgresql://substrate_readwrite_user:substrate-readwrite@%s:5432/%s?%s" .host .database .params -}} +{{- end -}} + +{{- define "substrate.postgres.ownerConnectionString" -}} +{{- printf "postgresql://substrate_admin_user:substrate-admin@%s:5432/%s?%s" .host .database .params -}} +{{- end -}} + {{/* OTLP endpoint a signal exports to, or empty when the signal is disabled or no endpoint resolves. The per-signal endpoint wins over the generic one, matching diff --git a/charts/substrate/templates/ate-api-server-envvars.yaml b/charts/substrate/templates/ate-api-server-envvars.yaml index ca76ae3ef8..e80c5f3551 100644 --- a/charts/substrate/templates/ate-api-server-envvars.yaml +++ b/charts/substrate/templates/ate-api-server-envvars.yaml @@ -14,8 +14,20 @@ See the License for the specific language governing permissions and limitations under the License. */}} -{{- if and (not .Values.postgres.enabled) (empty .Values.postgres.connectionString) }} -{{- fail "postgres.connectionString is required when postgres.enabled=false" }} +{{- if not (kindIs "bool" .Values.postgres.bootstrap) }} +{{- fail "postgres.bootstrap must be true or false" }} +{{- end }} +{{- if and .Values.postgres.enabled .Values.postgres.bootstrap (or .Values.postgres.readWriteConnectionStringSecretRef.name .Values.postgres.ownerConnectionStringSecretRef.name) }} +{{- fail "postgres.bootstrap requires the chart-managed application Secrets; disable bootstrap for operator-managed Secrets" }} +{{- end }} +{{- if and (or (not .Values.postgres.enabled) (not .Values.postgres.bootstrap)) (not .Values.postgres.readWriteConnectionStringSecretRef.name) }} +{{- fail "postgres.readWriteConnectionStringSecretRef.name is required when bootstrap is disabled or PostgreSQL is external" }} +{{- end }} +{{- if and (or (not .Values.postgres.enabled) (not .Values.postgres.bootstrap)) (not .Values.postgres.ownerConnectionStringSecretRef.name) }} +{{- fail "postgres.ownerConnectionStringSecretRef.name is required when bootstrap is disabled or PostgreSQL is external" }} +{{- end }} +{{- if and .Values.postgres.bootstrap (not .Values.postgres.enabled) (not .Values.postgres.adminSecretRef.name) }} +{{- fail "postgres.adminSecretRef.name is required when bootstrap is enabled without bundled PostgreSQL" }} {{- end }} apiVersion: v1 kind: ConfigMap @@ -23,5 +35,4 @@ metadata: name: {{ .Values.ateApiServerEnvVarsConfigMap }} namespace: {{ .Release.Namespace }} data: - ATE_API_POSTGRES_CONNECTION_STRING: {{ .Values.postgres.connectionString | default (printf "postgresql://postgres@%s.%s.svc:5432/atepg?sslmode=verify-full&sslrootcert=/run/servicedns.podcert.ate.dev/trust-bundle.pem&sslcert=/run/podidentity.podcert.ate.dev/credential-bundle.pem&sslkey=/run/podidentity.podcert.ate.dev/credential-bundle.pem" (include "substrate.fullname" (list "postgres" .)) .Release.Namespace) | quote }} ATE_API_POSTGRES_SCHEMA: {{ .Values.postgres.schema | quote }} diff --git a/charts/substrate/templates/ate-api-server.yaml b/charts/substrate/templates/ate-api-server.yaml index fc8e728e3d..2bc0ea4260 100644 --- a/charts/substrate/templates/ate-api-server.yaml +++ b/charts/substrate/templates/ate-api-server.yaml @@ -14,6 +14,16 @@ See the License for the specific language governing permissions and limitations under the License. */}} +{{- $readWriteConnectionStringSecretRef := .Values.postgres.readWriteConnectionStringSecretRef -}} +{{- $ownerConnectionStringSecretRef := .Values.postgres.ownerConnectionStringSecretRef -}} +{{- $bootstrapEnabled := .Values.postgres.bootstrap -}} +{{- if not (kindIs "bool" $bootstrapEnabled) -}} +{{- fail "postgres.bootstrap must be true or false" -}} +{{- end -}} +{{- if and $bootstrapEnabled (or (ne .Values.postgres.readWriteRole "substrate_readwrite") (ne .Values.postgres.ownerRole "substrate_owner")) -}} +{{- fail "postgres bootstrap requires readWriteRole=substrate_readwrite and ownerRole=substrate_owner" -}} +{{- end -}} + apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: @@ -85,8 +95,19 @@ spec: - "--grpc-listen-addr=0.0.0.0:443" - "--grpc-server-cred-bundle=/run/servicedns.podcert.ate.dev/credential-bundle.pem" - "--authentication-config=/etc/ateapi/authentication/authentication.yaml" - - "--postgres-connection-string=@env" + - "--postgres-read-write-connection-string=@file:/etc/ateapi/postgres/read-write/connection-string" + - "--postgres-owner-connection-string=@file:/etc/ateapi/postgres/owner/connection-string" + - {{ printf "--postgres-read-write-role=%s" .Values.postgres.readWriteRole | quote }} + - {{ printf "--postgres-owner-role=%s" .Values.postgres.ownerRole | quote }} - "--postgres-schema=@env" + - {{ printf "--postgres-bootstrap=%t" $bootstrapEnabled | quote }} +{{- if $bootstrapEnabled }} + - "--postgres-admin-username-file=/etc/ateapi/postgres/admin/username" + - "--postgres-admin-password-file=/etc/ateapi/postgres/admin/password" +{{- end }} +{{- with .Values.postgres.pool.maxConnLifetime }} + - {{ printf "--postgres-max-conn-lifetime=%s" . | quote }} +{{- end }} - "--actor-id-jwt-pool=/run/actor-id-jwt-pool/pool.json" - "--actor-id-ca-pool=/run/actor-id-ca-pool/pool.json" - "--egress-gateway-address={{ include "substrate.fullname" (list "atenet-egress" .) }}.{{ .Release.Namespace }}.svc:443" @@ -140,6 +161,17 @@ spec: - { name: actor-id-ca-pool, mountPath: /run/actor-id-ca-pool, readOnly: true } - { name: podidentity, mountPath: /run/podidentity.podcert.ate.dev, readOnly: true } - { name: authentication-config, mountPath: /etc/ateapi/authentication, readOnly: true } + - name: postgres-read-write-connection + mountPath: /etc/ateapi/postgres/read-write + readOnly: true + - name: postgres-owner-connection + mountPath: /etc/ateapi/postgres/owner + readOnly: true +{{- if $bootstrapEnabled }} + - name: postgres-admin + mountPath: /etc/ateapi/postgres/admin + readOnly: true +{{- end }} ports: - containerPort: 443 - name: prometheus @@ -201,6 +233,34 @@ spec: matchLabels: podcert.ate.dev/canarying: live path: trust-bundle.pem + - name: postgres-read-write-connection + projected: + sources: + - secret: + name: {{ include "substrate.postgres.readWriteSecretName" . | quote }} + items: + - key: {{ get $readWriteConnectionStringSecretRef "key" | default "readWriteConnectionString" | quote }} + path: connection-string + - name: postgres-owner-connection + projected: + sources: + - secret: + name: {{ include "substrate.postgres.ownerSecretName" . | quote }} + items: + - key: {{ get $ownerConnectionStringSecretRef "key" | default "ownerConnectionString" | quote }} + path: connection-string +{{- if $bootstrapEnabled }} + - name: postgres-admin + projected: + sources: + - secret: + name: {{ include "substrate.postgres.adminSecretName" . | quote }} + items: + - key: {{ .Values.postgres.adminSecretRef.usernameKey | quote }} + path: username + - key: {{ .Values.postgres.adminSecretRef.passwordKey | quote }} + path: password +{{- end }} --- apiVersion: policy/v1 kind: PodDisruptionBudget diff --git a/charts/substrate/templates/postgres-secrets.yaml b/charts/substrate/templates/postgres-secrets.yaml new file mode 100644 index 0000000000..ad97a74be0 --- /dev/null +++ b/charts/substrate/templates/postgres-secrets.yaml @@ -0,0 +1,54 @@ +{{/* +Copyright 2026 Google LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/}} + +{{- if .Values.postgres.enabled }} +{{- $host := printf "%s.%s.svc" (include "substrate.fullname" (list "postgres" .)) .Release.Namespace -}} +{{- /* The default service-DNS CA signs with Ed25519, which pgx cannot hash for SCRAM channel binding. TLS verification and client certificates remain required. */ -}} +{{- $tls := "sslmode=verify-full&sslrootcert=/run/servicedns.podcert.ate.dev/trust-bundle.pem&sslcert=/run/podidentity.podcert.ate.dev/credential-bundle.pem&sslkey=/run/podidentity.podcert.ate.dev/credential-bundle.pem&channel_binding=disable" -}} +{{- if not .Values.postgres.adminSecretRef.name }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "substrate.postgres.adminSecretName" . }} + namespace: {{ .Release.Namespace }} +type: Opaque +stringData: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres +--- +{{- end }} +{{- if not .Values.postgres.readWriteConnectionStringSecretRef.name }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "substrate.postgres.readWriteSecretName" . }} + namespace: {{ .Release.Namespace }} +type: Opaque +stringData: + readWriteConnectionString: {{ include "substrate.postgres.readWriteConnectionString" (dict "host" $host "database" .Values.postgres.database "params" $tls) | quote }} +--- +{{- end }} +{{- if not .Values.postgres.ownerConnectionStringSecretRef.name }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "substrate.postgres.ownerSecretName" . }} + namespace: {{ .Release.Namespace }} +type: Opaque +stringData: + ownerConnectionString: {{ include "substrate.postgres.ownerConnectionString" (dict "host" $host "database" .Values.postgres.database "params" $tls) | quote }} +{{- end }} +{{- end }} diff --git a/charts/substrate/templates/postgres.yaml b/charts/substrate/templates/postgres.yaml index 77df191879..3e791d3bc9 100644 --- a/charts/substrate/templates/postgres.yaml +++ b/charts/substrate/templates/postgres.yaml @@ -34,9 +34,8 @@ data: # health checks, the workload's idempotent database bootstrap, and the # tls-reloader sidecar's configuration reloads. local all all trust - # PostgreSQL verifies client certificates against the pod-identity CA. It - # does not need its own serving CA because it never verifies its server certificate. - hostssl all all all trust clientcert=verify-ca + # PostgreSQL requires a password and a pod-identity client certificate. + hostssl all all all scram-sha-256 clientcert=verify-ca reload-tls.sh: | # PostgreSQL opens ssl_cert_file, ssl_key_file and ssl_ca_file at startup # and on SIGHUP, and nowhere else. The kubelet replaces the projected pod @@ -67,7 +66,7 @@ data: # Starting empty also means a restart of this container costs one # redundant reload rather than a missed one. if [ "${current}" != "${reloaded}" ] \ - && psql -U postgres -d postgres -Atc 'SELECT pg_reload_conf()' >/dev/null 2>&1; then + && psql -U "${POSTGRES_USER}" -d "${POSTGRES_DB}" -Atc 'SELECT pg_reload_conf()' >/dev/null 2>&1; then reloaded="${current}" echo "$(date -u +%FT%TZ) reloaded TLS configuration" fi @@ -130,6 +129,14 @@ spec: command: - /bin/sh - /etc/postgresql/reload-tls.sh + env: + - name: POSTGRES_DB + value: {{ .Values.postgres.database | quote }} + - name: POSTGRES_USER + valueFrom: + secretKeyRef: + name: {{ include "substrate.postgres.adminSecretName" . }} + key: {{ .Values.postgres.adminSecretRef.usernameKey }} volumeMounts: - name: config mountPath: /etc/postgresql @@ -149,25 +156,19 @@ spec: containers: - name: postgres image: {{ include "substrate.thirdPartyImage" (list .Values.images.postgres .) }} - lifecycle: - postStart: - exec: - command: - - /bin/sh - - -ec - - | - until psql -U postgres -d postgres -Atc 'SELECT 1' >/dev/null 2>&1; do - sleep 1 - done - if ! psql -U postgres -d postgres -Atc \ - "SELECT 1 FROM pg_database WHERE datname = 'atepg'" | grep -qx 1; then - createdb -U postgres atepg - fi env: - name: POSTGRES_DB - value: atepg - - name: POSTGRES_HOST_AUTH_METHOD - value: trust + value: {{ .Values.postgres.database | quote }} + - name: POSTGRES_USER + valueFrom: + secretKeyRef: + name: {{ include "substrate.postgres.adminSecretName" . }} + key: {{ .Values.postgres.adminSecretRef.usernameKey }} + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: {{ include "substrate.postgres.adminSecretName" . }} + key: {{ .Values.postgres.adminSecretRef.passwordKey }} - name: PGDATA value: /var/lib/postgresql/data/pgdata ports: @@ -175,12 +176,12 @@ spec: containerPort: 5432 readinessProbe: exec: - command: ["/bin/sh", "-ec", "psql -U postgres -d atepg -Atc 'SELECT 1' >/dev/null"] + command: ["/bin/sh", "-ec", "pg_isready -U \"$POSTGRES_USER\" -d \"$POSTGRES_DB\""] initialDelaySeconds: 2 periodSeconds: 2 livenessProbe: exec: - command: ["pg_isready", "-U", "postgres", "-d", "postgres"] + command: ["/bin/sh", "-ec", "pg_isready -U \"$POSTGRES_USER\" -d \"$POSTGRES_DB\""] initialDelaySeconds: 10 periodSeconds: 10 args: ["-c", "config_file=/etc/postgresql/postgresql.conf"] diff --git a/charts/substrate/tests/postgres_test.yaml b/charts/substrate/tests/postgres_test.yaml new file mode 100644 index 0000000000..3fcfdbe257 --- /dev/null +++ b/charts/substrate/tests/postgres_test.yaml @@ -0,0 +1,280 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +suite: PostgreSQL configuration +templates: +- ate-api-server-envvars.yaml +- ate-api-server.yaml +- postgres-secrets.yaml +- postgres.yaml +tests: +- it: uses the managed schema by default + template: ate-api-server-envvars.yaml + asserts: + - notExists: + path: data.ATE_API_POSTGRES_READ_WRITE_CONNECTION_STRING + - equal: + path: data.ATE_API_POSTGRES_SCHEMA + value: substrate + +- it: uses separate managed identities by default + template: ate-api-server.yaml + documentIndex: 3 + asserts: + - contains: + path: spec.template.spec.containers[0].args + content: --postgres-read-write-connection-string=@file:/etc/ateapi/postgres/read-write/connection-string + - contains: + path: spec.template.spec.containers[0].args + content: --postgres-owner-connection-string=@file:/etc/ateapi/postgres/owner/connection-string + - contains: + path: spec.template.spec.containers[0].args + content: --postgres-read-write-role=substrate_readwrite + - contains: + path: spec.template.spec.containers[0].args + content: --postgres-owner-role=substrate_owner + - contains: + path: spec.template.spec.containers[0].args + content: --postgres-bootstrap=true + - notExists: + path: spec.template.spec.initContainers + - notContains: + path: spec.template.spec.containers[0].args + content: + --postgres-bootstrap + - contains: + path: spec.template.spec.containers[0].volumeMounts + content: + name: postgres-admin + mountPath: /etc/ateapi/postgres/admin + readOnly: true + +- it: rejects custom roles while bootstrap creates fixed identities + template: ate-api-server.yaml + set: + postgres.ownerRole: tenant_owner + asserts: + - failedTemplate: + errorMessage: postgres bootstrap requires readWriteRole=substrate_readwrite and ownerRole=substrate_owner + +- it: creates the managed administrator Secret + template: postgres-secrets.yaml + documentIndex: 0 + asserts: + - equal: + path: metadata.name + value: postgres-admin + - equal: + path: stringData.POSTGRES_USER + value: postgres + - equal: + path: stringData.POSTGRES_PASSWORD + value: postgres + +- it: creates the managed read-write Secret + template: postgres-secrets.yaml + documentIndex: 1 + asserts: + - equal: + path: metadata.name + value: RELEASE-NAME-postgres-readwrite + - matchRegex: + path: stringData.readWriteConnectionString + pattern: ^postgresql://substrate_readwrite_user:substrate-readwrite@ + - matchRegex: + path: stringData.readWriteConnectionString + pattern: channel_binding=disable$ + +- it: creates the managed owner Secret + template: postgres-secrets.yaml + documentIndex: 2 + asserts: + - equal: + path: metadata.name + value: RELEASE-NAME-postgres-owner + - matchRegex: + path: stringData.ownerConnectionString + pattern: ^postgresql://substrate_admin_user:substrate-admin@ + - matchRegex: + path: stringData.ownerConnectionString + pattern: channel_binding=disable$ + +- it: configures the bundled database from the administrator Secret + template: postgres.yaml + documentIndex: 2 + asserts: + - equal: + path: spec.template.spec.containers[0].env[0].value + value: atepg + - equal: + path: spec.template.spec.containers[0].env[1].valueFrom.secretKeyRef.name + value: postgres-admin + - equal: + path: spec.template.spec.containers[0].env[2].valueFrom.secretKeyRef.key + value: POSTGRES_PASSWORD + +- it: rejects operator-managed Secrets during bundled bootstrap + template: ate-api-server-envvars.yaml + set: + postgres.readWriteConnectionStringSecretRef.name: custom-readwrite + asserts: + - failedTemplate: + errorMessage: postgres.bootstrap requires the chart-managed application Secrets; disable bootstrap for operator-managed Secrets + +- it: supports an external BYO database without bootstrap + template: ate-api-server.yaml + documentIndex: 3 + set: + postgres: + enabled: false + bootstrap: false + readWriteConnectionStringSecretRef: + name: substrate-readwrite + key: connectionString + ownerConnectionStringSecretRef: + name: substrate-owner + key: connectionString + readWriteRole: tenant_readwrite + ownerRole: tenant_owner + asserts: + - notExists: + path: spec.template.spec.initContainers + - notExists: + path: spec.template.spec.volumes[?(@.name == "postgres-admin")] + - contains: + path: spec.template.spec.containers[0].args + content: --postgres-read-write-role=tenant_readwrite + - contains: + path: spec.template.spec.containers[0].args + content: --postgres-owner-role=tenant_owner + - equal: + path: spec.template.spec.volumes[?(@.name == "postgres-read-write-connection")].projected.sources[0].secret.name + value: substrate-readwrite + - equal: + path: spec.template.spec.volumes[?(@.name == "postgres-owner-connection")].projected.sources[0].secret.name + value: substrate-owner + - notExists: + path: spec.template.spec.volumes[?(@.name == "postgres-read-write-connection")].projected.sources[0].secret.items[1] + +- it: supports operator-managed users with bundled PostgreSQL + template: ate-api-server.yaml + documentIndex: 3 + set: + postgres: + enabled: true + bootstrap: false + readWriteConnectionStringSecretRef: + name: rotated-readwrite + ownerConnectionStringSecretRef: + name: rotated-owner + asserts: + - contains: + path: spec.template.spec.containers[0].args + content: --postgres-bootstrap=false + - notExists: + path: spec.template.spec.volumes[?(@.name == "postgres-admin")] + - equal: + path: spec.template.spec.volumes[?(@.name == "postgres-read-write-connection")].projected.sources[0].secret.name + value: rotated-readwrite + - equal: + path: spec.template.spec.volumes[?(@.name == "postgres-owner-connection")].projected.sources[0].secret.name + value: rotated-owner + +- it: supports break-glass bootstrap for an external database + template: ate-api-server.yaml + documentIndex: 3 + set: + postgres: + enabled: false + bootstrap: true + adminSecretRef: + name: external-postgres-admin + usernameKey: DB_USER + passwordKey: DB_PASSWORD + readWriteConnectionStringSecretRef: + name: substrate-readwrite + ownerConnectionStringSecretRef: + name: substrate-owner + asserts: + - contains: + path: spec.template.spec.containers[0].args + content: --postgres-bootstrap=true + - equal: + path: spec.template.spec.volumes[?(@.name == "postgres-admin")].projected.sources[0].secret.name + value: external-postgres-admin + - equal: + path: spec.template.spec.volumes[?(@.name == "postgres-admin")].projected.sources[0].secret.items[0].key + value: DB_USER + - equal: + path: spec.template.spec.volumes[?(@.name == "postgres-admin")].projected.sources[0].secret.items[1].key + value: DB_PASSWORD + +- it: requires an owner Secret for an external database + template: ate-api-server-envvars.yaml + set: + postgres: + enabled: false + bootstrap: false + readWriteConnectionStringSecretRef: + name: substrate-readwrite + asserts: + - failedTemplate: + errorMessage: postgres.ownerConnectionStringSecretRef.name is required when bootstrap is disabled or PostgreSQL is external + +- it: requires a read-write Secret when bundled bootstrap is disabled + template: ate-api-server-envvars.yaml + set: + postgres: + bootstrap: false + ownerConnectionStringSecretRef: + name: rotated-owner + asserts: + - failedTemplate: + errorMessage: postgres.readWriteConnectionStringSecretRef.name is required when bootstrap is disabled or PostgreSQL is external + +- it: requires an owner Secret when bundled bootstrap is disabled + template: ate-api-server-envvars.yaml + set: + postgres: + bootstrap: false + readWriteConnectionStringSecretRef: + name: rotated-readwrite + asserts: + - failedTemplate: + errorMessage: postgres.ownerConnectionStringSecretRef.name is required when bootstrap is disabled or PostgreSQL is external + +- it: configures the maximum connection lifetime + template: ate-api-server.yaml + documentIndex: 3 + set: + postgres: + pool: + maxConnLifetime: 10m + asserts: + - contains: + path: spec.template.spec.containers[0].args + content: --postgres-max-conn-lifetime=10m + +- it: requires an administrator Secret for external bootstrap + template: ate-api-server-envvars.yaml + set: + postgres: + enabled: false + readWriteConnectionStringSecretRef: + name: substrate-readwrite + ownerConnectionStringSecretRef: + name: substrate-owner + asserts: + - failedTemplate: + errorMessage: postgres.adminSecretRef.name is required when bootstrap is enabled without bundled PostgreSQL diff --git a/charts/substrate/values.yaml b/charts/substrate/values.yaml index 618be970b1..24976b23c5 100644 --- a/charts/substrate/values.yaml +++ b/charts/substrate/values.yaml @@ -25,9 +25,37 @@ createNamespace: false postgres: enabled: true + # Bundled PostgreSQL creates this database; both managed connection Secrets use it. + database: atepg storageSize: 1Gi - connectionString: "" - schema: public + # Read the read/write connection string from a Secret. + readWriteConnectionStringSecretRef: + # With bundled bootstrap, leave empty for the chart-managed fixed login. + # With bootstrap disabled, provide an existing Secret. + name: "" + key: readWriteConnectionString + # Read the owner connection string from a Secret. + ownerConnectionStringSecretRef: + # With bundled bootstrap, leave empty for the chart-managed fixed login. + # With bootstrap disabled, provide an existing Secret. + name: "" + key: ownerConnectionString + # Create missing fixed identities and schema. Set false when using operator-managed users. + bootstrap: true + # Roles assumed on each connection. Use distinct roles for installs sharing a + # database; bootstrap requires these fixed defaults. + readWriteRole: substrate_readwrite + ownerRole: substrate_owner + adminSecretRef: + # When empty with bundled PostgreSQL, the chart creates a managed Secret. + name: "" + usernameKey: POSTGRES_USER + passwordKey: POSTGRES_PASSWORD + pool: + # Bounds how long established connections can keep an old credential. + # Leave empty to use the upstream pgx default. + maxConnLifetime: "" + schema: substrate resources: requests: cpu: "1" @@ -66,7 +94,7 @@ credentialProvider: # allowedNamespaces: [team-a-secrets] # Name of a ConfigMap in the release namespace that supplies per-environment -# overrides for ate-api-server (ATE_API_POSTGRES_CONNECTION_STRING, ...). +# overrides for ate-api-server (ATE_API_POSTGRES_READ_WRITE_CONNECTION_STRING, ...). # Mounted via envFrom with optional=true. Created by the chart from these values. ateApiServerEnvVarsConfigMap: ate-api-server-envvars diff --git a/manifests/ate-install/ate-api-server-envvars.yaml b/manifests/ate-install/ate-api-server-envvars.yaml index b49cff6e1e..647574752d 100644 --- a/manifests/ate-install/ate-api-server-envvars.yaml +++ b/manifests/ate-install/ate-api-server-envvars.yaml @@ -21,5 +21,4 @@ metadata: name: ate-api-server-envvars namespace: ate-system data: - ATE_API_POSTGRES_CONNECTION_STRING: "postgresql://postgres@postgres.ate-system.svc:5432/atepg?sslmode=verify-full&sslrootcert=/run/servicedns.podcert.ate.dev/trust-bundle.pem&sslcert=/run/podidentity.podcert.ate.dev/credential-bundle.pem&sslkey=/run/podidentity.podcert.ate.dev/credential-bundle.pem" - ATE_API_POSTGRES_SCHEMA: "public" + ATE_API_POSTGRES_SCHEMA: "substrate" diff --git a/manifests/ate-install/postgres-secrets.yaml b/manifests/ate-install/postgres-secrets.yaml new file mode 100644 index 0000000000..3da4995fd2 --- /dev/null +++ b/manifests/ate-install/postgres-secrets.yaml @@ -0,0 +1,44 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# DO NOT EDIT — generated from charts/substrate by hack/render-manifests.sh. +# Run `make helm-template` to regenerate. + +apiVersion: v1 +kind: Secret +metadata: + name: postgres-admin + namespace: ate-system +type: Opaque +stringData: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres +--- +apiVersion: v1 +kind: Secret +metadata: + name: postgres-readwrite + namespace: ate-system +type: Opaque +stringData: + readWriteConnectionString: "postgresql://substrate_readwrite_user:substrate-readwrite@postgres.ate-system.svc:5432/atepg?sslmode=verify-full&sslrootcert=/run/servicedns.podcert.ate.dev/trust-bundle.pem&sslcert=/run/podidentity.podcert.ate.dev/credential-bundle.pem&sslkey=/run/podidentity.podcert.ate.dev/credential-bundle.pem&channel_binding=disable" +--- +apiVersion: v1 +kind: Secret +metadata: + name: postgres-owner + namespace: ate-system +type: Opaque +stringData: + ownerConnectionString: "postgresql://substrate_admin_user:substrate-admin@postgres.ate-system.svc:5432/atepg?sslmode=verify-full&sslrootcert=/run/servicedns.podcert.ate.dev/trust-bundle.pem&sslcert=/run/podidentity.podcert.ate.dev/credential-bundle.pem&sslkey=/run/podidentity.podcert.ate.dev/credential-bundle.pem&channel_binding=disable"