Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2497,7 +2497,6 @@ jobs:
run: |
args="$(kubectl --context "$CTX" --namespace gameplane-system get deployment gameplane-api \
-o jsonpath='{.spec.template.spec.containers[?(@.name=="api")].args}')"
echo "api args: ${args}"
if ! grep -q -- '--db-driver=postgres' <<<"${args}"; then
echo "the gameplane-api Deployment does not run with --db-driver=postgres" >&2
exit 1
Expand Down Expand Up @@ -2538,6 +2537,8 @@ jobs:
GAMEPLANE_E2E_REUSE_CLUSTER=1 \
GAMEPLANE_E2E_CLUSTER="$CLUSTER" \
go test -tags=e2e -timeout 15m -v -parallel 1 -run "$RUN_REGEX" ./...
- name: verify Secret-backed PostgreSQL configuration after upgrade
run: bash deploy/kind/check-postgres-secret.sh "$CTX"
- name: dump cluster state on failure (e2e-postgres)
if: failure()
uses: ./.github/actions/dump-cluster-state
Expand Down
5 changes: 5 additions & 0 deletions changelog/unreleased/887-postgres-secret.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# PostgreSQL connection secrets

The API and `bootstrap-admin` can read the database connection string from an existing
Kubernetes Secret. Set `api.db.existingSecret` and `api.db.secretKey` to keep the DSN out of
Helm values. [PR #887](https://github.com/GameplanePanel/Gameplane/pull/887)
9 changes: 9 additions & 0 deletions charts/gameplane/templates/api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,9 @@ spec:
{{- end }}
{{- end }}
- --db-driver={{ .Values.api.db.driver }}
{{- if not .Values.api.db.existingSecret }}
- --db-dsn={{ .Values.api.db.dsn }}
{{- end }}
{{- if .Values.api.oidc.enabled }}
- {{ printf "--oidc-issuer=%s" .Values.api.oidc.issuer | quote }}
- {{ printf "--oidc-client-id=%s" .Values.api.oidc.clientID | quote }}
Expand Down Expand Up @@ -385,7 +387,14 @@ spec:
- name: GAMEPLANE_DB_DRIVER
value: {{ .Values.api.db.driver | quote }}
- name: GAMEPLANE_DB_DSN
{{- if .Values.api.db.existingSecret }}
valueFrom:
secretKeyRef:
name: {{ .Values.api.db.existingSecret | quote }}
key: {{ .Values.api.db.secretKey | default "dsn" | quote }}
{{- else }}
value: {{ .Values.api.db.dsn | quote }}
{{- end }}
{{- if and (not .Values.api.standalone) .Values.clusterOps.enabled }}
- name: GAMEPLANE_CLUSTER_OPS
value: "true"
Expand Down
9 changes: 5 additions & 4 deletions charts/gameplane/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -224,10 +224,11 @@ api:
db:
driver: sqlite # sqlite | postgres
dsn: "file:/data/gameplane.db?_pragma=journal_mode(WAL)"
# When driver=postgres [experimental], set dsn to a postgres connection
# string and rebuild the api image with -tags postgres (the published
# image is built without it). Example:
# dsn: "postgres://gameplane:***@postgres/gameplane?sslmode=require"
# An existing Secret in the release namespace takes precedence over dsn.
# For postgres, store the connection string there and use an API image
# built with -tags postgres. Helm only needs the Secret name and key.
existingSecret: ""
secretKey: dsn

# Standalone encryption keys use separate storage configured under panelKey.
panelKey:
Expand Down
45 changes: 45 additions & 0 deletions deploy/kind/check-postgres-secret.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#!/usr/bin/env bash
# Runs after bootstrap, login and a populated Helm upgrade in e2e-postgres.
set -euo pipefail
k=(kubectl --context "$1" --namespace gameplane-system)
h=(helm --kube-context "$1" --namespace gameplane-system)
deployment="$("${k[@]}" get deployment gameplane-api -o json)"
api="$(jq -ce '.spec.template.spec.containers[] | select(.name == "api")' <<< "$deployment")"
jq -e '
(.args | all(startswith("--db-dsn") | not)) and
([.env[] | select(.name == "GAMEPLANE_DB_DSN")] ==
[{"name":"GAMEPLANE_DB_DSN","valueFrom":{"secretKeyRef":{"name":"gameplane-postgres","key":"dsn"}}}])
' <<< "$api" >/dev/null

# The password marker belongs only in the separately provisioned Kind fixture.
canary=e2e-postgres-password
pod="$("${k[@]}" get pods -l app.kubernetes.io/name=gameplane-api -o json)"
logs="$("${k[@]}" logs deployment/gameplane-api -c api)"
revisions="$("${h[@]}" history gameplane -o json | jq -r '.[].revision')"
for revision in $revisions; do
values="$("${h[@]}" get values gameplane --revision "$revision" --all)"
manifest="$("${h[@]}" get manifest gameplane --revision "$revision")"
if [[ "$values$manifest" == *"$canary"* ]]; then
echo "PostgreSQL credential found in Helm release revision $revision" >&2
exit 1
fi
done
if [[ "$deployment$pod$logs" == *"$canary"* ]]; then
echo "PostgreSQL credential found in API metadata or logs" >&2
exit 1
fi

# Use the installed API image and rendered env selector on disposable pods.
# Break one field at a time without disturbing the healthy API writer.
for field in name key; do
probe="missing-dsn-$field"
jq --arg field "$field" --arg probe "$probe" '{
apiVersion: "v1", kind: "Pod", metadata: {name: $probe},
spec: {restartPolicy: "Never", automountServiceAccountToken: false,
containers: [{name: "api", image, imagePullPolicy, args: ["--help"],
securityContext, env: [.env[] | select(.name == "GAMEPLANE_DB_DSN")
| .valueFrom.secretKeyRef[$field] = $probe]}]}}' <<< "$api" | "${k[@]}" create -f -
"${k[@]}" wait --for='jsonpath={.status.containerStatuses[0].state.waiting.reason}=CreateContainerConfigError' \
"pod/$probe" --timeout=90s
"${k[@]}" delete pod "$probe" --wait=false
done
4 changes: 1 addition & 3 deletions deploy/kind/e2e.sh
Original file line number Diff line number Diff line change
Expand Up @@ -401,9 +401,7 @@ EOF
--set networkPolicies.probePorts[0].port=25566)
fi
if [ "${PROFILE}" = postgres ]; then
dsn="$(kubectl get secret gameplane-postgres --namespace gameplane-system \
-o jsonpath='{.data.dsn}' | base64 -d)"
DB_SETS=(--set "api.db.driver=postgres" --set-string "api.db.dsn=${dsn}")
DB_SETS=(--set api.db.driver=postgres --set api.db.existingSecret=gameplane-postgres)
fi

echo "helm upgrade --install gameplane"
Expand Down
24 changes: 24 additions & 0 deletions docs/install.md
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,7 @@ Top-level knobs (see `values.yaml` for the full list):
`--set operator.gameDataStorage.storageClassName=fast-nvme`
- `api.db.driver` — `sqlite` (default, production-tested) or `postgres` [experimental] (requires an api image built with `-tags postgres`; the published image is built without it). The CI `api (postgres)` job and the PostgreSQL E2E buckets, including `postgres-availability` (`TestUpgrade_PostgresAvailability`), exercise it.
- `api.db.dsn` — connection string; SQLite default persists to a PVC
- `api.db.existingSecret` and `api.db.secretKey` — name of a pre-provisioned Secret in the release namespace and the key holding the complete DSN (default key `dsn`). A Secret reference takes precedence over the literal DSN for both the API and `bootstrap-admin`. A missing Secret or key prevents the API container from starting.
- `api.storage.existingClaim` — pre-existing PVC for the API's SQLite database (default `""`). PostgreSQL standalone deployments create no local database PVC or mount. The chart annotates `gameplane-api-data` with `helm.sh/resource-policy: keep` so switching to an existing claim preserves the previous PVC.
- `api.panelKey.storage.existingClaim` — pre-existing key-only PVC for generated standalone encryption keys; independent of database storage. `api.panelKey.storage.size` and `storageClassName` configure a new key PVC when no existing claim is selected.
- `api.panelKey.existingSecret` — mount an externally provisioned standalone key read-only instead of a key PVC. `api.panelKey.secretKey` selects the Secret entry (default `panel.key`); `api.panelKey.fileName` selects its mounted filename. Back up the database and matching key separately under different access policies. See [key custody](standalone-panel.md#central-panel-on-kubernetes).
Expand Down Expand Up @@ -1079,6 +1080,29 @@ a separate feature requiring database-level transaction serialization for
audit append/prune and user-management invariants, plus coordinated background
work and rate limiting.

### Moving a PostgreSQL DSN into a Secret

Provision an Opaque Secret in the API release namespace through your secret
manager. Store the complete connection string under the key `dsn`. Apply
these Helm values with the rest of the installation's configuration:

```yaml
api:
db:
driver: postgres
existingSecret: gameplane-postgres
secretKey: dsn
```

When migrating a literal DSN, remove the password-bearing `api.db.dsn` from
your values files and supply the full sanitized configuration with
`helm upgrade --reset-values`. Reusing old values can copy the old credential
into the next release record. Historical Helm revisions retain earlier
credentials. Rotate those credentials and restrict access to retained history.
After changing the Secret contents, restart the API Deployment so serve and
exec subcommands pick up the new environment. Check readiness and a database
login before revoking the previous credential.

### API database readiness

`/healthz` reports process liveness. `/readyz` pings the database with a
Expand Down
Loading