diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ffefca5..0dd15f45 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -60,6 +60,29 @@ written while it was being built. See [RELEASING.md](RELEASING.md). - Pages an organiser has not published no longer appear in the hackathon's navigation. They are reached through Manage Pages, which has moved above Manage Voting. +- Hackagon can now run against a Postgres it does not install — a managed cloud + database, or one an operator provisions. Set `postgresql.enabled` to `false`, + point `backend.config.database.host` and `keycloak.database.external.host` at + your server, and create the two databases yourself. Previously the chart + always addressed a server named after its own release, so there was no way to + reach anything else. +- The database password can now be read from a Kubernetes Secret you already + hold, via `backend.config.database.existingSecret` and + `existingSecretPasswordKey` (and the equivalent keys under + `keycloak.database.external`). This is what a secret store or a Postgres + operator needs: it writes the credentials, the chart reads them, and nobody + has to copy a password into a values file. + +### Changed + +- The backend's database password is no longer written into a ConfigMap. The + chart puts it in a Secret — its own, or the one you named — and hands it to + the backend as an environment variable. Before, anyone able to list ConfigMaps + in the namespace could read the database password. +- Fixed `keycloak.database.external.database` and `.user` being silently + ignored: the Keycloak chart calls them `name` and `username`, so a non-default + database name or user never reached Keycloak and it quietly used `keycloak` + for both. The values are now named to match and the setting takes effect. - A hackathon overview with no phase running no longer opens with "No phase is running" and a footnote saying the timeline follows the dates alone. The card diff --git a/helm-chart/Chart.yaml b/helm-chart/Chart.yaml index a262dfc5..65cb2299 100644 --- a/helm-chart/Chart.yaml +++ b/helm-chart/Chart.yaml @@ -6,7 +6,7 @@ type: application # The chart's own version. Bumped by hand when the chart changes, and # independent of the app: the CI publishes whatever it finds here. -version: 0.4.0 +version: 0.5.0 # The app release this chart deploys. A new app release does # not become deployable until someone points the chart at it. appVersion: "0.9.1" diff --git a/helm-chart/templates/NOTES.txt b/helm-chart/templates/NOTES.txt index 7a35c30b..97741ced 100644 --- a/helm-chart/templates/NOTES.txt +++ b/helm-chart/templates/NOTES.txt @@ -8,11 +8,17 @@ To get the generated frontend OIDC secrets, run: kubectl get secret {{ include "hackagon.fullname" . }}-frontend-secrets \ -n {{ .Release.Namespace }} -o jsonpath='{.data.secrets\.yaml}' | base64 -d -Passwords are set in your values files. Retrieve them with: +The backend reads its database password from a Secret: - # Platform (hackagon) user password - kubectl get secret {{ include "hackagon.fullname" . }}-postgresql \ + kubectl get secret {{ include "hackagon.backendDatabaseSecretName" . }} \ + -n {{ .Release.Namespace }} -o jsonpath='{.data.{{ include "hackagon.backendDatabaseSecretKey" . }}}' | base64 -d +{{- if .Values.postgresql.enabled }} + +The bundled postgres superuser password: + + kubectl get secret {{ .Release.Name }}-postgresql \ -n {{ .Release.Namespace }} -o jsonpath='{.data.postgres-password}' | base64 -d +{{- end }} Check the status of your release: diff --git a/helm-chart/templates/_helpers.tpl b/helm-chart/templates/_helpers.tpl index 9976d882..5fc43455 100644 --- a/helm-chart/templates/_helpers.tpl +++ b/helm-chart/templates/_helpers.tpl @@ -132,6 +132,34 @@ PostgreSQL service name (bitnami chart names it -postgresql) {{- printf "%s-postgresql" .Release.Name }} {{- end }} +{{/* +The postgres host the backend connects to. `backend.config.database.host` wins; +empty falls back to the bundled subchart's service, which is where this chart +puts postgres when `postgresql.enabled`. Rendered with `tpl`, like every other +host value. +*/}} +{{- define "hackagon.backendDatabaseHost" -}} +{{- $host := tpl (.Values.backend.config.database.host | default "") . }} +{{- $host | default (include "hackagon.postgresqlServiceName" .) }} +{{- end }} + +{{/* +Secret holding the backend's database password. An `existingSecret` is used as +given; otherwise the chart manages its own, so the password never lands in the +backend ConfigMap either way. +*/}} +{{- define "hackagon.backendDatabaseSecretName" -}} +{{- .Values.backend.config.database.existingSecret | default (printf "%s-backend-db" (include "hackagon.fullname" .)) }} +{{- end }} + +{{/* +Key within that Secret. The chart-managed Secret is written under the same key, +so both paths read the same way. +*/}} +{{- define "hackagon.backendDatabaseSecretKey" -}} +{{- .Values.backend.config.database.existingSecretPasswordKey | default "password" }} +{{- end }} + {{/* Get password: use provided value or generate one */}} diff --git a/helm-chart/templates/backend-configmap.yaml b/helm-chart/templates/backend-configmap.yaml index 5c4c559b..12aa4b2f 100644 --- a/helm-chart/templates/backend-configmap.yaml +++ b/helm-chart/templates/backend-configmap.yaml @@ -16,11 +16,14 @@ data: adminkeycloakid: {{ .Values.backend.config.server.adminkeycloakid | quote }} database: driver: {{ .Values.backend.config.database.driver | quote }} - host: {{ include "hackagon.postgresqlServiceName" . | quote }} + host: {{ include "hackagon.backendDatabaseHost" . | quote }} port: {{ .Values.backend.config.database.port }} dbname: {{ .Values.backend.config.database.dbname | quote }} user: {{ .Values.backend.config.database.user | quote }} - password: {{ .Values.backend.config.database.postgresPassword | required "postgresql.auth.postgresPassword is required" | quote }} + # No `password` here on purpose: a ConfigMap is world-readable to anything + # that can read the namespace. The backend reads it from the environment + # instead (HACKAGON_DATABASE_PASSWORD, set from a Secret in + # backend-deployment.yaml), which koanf layers over this file. oidc: jwksurl: {{ tpl .Values.backend.config.oidc.jwksurl . | quote }} issuerurl: {{ include "hackagon.oidcIssuer" . | quote }} diff --git a/helm-chart/templates/backend-deployment.yaml b/helm-chart/templates/backend-deployment.yaml index 9e499789..5ba593b3 100644 --- a/helm-chart/templates/backend-deployment.yaml +++ b/helm-chart/templates/backend-deployment.yaml @@ -30,6 +30,14 @@ spec: {{- end }} args: - "--config-dir=/etc/hackagon/" + env: + # Overrides `database.password` from the mounted config.yaml, which + # deliberately leaves it unset. + - name: HACKAGON_DATABASE_PASSWORD + valueFrom: + secretKeyRef: + name: {{ include "hackagon.backendDatabaseSecretName" . }} + key: {{ include "hackagon.backendDatabaseSecretKey" . }} ports: - name: grpc containerPort: 3000 diff --git a/helm-chart/templates/backend-postgres-secret.yaml b/helm-chart/templates/backend-postgres-secret.yaml new file mode 100644 index 00000000..3d518c41 --- /dev/null +++ b/helm-chart/templates/backend-postgres-secret.yaml @@ -0,0 +1,13 @@ +{{- if not .Values.backend.config.database.existingSecret }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "hackagon.backendDatabaseSecretName" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "hackagon.labels" . | nindent 4 }} + app.kubernetes.io/component: backend +type: Opaque +stringData: + {{ include "hackagon.backendDatabaseSecretKey" . }}: {{ .Values.backend.config.database.postgresPassword | required "backend.config.database.postgresPassword is required unless backend.config.database.existingSecret is set" | quote }} +{{- end }} diff --git a/helm-chart/templates/keycloak-init-configmap.yaml b/helm-chart/templates/keycloak-init-configmap.yaml index 78f2f04c..ffd74e93 100644 --- a/helm-chart/templates/keycloak-init-configmap.yaml +++ b/helm-chart/templates/keycloak-init-configmap.yaml @@ -1,3 +1,10 @@ +{{- if .Values.postgresql.enabled }} +{{/* +Bootstraps the roles and databases inside the postgres this chart installs. An +external postgres is expected to have them already, so with +`postgresql.enabled: false` this ConfigMap is not rendered at all and neither +password has to be given to the chart in plaintext. +*/}} apiVersion: v1 kind: ConfigMap metadata: @@ -7,9 +14,12 @@ metadata: {{- include "hackagon.labels" . | nindent 4 }} data: 01-create-keycloak-db.sql: | - CREATE USER keycloak WITH PASSWORD '{{ .Values.keycloak.database.external.password | required "keycloak.database.external.password is required" }}'; - CREATE DATABASE keycloak OWNER keycloak; - GRANT ALL PRIVILEGES ON DATABASE keycloak TO keycloak; - CREATE USER hackagon WITH PASSWORD '{{ .Values.backend.config.database.postgresPassword | required "backend.config.database.postgresPassword" }}'; - CREATE DATABASE hackagon OWNER hackagon; - GRANT ALL PRIVILEGES ON DATABASE hackagon TO hackagon; + {{- if .Values.keycloak.enabled }} + CREATE USER {{ .Values.keycloak.database.external.username }} WITH PASSWORD '{{ .Values.keycloak.database.external.password }}'; + CREATE DATABASE {{ .Values.keycloak.database.external.name }} OWNER {{ .Values.keycloak.database.external.username }}; + GRANT ALL PRIVILEGES ON DATABASE {{ .Values.keycloak.database.external.name }} TO {{ .Values.keycloak.database.external.username }}; + {{- end }} + CREATE USER {{ .Values.backend.config.database.user }} WITH PASSWORD '{{ .Values.backend.config.database.postgresPassword | required "backend.config.database.postgresPassword is required when postgresql.enabled is true" }}'; + CREATE DATABASE {{ .Values.backend.config.database.dbname }} OWNER {{ .Values.backend.config.database.user }}; + GRANT ALL PRIVILEGES ON DATABASE {{ .Values.backend.config.database.dbname }} TO {{ .Values.backend.config.database.user }}; +{{- end }} diff --git a/helm-chart/templates/keycloak-realm-configmap.yaml b/helm-chart/templates/keycloak-realm-configmap.yaml index a62204d6..54afa6fc 100644 --- a/helm-chart/templates/keycloak-realm-configmap.yaml +++ b/helm-chart/templates/keycloak-realm-configmap.yaml @@ -1,5 +1,3 @@ -{{- $host := .Values.keycloak.database.external.host | required "keycloak.database.external.host is required (e.g. \"hackagon-postgresql\")" -}} -{{- $keycloakPassword := .Values.keycloak.database.external.password | required "keycloak.database.external.password is required" -}} apiVersion: v1 kind: ConfigMap metadata: diff --git a/helm-chart/templates/validations.yaml b/helm-chart/templates/validations.yaml new file mode 100644 index 00000000..94a57270 --- /dev/null +++ b/helm-chart/templates/validations.yaml @@ -0,0 +1,32 @@ +{{/* +Value combinations the chart cannot render. Deliberately produces no manifest; +it exists so these failures are reported in one place with a usable message +rather than as a `required` deep inside an unrelated template. +*/}} + +{{- $db := .Values.backend.config.database }} + +{{/* +The bundled postgres bootstraps its databases from an initdb SQL script, which +is a ConfigMap: it can only be written with a password the chart can read. +*/}} +{{- if and .Values.postgresql.enabled $db.existingSecret }} + {{- fail "backend.config.database.existingSecret is set, but postgresql.enabled is true: the bundled postgres creates the hackagon role from an init script and cannot read a Secret. Either set backend.config.database.postgresPassword instead, or set postgresql.enabled=false and provision the database on your own postgres." }} +{{- end }} + +{{- if and $db.existingSecret $db.postgresPassword }} + {{- fail "backend.config.database.existingSecret and backend.config.database.password cannot be set at the same time. Choose one." }} +{{- end }} + +{{- if .Values.keycloak.enabled }} + {{- if and .Values.postgresql.enabled .Values.keycloak.database.external.existingSecret }} + {{- fail "keycloak.database.external.existingSecret is set, but postgresql.enabled is true: the bundled postgres creates the keycloak role from an init script and cannot read a Secret. Either set keycloak.database.external.password instead, or set postgresql.enabled=false and provision the database on your own postgres." }} + {{- end }} + {{- $_ := .Values.keycloak.database.external.host | required "keycloak.database.external.host is required (e.g. \"hackagon-postgresql\", or the hostname of an external postgres)" }} + {{- if not .Values.keycloak.database.external.existingSecret }} + {{- $_ := .Values.keycloak.database.external.password | required "keycloak.database.external.password is required unless keycloak.database.external.existingSecret is set" }} + {{- end }} + {{- if and .Values.keycloak.database.external.existingSecret .Values.keycloak.database.external.password }} + {{- fail "keycloak.database.external.existingSecret and keycloak.database.external.password cannot be set at the same time. Choose one." }} + {{- end }} +{{- end }} diff --git a/helm-chart/values.yaml b/helm-chart/values.yaml index ec8728de..91613a82 100644 --- a/helm-chart/values.yaml +++ b/helm-chart/values.yaml @@ -118,11 +118,25 @@ backend: adminkeycloakid: "" database: driver: postgres - host: "" # Auto-generated from release name in template + # -- Postgres host the backend connects to. Empty falls back to the + # bundled subchart's service (`-postgresql`). Set it to reach a + # postgres this chart does not manage — a managed provider, or one + # installed under a different release name. Rendered with `tpl`. + host: "" port: 5432 dbname: hackagon user: hackagon - postgresPassword: "" + # -- Password for `user`. The chart puts it in a Secret of its own and + # injects it as an env var, never into the backend ConfigMap. + postgresPassword: "" # Only use it existingSecret is empty + # -- Read the password from a Secret you already have instead of letting + # the chart manage one. Required by a postgres operator or an external + # secret store, which writes the credentials before the chart runs. + # Incompatible with `postgresql.enabled`: the bundled postgres bootstraps + # its roles from an init script and cannot read a Secret. + existingSecret: "" + # -- Key inside `existingSecret` holding the password. + existingSecretPasswordKey: password oidc: # -- Where the backend fetches Keycloak's signing keys. # With an external Keycloak (keycloak.enabled: false), @@ -164,15 +178,26 @@ keycloak: admin: username: hackagon-admin - # -- External database (reuse the same postgres instance) + # -- External database. Points at the bundled postgres by default, but any + # reachable postgres works. These keys are passed to the keycloak subchart + # verbatim, so they must use its names (`name`/`username`, not + # `database`/`user`). database: external: vendor: postgres - host: "" # Required: set to -postgresql (e.g. "hackagon-postgresql") + # -- Required: -postgresql (e.g. "hackagon-postgresql") for the + # bundled postgres, otherwise the external host. + host: "" port: 5432 - database: keycloak - user: keycloak - password: "" + name: keycloak + username: keycloak + password: "" # Only use it existingSecret is empty + # -- Read the password from a Secret you already have. Handled by the + # keycloak subchart. Incompatible with `postgresql.enabled`, for the same + # reason as the backend's. + existingSecret: "" + # -- Key inside `existingSecret` holding the password. + existingSecretPasswordKey: password # -- Realm import from ConfigMap realmImport: @@ -211,11 +236,18 @@ keycloakIngress: # ============================================================ # PostgreSQL (bitnami) — two databases, two users # ============================================================ +# Set `enabled: false` to run against a postgres this chart does not install. +# Then create the `hackagon` and `keycloak` roles and databases yourself, point +# `backend.config.database.host` and `keycloak.database.external.host` at it, +# and supply the passwords — either directly or from existing Secrets. postgresql: enabled: true auth: username: postgres database: postgres + # -- The bitnami subchart also accepts `auth.existingSecret` together with + # `auth.secretKeys.adminPasswordKey`, if you would rather not put the + # superuser password in values. postgresPassword: "" primary: