From b7bd8fc9a739329df39c9c49151ce60bbef7e5a8 Mon Sep 17 00:00:00 2001 From: mikkeldamsgaard Date: Wed, 25 Feb 2026 09:09:28 +0100 Subject: [PATCH] feat(netbird): structured database config with Initium init containers Replace raw DSN configuration with structured database.* values and add Initium init containers for database readiness and creation. Changes: - Replace server.secrets.storeDsn with structured database.type, database.host, database.port, database.user, database.name, database.passwordSecret, and database.sslMode - Construct DSN internally from structured values (postgresql, mysql) - Add db-wait init container using Initium wait-for (TCP probe, 120s timeout) - Add db-seed init container using Initium seed (create_if_missing: true) - Render seed spec as seed.yaml in server ConfigMap for non-sqlite engines - Inject DB_PASSWORD via secretKeyRef into config-init for DSN rendering - Upgrade Initium image to v1.0.0 (duration string timeout format) - Add CHANGELOG.md - Add example seed configurations under examples/seed/ - Update README with structured database docs, PostgreSQL/MySQL examples, and full values reference - Add 10 new unit tests (120 total) covering init container ordering, env var injection, and database-specific rendering - E2E tests verified with SQLite, PostgreSQL, and MySQL backends Closes #1 --- .github/workflows/ci.yaml | 8 +- .gitignore | 1 + CHANGELOG.md | 34 ++ charts/netbird/README.md | 79 +++- charts/netbird/ci/e2e-values-mysql.yaml | 23 +- charts/netbird/ci/e2e-values-postgres.yaml | 23 +- charts/netbird/ci/e2e-values.yaml | 6 +- charts/netbird/templates/NOTES.txt | 2 +- charts/netbird/templates/_helpers.tpl | 103 +++-- .../netbird/templates/server-configmap.yaml | 6 +- .../netbird/templates/server-deployment.yaml | 56 ++- .../server-configmap_test.yaml.snap | 106 ++++- .../netbird/tests/server-configmap_test.yaml | 39 +- .../netbird/tests/server-deployment_test.yaml | 163 +++++++- charts/netbird/values.yaml | 367 ++++-------------- ci/scripts/e2e.sh | 12 +- examples/seed/README.md | 64 +++ examples/seed/mysql-values.yaml | 64 +++ examples/seed/postgresql-values.yaml | 65 ++++ 19 files changed, 839 insertions(+), 382 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 examples/seed/README.md create mode 100644 examples/seed/mysql-values.yaml create mode 100644 examples/seed/postgresql-values.yaml diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 5192956..90b92fd 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -17,7 +17,7 @@ jobs: - name: Set up Helm uses: azure/setup-helm@v4 with: - version: v3.17.0 + version: v4.0.2 - name: Helm lint run: | @@ -49,7 +49,7 @@ jobs: - name: Set up Helm uses: azure/setup-helm@v4 with: - version: v3.17.0 + version: v4.0.2 - name: Create kind cluster uses: helm/kind-action@v1 @@ -82,7 +82,7 @@ jobs: - name: Set up Helm uses: azure/setup-helm@v4 with: - version: v3.17.0 + version: v4.0.2 - name: Create kind cluster uses: helm/kind-action@v1 @@ -117,7 +117,7 @@ jobs: - name: Set up Helm uses: azure/setup-helm@v4 with: - version: v3.17.0 + version: v4.0.2 - name: Create kind cluster uses: helm/kind-action@v1 diff --git a/.gitignore b/.gitignore index 8677b52..6ce244a 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ *.tgz charts/*/charts/*.tgz +CLAUDE.md \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..0a121d4 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,34 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/), +and this project adheres to [Semantic Versioning](https://semver.org/). + +## Unreleased + +### Changed + +- **Breaking:** Replaced raw DSN secret (`server.secrets.storeDsn`) with structured + `database.*` configuration. The chart now constructs the DSN internally from + `database.type`, `database.host`, `database.port`, `database.user`, `database.name`, + and `database.passwordSecret`. Users no longer need to build DSN strings. +- **Breaking:** Removed `server.config.store.engine`. Use `database.type` instead + (`sqlite`, `postgresql`, `mysql`). + +### Added + +- Structured database configuration via `database.*` values with per-engine defaults + (port 5432 for postgresql, 3306 for mysql). +- `database.sslMode` for PostgreSQL SSL mode control (default: `disable`). +- Initium `wait-for` init container: waits for external database to be reachable + before starting the server (TCP probe with 120s timeout and exponential backoff). +- Initium `seed` init container: creates the target database if it does not exist + via a declarative seed spec (`create_if_missing: true`). +- `DB_PASSWORD` environment variable injected into config-init via `secretKeyRef` + for DSN construction at render time. +- Seed spec rendered as `seed.yaml` in the server ConfigMap for non-sqlite engines. +- Unit tests for init container ordering, env var injection, and database-specific + rendering (120 tests, up from 110). +- CHANGELOG.md. + diff --git a/charts/netbird/README.md b/charts/netbird/README.md index 064b414..2740067 100644 --- a/charts/netbird/README.md +++ b/charts/netbird/README.md @@ -13,6 +13,11 @@ This chart deploys the NetBird self-hosted stack as two components: The server uses a single `config.yaml` that is rendered from a ConfigMap template with sensitive values injected at pod startup from Kubernetes Secrets via [Initium](https://github.com/KitStream/initium)'s `render` subcommand (envsubst mode). +For external databases (PostgreSQL, MySQL), the chart automatically: +1. **Waits** for the database to be reachable (`initium wait-for`) +2. **Creates** the database if it doesn't exist (`initium seed --spec`) +3. **Constructs** the DSN internally from structured `database.*` values — you never need to build a DSN string + ## Prerequisites - Kubernetes 1.24+ @@ -30,7 +35,55 @@ helm install netbird ./charts/netbird \ ## Minimal Configuration Example +### SQLite (default) + +```yaml +server: + config: + exposedAddress: "https://netbird.example.com" + auth: + issuer: "https://auth.example.com" + dashboardRedirectURIs: + - "https://netbird.example.com/nb-auth" + - "https://netbird.example.com/nb-silent-auth" +``` + +### PostgreSQL + +```yaml +database: + type: postgresql + host: postgres.database.svc.cluster.local + port: 5432 + user: netbird + name: netbird + passwordSecret: + secretName: netbird-db-password + secretKey: password + +server: + config: + exposedAddress: "https://netbird.example.com" + auth: + issuer: "https://auth.example.com" + dashboardRedirectURIs: + - "https://netbird.example.com/nb-auth" + - "https://netbird.example.com/nb-silent-auth" +``` + +### MySQL + ```yaml +database: + type: mysql + host: mysql.database.svc.cluster.local + port: 3306 + user: netbird + name: netbird + passwordSecret: + secretName: netbird-db-password + secretKey: password + server: config: exposedAddress: "https://netbird.example.com" @@ -39,6 +92,14 @@ server: dashboardRedirectURIs: - "https://netbird.example.com/nb-auth" - "https://netbird.example.com/nb-silent-auth" +``` + +The chart automatically constructs the DSN and adds init containers to wait for the database and create it if needed. + +For all configurations, add ingress settings: + +```yaml +server: ingress: enabled: true hosts: @@ -112,6 +173,19 @@ dashboard: | `serviceAccount.annotations` | object | `{}` | ServiceAccount annotations | | `serviceAccount.name` | string | `""` | ServiceAccount name override | +### Database + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| `database.type` | string | `"sqlite"` | Database engine (`sqlite`, `postgresql`, `mysql`) | +| `database.host` | string | `""` | Database hostname (required for postgresql/mysql) | +| `database.port` | string | `""` | Database port (defaults: 5432 for postgresql, 3306 for mysql) | +| `database.user` | string | `""` | Database user (required for postgresql/mysql) | +| `database.name` | string | `""` | Database name (required for postgresql/mysql) | +| `database.passwordSecret.secretName` | string | `""` | Secret containing the database password | +| `database.passwordSecret.secretKey` | string | `"password"` | Key in the Secret | +| `database.sslMode` | string | `"disable"` | SSL mode for PostgreSQL (ignored for mysql/sqlite) | + ### Server | Key | Type | Default | Description | @@ -121,7 +195,7 @@ dashboard: | `server.image.tag` | string | `""` (appVersion) | Server image tag | | `server.image.pullPolicy` | string | `"IfNotPresent"` | Image pull policy | | `server.initImage.repository` | string | `"ghcr.io/kitstream/initium"` | Init container image ([Initium](https://github.com/KitStream/initium)) | -| `server.initImage.tag` | string | `"0.1.2"` | Init container image tag | +| `server.initImage.tag` | string | `"1.0.0"` | Init container image tag | | `server.imagePullSecrets` | list | `[]` | Component-level pull secrets | #### Server Configuration @@ -140,7 +214,6 @@ dashboard: | `server.config.auth.signKeyRefreshEnabled` | bool | `true` | Auto-refresh IdP signing keys | | `server.config.auth.dashboardRedirectURIs` | list | `[]` | Dashboard OAuth2 redirect URIs | | `server.config.auth.cliRedirectURIs` | list | `["http://localhost:53000/"]` | CLI redirect URIs | -| `server.config.store.engine` | string | `"sqlite"` | Database engine (sqlite, postgres, mysql) | #### Server Secrets @@ -152,8 +225,6 @@ dashboard: | `server.secrets.storeEncryptionKey.secretName` | string | `""` | Existing Secret name (empty = auto-generate) | | `server.secrets.storeEncryptionKey.secretKey` | string | `"encryptionKey"` | Key in the Secret | | `server.secrets.storeEncryptionKey.autoGenerate` | bool | `true` | Auto-generate on first install | -| `server.secrets.storeDsn.secretName` | string | `""` | Secret containing the database DSN | -| `server.secrets.storeDsn.secretKey` | string | `"dsn"` | Key in the DSN Secret | #### Server Storage diff --git a/charts/netbird/ci/e2e-values-mysql.yaml b/charts/netbird/ci/e2e-values-mysql.yaml index 9c99da7..9c3e160 100644 --- a/charts/netbird/ci/e2e-values-mysql.yaml +++ b/charts/netbird/ci/e2e-values-mysql.yaml @@ -1,7 +1,17 @@ # E2E test values — MySQL backend in a kind cluster. # -# Requires: MySQL deployed in the same namespace before chart install. -# The e2e script deploys mysql via a simple Deployment + Service. +# The e2e script deploys mysql via a simple Deployment + Service +# and creates the password secret before chart install. + +database: + type: mysql + host: "mysql.netbird-e2e.svc.cluster.local" + port: 3306 + user: netbird + name: netbird + passwordSecret: + secretName: netbird-db-password + secretKey: password server: persistentVolume: @@ -17,13 +27,6 @@ server: dashboardRedirectURIs: - "https://netbird.localhost/nb-auth" - "https://netbird.localhost/nb-silent-auth" - store: - engine: mysql - - secrets: - storeDsn: - secretName: netbird-db-dsn - secretKey: dsn livenessProbe: failureThreshold: 10 @@ -47,5 +50,3 @@ dashboard: authAuthority: "https://auth.localhost" authClientId: "test-client" authAudience: "test-audience" - - diff --git a/charts/netbird/ci/e2e-values-postgres.yaml b/charts/netbird/ci/e2e-values-postgres.yaml index b7540d1..847ecc1 100644 --- a/charts/netbird/ci/e2e-values-postgres.yaml +++ b/charts/netbird/ci/e2e-values-postgres.yaml @@ -1,7 +1,18 @@ # E2E test values — PostgreSQL backend in a kind cluster. # -# Requires: PostgreSQL deployed in the same namespace before chart install. -# The e2e script deploys postgres via a simple Deployment + Service. +# The e2e script deploys postgres via a simple Deployment + Service +# and creates the password secret before chart install. + +database: + type: postgresql + host: "postgres.netbird-e2e.svc.cluster.local" + port: 5432 + user: netbird + name: netbird + passwordSecret: + secretName: netbird-db-password + secretKey: password + sslMode: disable server: persistentVolume: @@ -17,13 +28,6 @@ server: dashboardRedirectURIs: - "https://netbird.localhost/nb-auth" - "https://netbird.localhost/nb-silent-auth" - store: - engine: postgres - - secrets: - storeDsn: - secretName: netbird-db-dsn - secretKey: dsn livenessProbe: failureThreshold: 10 @@ -48,4 +52,3 @@ dashboard: authClientId: "test-client" authAudience: "test-audience" - diff --git a/charts/netbird/ci/e2e-values.yaml b/charts/netbird/ci/e2e-values.yaml index 130797e..f16c04a 100644 --- a/charts/netbird/ci/e2e-values.yaml +++ b/charts/netbird/ci/e2e-values.yaml @@ -7,6 +7,9 @@ # - Uses sqlite engine with auto-generated secrets # - Sets dummy auth values (server will start but auth won't be functional) +database: + type: sqlite + server: persistentVolume: enabled: false @@ -21,8 +24,6 @@ server: dashboardRedirectURIs: - "https://netbird.localhost/nb-auth" - "https://netbird.localhost/nb-silent-auth" - store: - engine: sqlite # Lower probe thresholds for faster feedback in CI livenessProbe: @@ -47,4 +48,3 @@ dashboard: authAuthority: "https://auth.localhost" authClientId: "test-client" authAudience: "test-audience" - diff --git a/charts/netbird/templates/NOTES.txt b/charts/netbird/templates/NOTES.txt index d88e6c9..b125ca8 100644 --- a/charts/netbird/templates/NOTES.txt +++ b/charts/netbird/templates/NOTES.txt @@ -33,4 +33,4 @@ Components: {{- end }} {{- end }} - Database: {{ .Values.server.config.store.engine }} + Database: {{ .Values.database.type }} diff --git a/charts/netbird/templates/_helpers.tpl b/charts/netbird/templates/_helpers.tpl index 51778b4..eb6b613 100644 --- a/charts/netbird/templates/_helpers.tpl +++ b/charts/netbird/templates/_helpers.tpl @@ -98,29 +98,22 @@ from a map of ENV_VAR: "secretName/secretKey" {{- end }} {{/* -netbird.escapeEnvsubst — escapes a string so that envsubst will not -interpret any ${...} or $VAR references inside user-supplied values. -All "$" characters are replaced with the literal string "${DOLLAR}" -and the init container pre-defines DOLLAR='$' as an env var before -Initium's render subcommand runs envsubst. +netbird.escapeEnvsubst — escapes "$" to "${DOLLAR}" so Initium's +render subcommand (envsubst mode) won't interpret user values. */}} {{- define "netbird.escapeEnvsubst" -}} {{- . | replace "$" "${DOLLAR}" }} {{- end }} {{/* -netbird.server.generatedSecretName — name of the Secret this chart creates -when auto-generating secrets (authSecret, storeEncryptionKey). +netbird.server.generatedSecretName — name of the auto-generated Secret. */}} {{- define "netbird.server.generatedSecretName" -}} {{- printf "%s-generated" (include "netbird.server.fullname" .) | trunc 63 | trimSuffix "-" }} {{- end }} {{/* -netbird.server.resolveSecretName — resolves the effective secret name for a -given secret ref. If the user supplied a secretName, use it. Otherwise, if -autoGenerate is true, use the chart-generated secret name. Otherwise return "". -Usage: include "netbird.server.resolveSecretName" (dict "ref" .Values.server.secrets.authSecret "generated" (include "netbird.server.generatedSecretName" .)) +netbird.server.resolveSecretName — resolves the effective secret name. */}} {{- define "netbird.server.resolveSecretName" -}} {{- if .ref.secretName -}} @@ -130,23 +123,61 @@ Usage: include "netbird.server.resolveSecretName" (dict "ref" .Values.server.sec {{- end -}} {{- end }} +{{/* ===== Database helpers ===== */}} + +{{/* +netbird.database.engine — maps database.type to the NetBird store engine name. + postgresql -> postgres, mysql -> mysql, sqlite -> sqlite +*/}} +{{- define "netbird.database.engine" -}} +{{- if eq .Values.database.type "postgresql" -}}postgres +{{- else -}}{{ .Values.database.type }} +{{- end -}} +{{- end }} + +{{/* +netbird.database.port — resolves the effective database port. +Defaults to 5432 for postgresql, 3306 for mysql. +*/}} +{{- define "netbird.database.port" -}} +{{- if .Values.database.port -}} +{{- .Values.database.port -}} +{{- else if eq .Values.database.type "postgresql" -}}5432 +{{- else if eq .Values.database.type "mysql" -}}3306 +{{- else -}}0 +{{- end -}} +{{- end }} + +{{/* +netbird.database.isExternal — true when database.type is not sqlite. +*/}} +{{- define "netbird.database.isExternal" -}} +{{- ne .Values.database.type "sqlite" -}} +{{- end }} + +{{/* +netbird.database.dsn — constructs the DSN string with ${DB_PASSWORD} placeholder. + postgresql: host=H user=U password=${DB_PASSWORD} dbname=D port=P sslmode=S + mysql: U:${DB_PASSWORD}@tcp(H:P)/D + sqlite: (empty string) +*/}} +{{- define "netbird.database.dsn" -}} +{{- if eq .Values.database.type "postgresql" -}} +host={{ .Values.database.host }} user={{ .Values.database.user }} password=${DB_PASSWORD} dbname={{ .Values.database.name }} port={{ include "netbird.database.port" . }} sslmode={{ .Values.database.sslMode }} +{{- else if eq .Values.database.type "mysql" -}} +{{ .Values.database.user }}:${DB_PASSWORD}@tcp({{ .Values.database.host }}:{{ include "netbird.database.port" . }})/{{ .Values.database.name }} +{{- end -}} +{{- end }} + {{/* netbird.server.configTemplate — renders the config.yaml template with -envsubst-style placeholders for sensitive values that the Initium init -container will substitute at runtime using its `render` subcommand -(envsubst mode). +envsubst-style placeholders. Initium's render subcommand substitutes +these at pod startup. -Placeholders (envsubst variables): +Placeholders: ${AUTH_SECRET} <- server.secrets.authSecret ${ENCRYPTION_KEY} <- server.secrets.storeEncryptionKey - ${STORE_DSN} <- server.secrets.storeDsn (only for postgres/mysql) - -All user-supplied values are escaped via the netbird.escapeEnvsubst helper -so that any "$" in user input is rendered literally and not interpreted by -envsubst. - -The generated structure matches the official NetBird config.yaml format: - https://docs.netbird.io/selfhosted/configuration-files + ${DB_PASSWORD} <- database.passwordSecret (embedded in DSN, non-sqlite only) */}} {{- define "netbird.server.configTemplate" -}} server: @@ -179,7 +210,29 @@ server: {{- end }} store: - engine: {{ include "netbird.escapeEnvsubst" .Values.server.config.store.engine | quote }} - dsn: {{ if ne .Values.server.config.store.engine "sqlite" }}"${STORE_DSN}"{{ else }}""{{ end }} + engine: {{ include "netbird.database.engine" . | quote }} + dsn: {{ if eq (include "netbird.database.isExternal" .) "true" }}"{{ include "netbird.database.dsn" . }}"{{ else }}""{{ end }} encryptionKey: "${ENCRYPTION_KEY}" {{- end }} + +{{/* +netbird.database.seedSpec — renders the Initium seed spec YAML for +creating the target database if it doesn't exist. +Only rendered for non-sqlite database types. + +The spec is a MiniJinja template — {{ env.DB_PASSWORD }} is resolved +by Initium at runtime from the DB_PASSWORD environment variable. +*/}} +{{- define "netbird.database.seedSpec" -}} +database: + driver: {{ include "netbird.database.engine" . }} +{{- if eq .Values.database.type "postgresql" }} + url: "postgres://{{ .Values.database.user }}:{{ "{{ env.DB_PASSWORD }}" }}@{{ .Values.database.host }}:{{ include "netbird.database.port" . }}/?sslmode={{ .Values.database.sslMode }}" +{{- else if eq .Values.database.type "mysql" }} + url: "mysql://{{ .Values.database.user }}:{{ "{{ env.DB_PASSWORD }}" }}@{{ .Values.database.host }}:{{ include "netbird.database.port" . }}/{{ .Values.database.name }}" +{{- end }} +phases: + - name: create-database + database: {{ .Values.database.name }} + create_if_missing: true +{{- end }} diff --git a/charts/netbird/templates/server-configmap.yaml b/charts/netbird/templates/server-configmap.yaml index bc618a5..2fbdca9 100644 --- a/charts/netbird/templates/server-configmap.yaml +++ b/charts/netbird/templates/server-configmap.yaml @@ -1,4 +1,4 @@ -apiVersion: v1 +apiVersion: v1 kind: ConfigMap metadata: name: {{ include "netbird.server.fullname" . }} @@ -8,3 +8,7 @@ metadata: data: config.yaml.tpl: | {{- include "netbird.server.configTemplate" . | nindent 4 }} + {{- if eq (include "netbird.database.isExternal" .) "true" }} + seed.yaml: | + {{- include "netbird.database.seedSpec" . | nindent 4 }} + {{- end }} diff --git a/charts/netbird/templates/server-deployment.yaml b/charts/netbird/templates/server-deployment.yaml index d212bfb..2a9fe25 100644 --- a/charts/netbird/templates/server-deployment.yaml +++ b/charts/netbird/templates/server-deployment.yaml @@ -35,11 +35,51 @@ spec: {{- $generated := include "netbird.server.generatedSecretName" . }} {{- $authSecretName := include "netbird.server.resolveSecretName" (dict "ref" .Values.server.secrets.authSecret "generated" $generated) }} {{- $encKeySecretName := include "netbird.server.resolveSecretName" (dict "ref" .Values.server.secrets.storeEncryptionKey "generated" $generated) }} - {{- $dsnSecretName := "" }} - {{- if and (ne .Values.server.config.store.engine "sqlite") .Values.server.secrets.storeDsn.secretName }} - {{- $dsnSecretName = .Values.server.secrets.storeDsn.secretName }} - {{- end }} + {{- $isExternal := eq (include "netbird.database.isExternal" .) "true" }} initContainers: + {{- if $isExternal }} + # ── Wait for database to be reachable ────────────────────────── + - name: db-wait + image: "{{ .Values.server.initImage.repository }}:{{ .Values.server.initImage.tag }}" + args: + - wait-for + - --target + - "tcp://{{ .Values.database.host }}:{{ include "netbird.database.port" . }}" + - --timeout + - "120s" + securityContext: + runAsNonRoot: true + runAsUser: 65534 + readOnlyRootFilesystem: true + allowPrivilegeEscalation: false + capabilities: + drop: [ALL] + # ── Create database if missing ───────────────────────────────── + - name: db-seed + image: "{{ .Values.server.initImage.repository }}:{{ .Values.server.initImage.tag }}" + args: + - seed + - --spec + - /spec/seed.yaml + env: + - name: DB_PASSWORD + valueFrom: + secretKeyRef: + name: {{ .Values.database.passwordSecret.secretName }} + key: {{ .Values.database.passwordSecret.secretKey }} + securityContext: + runAsNonRoot: true + runAsUser: 65534 + readOnlyRootFilesystem: true + allowPrivilegeEscalation: false + capabilities: + drop: [ALL] + volumeMounts: + - name: config-tpl + mountPath: /spec + readOnly: true + {{- end }} + # ── Render config template ───────────────────────────────────── - name: config-init image: "{{ .Values.server.initImage.repository }}:{{ .Values.server.initImage.tag }}" args: @@ -73,12 +113,12 @@ spec: - name: ENCRYPTION_KEY value: "" {{- end }} - {{- if $dsnSecretName }} - - name: STORE_DSN + {{- if $isExternal }} + - name: DB_PASSWORD valueFrom: secretKeyRef: - name: {{ $dsnSecretName }} - key: {{ .Values.server.secrets.storeDsn.secretKey }} + name: {{ .Values.database.passwordSecret.secretName }} + key: {{ .Values.database.passwordSecret.secretKey }} {{- end }} securityContext: runAsNonRoot: true diff --git a/charts/netbird/tests/__snapshot__/server-configmap_test.yaml.snap b/charts/netbird/tests/__snapshot__/server-configmap_test.yaml.snap index 725849b..9a67150 100644 --- a/charts/netbird/tests/__snapshot__/server-configmap_test.yaml.snap +++ b/charts/netbird/tests/__snapshot__/server-configmap_test.yaml.snap @@ -24,6 +24,40 @@ should escape dollar signs in user-supplied values: engine: "sqlite" dsn: "" encryptionKey: "${ENCRYPTION_KEY}" +should include seed.yaml for postgresql: + 1: | + config.yaml.tpl: | + server: + listenAddress: ":80" + exposedAddress: "" + stunPorts: + - 3478 + metricsPort: 9090 + healthcheckAddress: ":9000" + logLevel: "info" + logFile: "console" + + authSecret: "${AUTH_SECRET}" + dataDir: "/var/lib/netbird" + + auth: + issuer: "" + signKeyRefreshEnabled: true + cliRedirectURIs: + - "http://localhost:53000/" + + store: + engine: "postgres" + dsn: "host=pg.example.com user=netbird password=${DB_PASSWORD} dbname=netbird port=5432 sslmode=disable" + encryptionKey: "${ENCRYPTION_KEY}" + seed.yaml: | + database: + driver: postgres + url: "postgres://netbird:{{ env.DB_PASSWORD }}@pg.example.com:5432/?sslmode=disable" + phases: + - name: create-database + database: netbird + create_if_missing: true should match default config snapshot: 1: | config.yaml.tpl: | @@ -50,6 +84,32 @@ should match default config snapshot: engine: "sqlite" dsn: "" encryptionKey: "${ENCRYPTION_KEY}" +should not include seed.yaml for sqlite: + 1: | + config.yaml.tpl: | + server: + listenAddress: ":80" + exposedAddress: "" + stunPorts: + - 3478 + metricsPort: 9090 + healthcheckAddress: ":9000" + logLevel: "info" + logFile: "console" + + authSecret: "${AUTH_SECRET}" + dataDir: "/var/lib/netbird" + + auth: + issuer: "" + signKeyRefreshEnabled: true + cliRedirectURIs: + - "http://localhost:53000/" + + store: + engine: "sqlite" + dsn: "" + encryptionKey: "${ENCRYPTION_KEY}" should render auth issuer from values: 1: | config.yaml.tpl: | @@ -157,7 +217,41 @@ should render listen address from values: engine: "sqlite" dsn: "" encryptionKey: "${ENCRYPTION_KEY}" -should render postgres engine with DSN placeholder: +should render mysql engine with constructed DSN: + 1: | + config.yaml.tpl: | + server: + listenAddress: ":80" + exposedAddress: "" + stunPorts: + - 3478 + metricsPort: 9090 + healthcheckAddress: ":9000" + logLevel: "info" + logFile: "console" + + authSecret: "${AUTH_SECRET}" + dataDir: "/var/lib/netbird" + + auth: + issuer: "" + signKeyRefreshEnabled: true + cliRedirectURIs: + - "http://localhost:53000/" + + store: + engine: "mysql" + dsn: "netbird:${DB_PASSWORD}@tcp(mysql.example.com:3306)/netbird" + encryptionKey: "${ENCRYPTION_KEY}" + seed.yaml: | + database: + driver: mysql + url: "mysql://netbird:{{ env.DB_PASSWORD }}@mysql.example.com:3306/netbird" + phases: + - name: create-database + database: netbird + create_if_missing: true +should render postgres engine with constructed DSN: 1: | config.yaml.tpl: | server: @@ -181,8 +275,16 @@ should render postgres engine with DSN placeholder: store: engine: "postgres" - dsn: "${STORE_DSN}" + dsn: "host=pg.example.com user=netbird password=${DB_PASSWORD} dbname=netbird port=5432 sslmode=disable" encryptionKey: "${ENCRYPTION_KEY}" + seed.yaml: | + database: + driver: postgres + url: "postgres://netbird:{{ env.DB_PASSWORD }}@pg.example.com:5432/?sslmode=disable" + phases: + - name: create-database + database: netbird + create_if_missing: true should render sqlite engine with empty DSN: 1: | config.yaml.tpl: | diff --git a/charts/netbird/tests/server-configmap_test.yaml b/charts/netbird/tests/server-configmap_test.yaml index 3a2155c..8c5127a 100644 --- a/charts/netbird/tests/server-configmap_test.yaml +++ b/charts/netbird/tests/server-configmap_test.yaml @@ -37,14 +37,47 @@ tests: - it: should render sqlite engine with empty DSN set: - server.config.store.engine: sqlite + database.type: sqlite asserts: - matchSnapshot: path: data - - it: should render postgres engine with DSN placeholder + - it: should render postgres engine with constructed DSN set: - server.config.store.engine: postgres + database.type: postgresql + database.host: pg.example.com + database.user: netbird + database.name: netbird + database.passwordSecret.secretName: my-pg-secret + asserts: + - matchSnapshot: + path: data + + - it: should render mysql engine with constructed DSN + set: + database.type: mysql + database.host: mysql.example.com + database.user: netbird + database.name: netbird + database.passwordSecret.secretName: my-mysql-secret + asserts: + - matchSnapshot: + path: data + + - it: should include seed.yaml for postgresql + set: + database.type: postgresql + database.host: pg.example.com + database.user: netbird + database.name: netbird + database.passwordSecret.secretName: my-pg-secret + asserts: + - matchSnapshot: + path: data + + - it: should not include seed.yaml for sqlite + set: + database.type: sqlite asserts: - matchSnapshot: path: data diff --git a/charts/netbird/tests/server-deployment_test.yaml b/charts/netbird/tests/server-deployment_test.yaml index 1a04716..3561dd0 100644 --- a/charts/netbird/tests/server-deployment_test.yaml +++ b/charts/netbird/tests/server-deployment_test.yaml @@ -135,6 +135,26 @@ tests: name: data emptyDir: {} + # ── Init container: config-init ────────────────────────────────────── + + - it: should use config-init container with initium image + asserts: + - equal: + path: spec.template.spec.initContainers[0].name + value: config-init + - equal: + path: spec.template.spec.initContainers[0].image + value: "ghcr.io/kitstream/initium:1.0.0" + + - it: should have only config-init for sqlite + asserts: + - lengthEqual: + path: spec.template.spec.initContainers + count: 1 + - equal: + path: spec.template.spec.initContainers[0].name + value: config-init + - it: should inject auto-generated secrets via env vars by default asserts: - contains: @@ -178,27 +198,138 @@ tests: name: my-enc-secret key: encryptionKey - - it: should inject DSN secret via env var for postgres engine + - it: should not inject DB_PASSWORD env var for sqlite + asserts: + - notContains: + path: spec.template.spec.initContainers[0].env + content: + name: DB_PASSWORD + any: true + + # ── Init containers: db-wait + db-seed for external DB ─────────────── + + - it: should add db-wait and db-seed init containers for postgresql + set: + database.type: postgresql + database.host: pg.example.com + database.user: netbird + database.name: netbird + database.passwordSecret.secretName: my-pg-secret + asserts: + - lengthEqual: + path: spec.template.spec.initContainers + count: 3 + - equal: + path: spec.template.spec.initContainers[0].name + value: db-wait + - equal: + path: spec.template.spec.initContainers[1].name + value: db-seed + - equal: + path: spec.template.spec.initContainers[2].name + value: config-init + + - it: should add db-wait and db-seed init containers for mysql + set: + database.type: mysql + database.host: mysql.example.com + database.user: netbird + database.name: netbird + database.passwordSecret.secretName: my-mysql-secret + asserts: + - lengthEqual: + path: spec.template.spec.initContainers + count: 3 + - equal: + path: spec.template.spec.initContainers[0].name + value: db-wait + - equal: + path: spec.template.spec.initContainers[1].name + value: db-seed + - equal: + path: spec.template.spec.initContainers[2].name + value: config-init + + - it: should configure db-wait with correct target for postgresql set: - server.config.store.engine: postgres - server.secrets.storeDsn.secretName: my-db-secret + database.type: postgresql + database.host: pg.example.com + database.port: 5432 + database.user: netbird + database.name: netbird + database.passwordSecret.secretName: my-pg-secret asserts: - contains: - path: spec.template.spec.initContainers[0].env + path: spec.template.spec.initContainers[0].args + content: "tcp://pg.example.com:5432" + + - it: should configure db-wait with default port for mysql + set: + database.type: mysql + database.host: mysql.example.com + database.user: netbird + database.name: netbird + database.passwordSecret.secretName: my-mysql-secret + asserts: + - contains: + path: spec.template.spec.initContainers[0].args + content: "tcp://mysql.example.com:3306" + + - it: should inject DB_PASSWORD into db-seed for postgresql + set: + database.type: postgresql + database.host: pg.example.com + database.user: netbird + database.name: netbird + database.passwordSecret.secretName: my-pg-secret + database.passwordSecret.secretKey: dbpass + asserts: + - contains: + path: spec.template.spec.initContainers[1].env content: - name: STORE_DSN + name: DB_PASSWORD valueFrom: secretKeyRef: - name: my-db-secret - key: dsn + name: my-pg-secret + key: dbpass - - it: should not inject DSN env var for sqlite engine + - it: should inject DB_PASSWORD into config-init for postgresql + set: + database.type: postgresql + database.host: pg.example.com + database.user: netbird + database.name: netbird + database.passwordSecret.secretName: my-pg-secret + database.passwordSecret.secretKey: dbpass asserts: - - notContains: - path: spec.template.spec.initContainers[0].env + - contains: + path: spec.template.spec.initContainers[2].env content: - name: STORE_DSN - any: true + name: DB_PASSWORD + valueFrom: + secretKeyRef: + name: my-pg-secret + key: dbpass + + - it: should set hardened securityContext on all init containers for postgresql + set: + database.type: postgresql + database.host: pg.example.com + database.user: netbird + database.name: netbird + database.passwordSecret.secretName: my-pg-secret + asserts: + - equal: + path: spec.template.spec.initContainers[0].securityContext.runAsNonRoot + value: true + - equal: + path: spec.template.spec.initContainers[1].securityContext.runAsNonRoot + value: true + - equal: + path: spec.template.spec.initContainers[2].securityContext.runAsNonRoot + value: true + + # ── Other deployment settings ──────────────────────────────────────── - it: should set resource limits when specified set: @@ -268,12 +399,4 @@ tests: value: readOnlyRootFilesystem: true - - it: should use init container with initium image - asserts: - - equal: - path: spec.template.spec.initContainers[0].name - value: config-init - - equal: - path: spec.template.spec.initContainers[0].image - value: "ghcr.io/kitstream/initium:0.1.2" diff --git a/charts/netbird/values.yaml b/charts/netbird/values.yaml index f917cfe..77045d5 100644 --- a/charts/netbird/values.yaml +++ b/charts/netbird/values.yaml @@ -26,6 +26,56 @@ serviceAccount: # referencing a Kubernetes docker-registry secret. imagePullSecrets: [] +# ============================================================================= +# Database Configuration +# ============================================================================= +# Structured database connection parameters. The chart constructs the +# internal DSN from these fields — you never need to build a DSN string. +# +# For sqlite, only `database.type` is needed (no host/port/user/password). +# For postgresql and mysql, the chart: +# 1. Waits for the database to be reachable (Initium wait-for) +# 2. Creates the database if it doesn't exist (Initium seed) +# 3. Constructs the DSN and renders it into the server config +# ============================================================================= +database: + # -- Database engine. Options: sqlite, postgresql, mysql. + type: "sqlite" + + # -- Database hostname. Required for postgresql and mysql. + host: "" + + # -- Database port. Defaults to 5432 for postgresql, 3306 for mysql. + # Ignored for sqlite. + port: "" + + # -- Database user. Required for postgresql and mysql. + user: "" + + # -- Database name. Required for postgresql and mysql. + name: "" + + # -- Kubernetes Secret containing the database password. + # Required for postgresql and mysql. Ignored for sqlite. + # + # Example Secret: + # apiVersion: v1 + # kind: Secret + # metadata: + # name: netbird-db-password + # type: Opaque + # stringData: + # password: "s3cret" + passwordSecret: + # -- Name of the Kubernetes Secret containing the password. + secretName: "" + # -- Key within the Secret that holds the password value. + secretKey: "password" + + # -- SSL mode for PostgreSQL connections (e.g. disable, require, verify-ca, + # verify-full). Ignored for mysql and sqlite. + sslMode: "disable" + # ============================================================================= # Combined NetBird Server (Management + Signal + Relay + STUN) # ============================================================================= @@ -42,45 +92,38 @@ server: # -- Image pull policy. pullPolicy: IfNotPresent - # -- Init container image used to generate config.yaml at pod startup. - # The init container uses GNU envsubst (from the gettext-envsubst Alpine - # package) to substitute ${VAR} placeholders in the config.yaml template - # with secret values read from mounted Kubernetes Secrets. The package is - # installed at runtime via `apk add --no-cache gettext-envsubst` (~40 KB). + # -- Init container image used for config rendering, database readiness + # checks, and database creation. # - # User-supplied values in server.config.* are escaped at chart-render - # time: any "$" in user input is rewritten to "${DOLLAR}" (where DOLLAR - # is exported as a literal "$" by the init script), so envsubst will - # never accidentally interpret user data as a variable reference. + # Initium (https://github.com/KitStream/initium) provides: + # - `render` — envsubst-based config template rendering + # - `wait-for` — TCP/HTTP readiness probes with retry + # - `seed` — declarative database/schema creation # - # Initium (https://github.com/KitStream/initium) is used as the init - # container to render the config template with secret values injected - # from Kubernetes Secrets via the `render` subcommand (envsubst mode). + # The init container runs non-root (UID 65534) with a read-only + # filesystem, no capabilities, and no shell (FROM scratch). initImage: # -- Init container image repository. repository: ghcr.io/kitstream/initium # -- Init container image tag. - tag: "0.1.2" + tag: "1.0.0" - # -- Component-level image pull secrets. Overrides the global imagePullSecrets - # when set. Each entry is a map with a "name" key. + # -- Component-level image pull secrets. imagePullSecrets: [] # --------------------------------------------------------------------------- # Server config.yaml — structured fields # # These fields are rendered into a config.yaml.tpl template stored in a - # ConfigMap. At pod startup, an init container uses GNU envsubst to - # substitute ${VAR} placeholders with secret values from Kubernetes - # Secrets, producing the final config.yaml mounted into the server - # container. User-supplied values below are automatically escaped so - # that any "$" characters in your values are preserved literally. + # ConfigMap. At pod startup, Initium's render subcommand substitutes + # ${VAR} placeholders with secret values from Kubernetes Secrets, + # producing the final config.yaml. User-supplied values below are + # automatically escaped so that any "$" characters are preserved. # # See: https://docs.netbird.io/selfhosted/configuration-files # --------------------------------------------------------------------------- config: # -- The address and port the combined server listens on inside the container. - # TLS is handled by the reverse proxy / ingress controller. listenAddress: ":80" # -- The public-facing URL where peers connect to the server. @@ -107,8 +150,7 @@ server: # in containers) or specify a file path. logFile: "console" - # -- Data directory path where the server stores its database and state - # files. Maps to the persistent volume mount. + # -- Data directory path where the server stores its database and state files. dataDir: "/var/lib/netbird" # -- Authentication / embedded IdP settings. @@ -130,113 +172,41 @@ server: cliRedirectURIs: - "http://localhost:53000/" - # -- Database / store settings. - store: - # -- Database engine. Options: sqlite, postgres, mysql. - # For postgres/mysql, a DSN must be supplied via the secrets reference below. - engine: "sqlite" - # -- Note: store.dsn and store.encryptionKey are sensitive and injected - # at runtime from Kubernetes Secrets — see server.secrets below. - # When using sqlite, dsn is rendered as an empty string. - # --------------------------------------------------------------------------- # Secret references — sensitive config.yaml values # - # Each entry specifies the Kubernetes Secret name and key from which the - # value is read by the init container at pod startup. The Secret must - # exist in the same namespace as the release. - # - # Templating engine: - # The ConfigMap stores a config.yaml.tpl template that uses envsubst - # ${VAR} syntax for secret placeholders. At pod startup the Initium - # init container (https://github.com/KitStream/initium) reads secret - # values from Kubernetes Secrets via secretKeyRef env vars and runs - # its `render` subcommand (envsubst mode) to produce the final - # config.yaml. User-supplied values in server.config.* are escaped - # at chart-render time so that any "$" in user input is preserved - # literally and cannot trigger unintended variable substitution. - # - # External Secret format: - # When you supply your own Secret (secretName is set), the Secret must - # contain the key specified by secretKey. The value must be a - # base64-encoded representation of exactly 32 random bytes (for - # authSecret and storeEncryptionKey). You can generate one with: - # openssl rand -base64 32 - # Example Secret for authSecret + storeEncryptionKey in one Secret: - # apiVersion: v1 - # kind: Secret - # metadata: - # name: my-netbird-secrets - # type: Opaque - # stringData: - # authSecret: "q+3Df8Jk9mR2...base64-of-32-bytes..." - # encryptionKey: "xY7bP0wN4aK1...base64-of-32-bytes..." + # The ConfigMap stores a config.yaml.tpl template that uses envsubst + # ${VAR} syntax for secret placeholders. At pod startup the Initium + # init container reads secret values via secretKeyRef env vars and runs + # its `render` subcommand to produce the final config.yaml. # # Auto-generation: # Set autoGenerate: true (and leave secretName empty) to have this chart - # create a Secret containing a base64-encoded 32-byte random value on - # first install (via Helm's randBytes 32). The generated Secret uses - # the `data:` field (randBytes 32 | b64enc — the outer base64 layer is - # for Kubernetes, the inner base64 string is what the server reads and - # decodes to 32 raw bytes). The Secret carries the annotation - # "helm.sh/resource-policy: keep" so it survives helm uninstall / - # upgrade. Subsequent upgrades reuse the existing value via Helm lookup. - # Supported for: authSecret, storeEncryptionKey. - # NOT supported for: storeDsn (must be supplied externally). + # create a Secret on first install (via Helm's randBytes 32). The Secret + # carries "helm.sh/resource-policy: keep" so it survives uninstall/upgrade. # --------------------------------------------------------------------------- secrets: - # -- Shared authentication secret used internally by the combined server - # for relay credential validation. - # Value: base64-encoded 32 random bytes (generate with: openssl rand -base64 32). + # -- Shared authentication secret for relay credential validation. + # Value: base64-encoded 32 random bytes (openssl rand -base64 32). authSecret: # -- Name of an existing Kubernetes Secret. Leave empty to auto-generate. secretName: "" - # -- Key within the Secret that holds the value. + # -- Key within the Secret. secretKey: "authSecret" - # -- When true and secretName is empty, the chart creates a Secret - # (using data: with randBytes 32 | b64enc) on first install and reuses - # it on upgrades. + # -- Auto-generate on first install. autoGenerate: true - # -- Encryption key for sensitive data at rest (setup keys, API tokens, - # etc. stored in the database). Must be exactly 32 bytes. Must be backed - # up — losing it means losing access to encrypted data. - # Value: base64-encoded 32 random bytes (generate with: openssl rand -base64 32). + # -- Encryption key for sensitive data at rest. Must be exactly 32 bytes. + # Value: base64-encoded 32 random bytes (openssl rand -base64 32). storeEncryptionKey: # -- Name of an existing Kubernetes Secret. Leave empty to auto-generate. secretName: "" - # -- Key within the Secret that holds the value. + # -- Key within the Secret. secretKey: "encryptionKey" - # -- When true and secretName is empty, the chart creates a Secret - # (using data: with randBytes 32 | b64enc) on first install and reuses - # it on upgrades. + # -- Auto-generate on first install. autoGenerate: true - # -- Database connection string (DSN) for postgres or mysql engines. - # Only required when server.config.store.engine is not "sqlite". - # Auto-generation is NOT supported — you must supply an external Secret. - # - # The DSN value format depends on the store engine: - # postgres: "host= user= password= dbname= port=" - # mysql: ":@tcp(:)/" - # - # Example Secret: - # apiVersion: v1 - # kind: Secret - # metadata: - # name: netbird-db - # type: Opaque - # stringData: - # dsn: "host=pg.example.com user=netbird password=s3cret dbname=netbird port=5432" - storeDsn: - # -- Name of the Kubernetes Secret containing the DSN. - secretName: "" - # -- Key within the Secret that holds the DSN value. - secretKey: "dsn" - - - # -- Persistent volume for server data mounted at the configured dataDir. - # Used for SQLite databases, encryption keys, and other runtime state. + # -- Persistent volume for server data. persistentVolume: # -- Whether to create a PersistentVolumeClaim. enabled: true @@ -251,118 +221,56 @@ server: annotations: {} # -- STUN UDP port the server listens on inside the container. - # Exposed via a dedicated LoadBalancer service for NAT traversal. stunPort: 3478 - # -- Primary service exposing the combined HTTP port (API, OAuth2, gRPC, relay, ws-proxy). + # -- Primary service exposing the combined HTTP port. service: - # -- Kubernetes service type. type: ClusterIP - # -- The combined server listens on a single HTTP port for everything - # (management API, OAuth2, gRPC signal, gRPC management, relay, ws-proxy). port: 80 - # -- Dedicated service for the STUN/TURN UDP port required for NAT traversal. + # -- Dedicated service for the STUN/TURN UDP port. stunService: - # -- Kubernetes service type. LoadBalancer is recommended so external - # clients can reach the STUN endpoint. type: LoadBalancer - # -- STUN service port. port: 3478 - # -- Additional annotations (e.g. cloud-provider load-balancer settings). annotations: {} # -- Ingress for HTTP routes (API + OAuth2). - # Routes paths such as /api and /oauth2 to the server's HTTP port. ingress: - # -- Whether to create this Ingress resource. enabled: false - # -- Ingress class name (e.g. "nginx"). className: "nginx" - # -- Additional annotations on the Ingress. annotations: {} - # -- List of host rules. Each entry has a host and a list of paths. hosts: [] - # - host: netbird.example.com - # paths: - # - path: /api - # pathType: ImplementationSpecific - # - path: /oauth2 - # pathType: ImplementationSpecific - # -- TLS configuration. Each entry specifies a secretName and list of hosts. tls: [] # -- Ingress for gRPC routes (signal + management gRPC). - # Requires HTTP/2 (h2c) backend support. Default annotations configure the - # nginx ingress controller for GRPC backend protocol and extended timeouts - # needed by long-lived gRPC streams. ingressGrpc: - # -- Whether to create this Ingress resource. enabled: false - # -- Ingress class name. className: "nginx" - # -- Annotations. Defaults include GRPC backend-protocol, ssl-redirect, - # and 1-hour proxy timeouts for persistent gRPC connections. annotations: nginx.ingress.kubernetes.io/backend-protocol: "GRPC" nginx.ingress.kubernetes.io/ssl-redirect: "true" nginx.ingress.kubernetes.io/proxy-read-timeout: "3600" nginx.ingress.kubernetes.io/proxy-send-timeout: "3600" - # -- List of host rules. hosts: [] - # - host: netbird.example.com - # paths: - # - path: /signalexchange.SignalExchange - # pathType: ImplementationSpecific - # - path: /management.ManagementService - # pathType: ImplementationSpecific - # -- TLS configuration. tls: [] # -- Ingress for relay and WebSocket routes. - # Routes /relay and /ws-proxy traffic which use WebSocket upgrades. ingressRelay: - # -- Whether to create this Ingress resource. enabled: false - # -- Ingress class name. className: "nginx" - # -- Additional annotations on the Ingress. annotations: {} - # -- List of host rules. hosts: [] - # - host: netbird.example.com - # paths: - # - path: /relay - # pathType: ImplementationSpecific - # - path: /ws-proxy - # pathType: ImplementationSpecific - # -- TLS configuration. tls: [] - # -- CPU/memory resource requests and limits for the server container. + # -- CPU/memory resource requests and limits. resources: {} - # limits: - # cpu: 500m - # memory: 512Mi - # requests: - # cpu: 100m - # memory: 128Mi - - # -- Node selector labels for pod scheduling. nodeSelector: {} - # -- Tolerations for pod scheduling. tolerations: [] - # -- Affinity rules for pod scheduling. affinity: {} - # -- Additional annotations added to the server pod. podAnnotations: {} - # -- Additional labels added to the server pod. podLabels: {} - # -- Pod-level security context (e.g. fsGroup, runAsUser). podSecurityContext: {} - # -- Container-level security context (e.g. readOnlyRootFilesystem, capabilities). securityContext: {} - # -- Liveness probe configuration. Checks the HTTP port. livenessProbe: failureThreshold: 3 initialDelaySeconds: 15 @@ -370,7 +278,6 @@ server: timeoutSeconds: 3 tcpSocket: port: http - # -- Readiness probe configuration. Checks the HTTP port. readinessProbe: failureThreshold: 3 initialDelaySeconds: 15 @@ -383,169 +290,61 @@ server: # Dashboard # ============================================================================= dashboard: - # -- Number of dashboard pod replicas. replicaCount: 1 - # -- Container image configuration for the NetBird dashboard. image: - # -- Image repository. repository: netbirdio/dashboard - # -- Image tag. The dashboard follows its own release cycle, independent - # of the server's appVersion. tag: "v2.32.4" - # -- Image pull policy. pullPolicy: IfNotPresent - # -- Component-level image pull secrets. Overrides the global imagePullSecrets - # when set. imagePullSecrets: [] - # --------------------------------------------------------------------------- - # Dashboard configuration - # - # These fields are rendered as environment variables on the dashboard - # container. They configure the dashboard's connection to the management - # API and the OAuth2 / OIDC authentication settings. - # See: https://docs.netbird.io/selfhosted/configuration-files - # --------------------------------------------------------------------------- config: - # -- Endpoint settings - - # -- URL of the NetBird management API. - # Format: "https://netbird.example.com/api" or "https://netbird.example.com". mgmtApiEndpoint: "" - - # -- URL of the NetBird management gRPC endpoint. - # Format: "https://netbird.example.com". mgmtGrpcApiEndpoint: "" - - # -- OAuth2 / OIDC settings - - # -- OAuth2 audience identifier for the dashboard client. authAudience: "netbird-dashboard" - - # -- OAuth2 client ID for the dashboard. authClientId: "netbird-dashboard" - - # -- OAuth2 authority / issuer URL. - # Format: "https://netbird.example.com/oauth2". authAuthority: "" - - # -- Whether Auth0 is used as the identity provider. Set to "true" only - # when using Auth0. useAuth0: "false" - - # -- OAuth2 scopes requested during authentication. authSupportedScopes: "openid profile email groups" - - # -- Path where the dashboard redirects after authentication. authRedirectUri: "/nb-auth" - - # -- Path for silent (iframe-based) token renewal. authSilentRedirectUri: "/nb-silent-auth" - - # -- SSL / TLS settings - - # -- The HTTPS port NGINX listens on inside the container. nginxSslPort: "443" - - # -- Domain for Let's Encrypt certificate provisioning. Set to "none" - # when TLS is terminated externally (e.g. by an ingress controller). letsencryptDomain: "none" - # --------------------------------------------------------------------------- - # Dashboard secrets - # --------------------------------------------------------------------------- secrets: - # -- OAuth2 client secret for the dashboard. When using the embedded - # IdP this is typically empty. When using an external IdP (Auth0, - # Keycloak, etc.) supply the client secret via a Kubernetes Secret. - # - # If secretName is empty, the value of authClientSecret below is used - # directly as a plain-text env var (suitable for empty / non-sensitive - # values). When secretName is set, the value is read from the referenced - # Kubernetes Secret at the given key. - # - # Example external Secret: - # apiVersion: v1 - # kind: Secret - # metadata: - # name: netbird-dashboard-oidc - # type: Opaque - # stringData: - # clientSecret: "my-oidc-client-secret" authClientSecret: - # -- Plain-text value used when secretName is empty. value: "" - # -- Name of an existing Kubernetes Secret. Leave empty to use - # the plain-text value above. secretName: "" - # -- Key within the Secret that holds the client secret. secretKey: "clientSecret" - # -- Additional environment variables to set on the dashboard container. - # Use this for any env vars not covered by the structured config above. - # Each entry is a standard Kubernetes EnvVar object. - # - # Example: - # extraEnv: - # - name: MY_CUSTOM_VAR - # value: "some-value" - # - name: MY_SECRET_VAR - # valueFrom: - # secretKeyRef: - # name: my-secret - # key: my-key extraEnv: [] - # -- Dashboard service configuration. service: - # -- Kubernetes service type. type: ClusterIP - # -- Service port. port: 80 - # -- Dashboard ingress. Serves the UI on the root path (catch-all). ingress: - # -- Whether to create this Ingress resource. enabled: false - # -- Ingress class name. className: "nginx" - # -- Additional annotations on the Ingress. annotations: {} - # -- List of host rules. hosts: [] - # - host: netbird.example.com - # paths: - # - path: / - # pathType: Prefix - # -- TLS configuration. tls: [] - # -- CPU/memory resource requests and limits for the dashboard container. resources: {} - # -- Node selector labels for pod scheduling. nodeSelector: {} - # -- Tolerations for pod scheduling. tolerations: [] - # -- Affinity rules for pod scheduling. affinity: {} - # -- Additional annotations added to the dashboard pod. podAnnotations: {} - # -- Additional labels added to the dashboard pod. podLabels: {} - # -- Pod-level security context. podSecurityContext: {} - # -- Container-level security context. securityContext: {} - # -- Liveness probe configuration. Checks / on the HTTP port. livenessProbe: httpGet: path: / port: http initialDelaySeconds: 10 periodSeconds: 15 - # -- Readiness probe configuration. Checks / on the HTTP port. readinessProbe: httpGet: path: / diff --git a/ci/scripts/e2e.sh b/ci/scripts/e2e.sh index 80ed6b1..11a7423 100755 --- a/ci/scripts/e2e.sh +++ b/ci/scripts/e2e.sh @@ -90,9 +90,9 @@ EOF log "Waiting for PostgreSQL to be ready..." kubectl -n "$NAMESPACE" rollout status deployment/postgres --timeout=120s - # Create the DSN secret for netbird - kubectl -n "$NAMESPACE" create secret generic netbird-db-dsn \ - --from-literal=dsn="host=postgres.${NAMESPACE}.svc.cluster.local user=netbird password=testpassword dbname=netbird port=5432" + # Create the password secret for netbird + kubectl -n "$NAMESPACE" create secret generic netbird-db-password \ + --from-literal=password="testpassword" } deploy_mysql() { @@ -153,9 +153,9 @@ EOF log "Waiting for MySQL to be ready..." kubectl -n "$NAMESPACE" rollout status deployment/mysql --timeout=180s - # Create the DSN secret for netbird - kubectl -n "$NAMESPACE" create secret generic netbird-db-dsn \ - --from-literal=dsn="netbird:testpassword@tcp(mysql.${NAMESPACE}.svc.cluster.local:3306)/netbird" + # Create the password secret for netbird + kubectl -n "$NAMESPACE" create secret generic netbird-db-password \ + --from-literal=password="testpassword" } case "$BACKEND" in diff --git a/examples/seed/README.md b/examples/seed/README.md new file mode 100644 index 0000000..b4b5941 --- /dev/null +++ b/examples/seed/README.md @@ -0,0 +1,64 @@ +# Example Seed Configurations + +This directory contains example `values.yaml` files demonstrating the structured +database configuration introduced in the NetBird Helm chart. + +## Files + +| File | Description | +|------|-------------| +| [`postgresql-values.yaml`](postgresql-values.yaml) | Full example with PostgreSQL backend | +| [`mysql-values.yaml`](mysql-values.yaml) | Full example with MySQL backend | + +## How It Works + +When you set `database.type` to `postgresql` or `mysql`, the chart automatically +adds two Initium init containers to the server pod: + +1. **`db-wait`** — Waits for the database to be reachable via TCP with + exponential backoff (120s timeout). Uses Initium's + [`wait-for`](https://github.com/KitStream/initium#wait-for) subcommand. + +2. **`db-seed`** — Creates the target database if it doesn't exist, using a + declarative seed spec with `create_if_missing: true`. Uses Initium's + [`seed`](https://github.com/KitStream/initium#seed) subcommand. + +3. **`config-init`** — Renders the config.yaml template, substituting `${VAR}` + placeholders with actual secret values from Kubernetes Secrets. Uses Initium's + [`render`](https://github.com/KitStream/initium#render) subcommand. + +The DSN is constructed internally by the chart from the structured `database.*` +fields — you never need to build a connection string. + +## Providing the Password Secret + +Both examples expect a Kubernetes Secret with the database password: + +```bash +kubectl create secret generic netbird-db-password \ + --from-literal=password='your-db-password' \ + -n netbird +``` + +The chart references this secret via `database.passwordSecret.secretName` and +`database.passwordSecret.secretKey`. + +## Generated Seed Spec + +For reference, here is the seed spec the chart generates for PostgreSQL +(rendered into the `seed.yaml` key of the server ConfigMap): + +```yaml +database: + driver: postgres + url: "postgres://netbird:{{ env.DB_PASSWORD }}@postgres.database.svc.cluster.local:5432/?sslmode=disable" +phases: + - name: create-database + database: netbird + create_if_missing: true +``` + +The `{{ env.DB_PASSWORD }}` is a MiniJinja template variable that Initium +resolves at runtime from the `DB_PASSWORD` environment variable (injected via +`secretKeyRef` from your password Secret). + diff --git a/examples/seed/mysql-values.yaml b/examples/seed/mysql-values.yaml new file mode 100644 index 0000000..57082d1 --- /dev/null +++ b/examples/seed/mysql-values.yaml @@ -0,0 +1,64 @@ +# Example: NetBird with MySQL +# +# The chart automatically: +# 1. Waits for the database to be reachable (Initium wait-for) +# 2. Creates the "netbird" database if it doesn't exist (Initium seed) +# 3. Constructs the DSN internally — you never build a connection string +# +# Prerequisites: +# 1. A MySQL server reachable at the given host/port +# 2. A Kubernetes Secret with the database password: +# +# kubectl create secret generic netbird-db-password \ +# --from-literal=password='s3cret' \ +# -n netbird +# +# Install: +# helm install netbird ./charts/netbird \ +# -n netbird --create-namespace \ +# -f examples/seed/mysql-values.yaml + +database: + type: mysql + host: mysql.database.svc.cluster.local + port: 3306 + user: netbird + name: netbird + passwordSecret: + secretName: netbird-db-password + secretKey: password + +server: + config: + exposedAddress: "https://netbird.example.com" + auth: + issuer: "https://auth.example.com" + dashboardRedirectURIs: + - "https://netbird.example.com/nb-auth" + - "https://netbird.example.com/nb-silent-auth" + + ingress: + enabled: true + className: nginx + annotations: + cert-manager.io/cluster-issuer: letsencrypt-prod + hosts: + - host: netbird.example.com + paths: + - path: /api + pathType: ImplementationSpecific + - path: /oauth2 + pathType: ImplementationSpecific + tls: + - secretName: netbird-tls + hosts: + - netbird.example.com + +dashboard: + config: + mgmtApiEndpoint: "https://netbird.example.com" + mgmtGrpcApiEndpoint: "https://netbird.example.com" + authAuthority: "https://auth.example.com" + authClientId: "netbird-dashboard" + authAudience: "netbird-dashboard" + diff --git a/examples/seed/postgresql-values.yaml b/examples/seed/postgresql-values.yaml new file mode 100644 index 0000000..14b1e25 --- /dev/null +++ b/examples/seed/postgresql-values.yaml @@ -0,0 +1,65 @@ +# Example: NetBird with PostgreSQL +# +# The chart automatically: +# 1. Waits for the database to be reachable (Initium wait-for) +# 2. Creates the "netbird" database if it doesn't exist (Initium seed) +# 3. Constructs the DSN internally — you never build a connection string +# +# Prerequisites: +# 1. A PostgreSQL server reachable at the given host/port +# 2. A Kubernetes Secret with the database password: +# +# kubectl create secret generic netbird-db-password \ +# --from-literal=password='s3cret' \ +# -n netbird +# +# Install: +# helm install netbird ./charts/netbird \ +# -n netbird --create-namespace \ +# -f examples/seed/postgresql-values.yaml + +database: + type: postgresql + host: postgres.database.svc.cluster.local + port: 5432 + user: netbird + name: netbird + sslMode: disable # Options: disable, require, verify-ca, verify-full + passwordSecret: + secretName: netbird-db-password + secretKey: password + +server: + config: + exposedAddress: "https://netbird.example.com" + auth: + issuer: "https://auth.example.com" + dashboardRedirectURIs: + - "https://netbird.example.com/nb-auth" + - "https://netbird.example.com/nb-silent-auth" + + ingress: + enabled: true + className: nginx + annotations: + cert-manager.io/cluster-issuer: letsencrypt-prod + hosts: + - host: netbird.example.com + paths: + - path: /api + pathType: ImplementationSpecific + - path: /oauth2 + pathType: ImplementationSpecific + tls: + - secretName: netbird-tls + hosts: + - netbird.example.com + +dashboard: + config: + mgmtApiEndpoint: "https://netbird.example.com" + mgmtGrpcApiEndpoint: "https://netbird.example.com" + authAuthority: "https://auth.example.com" + authClientId: "netbird-dashboard" + authAudience: "netbird-dashboard" +