From 7acabccb2c0e5b60293c23ee637be7e6d9f935d6 Mon Sep 17 00:00:00 2001 From: Luis Carbonell Girones <3169189+lucargir@users.noreply.github.com> Date: Sun, 5 Jul 2026 15:01:51 +0200 Subject: [PATCH] feat(helm): Superset 6 structured config schema with deprecation path Restructure the chart configuration onto a first-class, documented values schema and render superset_config.py from it, while keeping existing installs working via a deprecation path (old keys auto-map + a NOTES warning, never a hard fail). Highlights: - New values sections: database.*, cache.*, cluster.*, config.*, featureFlags.* (+ globalPodAnnotations). superset_config.py is rendered by a new superset.config helper; the legacy superset-config define is removed. - Backward compatible: coalescing resolver helpers map legacy supersetNode.connections.* and root serviceAccountName onto the new schema; a deprecation notice is surfaced in NOTES.txt. Nothing fails on deprecated keys. - cache.driver ports the legacy redis_driver escape hatch (REDIS_DRIVER). - serviceAccount.name is now honored (with legacy serviceAccountName fallback). - init script runs superset db upgrade + superset init in addition to admin/examples; init.initscript is deprecated/unused. - Permissive values.schema.json; deployment checksums track the new config sections; bash /dev/tcp init wait (no dockerize); SQLALCHEMY_TRACK_MODIFICATIONS defaults False. - Generated superset_config.py is always valid Python: booleans render as True/False, SSL cert paths are quoted, CELERY_IMPORTS is only emitted when defined, sentinel config is gated on enabled. - Docs: UPGRADING.md; README regenerated. Chart 0.20.0 (appVersion 6.1.0). Validated: helm lint, helm template (no ), kubeconform 1.29 (strict), 69 helm-unittest cases, and ast.parse of the rendered config. --- helm/superset/Chart.yaml | 2 +- helm/superset/README.md | 54 +- helm/superset/UPGRADING.md | 161 +++++ helm/superset/templates/NOTES.txt | 6 + helm/superset/templates/_helpers.tpl | 630 ++++++++++++++++-- helm/superset/templates/deployment-beat.yaml | 10 +- .../superset/templates/deployment-flower.yaml | 5 +- .../superset/templates/deployment-worker.yaml | 10 +- helm/superset/templates/deployment-ws.yaml | 3 + helm/superset/templates/deployment.yaml | 12 +- helm/superset/templates/secret-env.yaml | 36 +- .../templates/secret-superset-config.yaml | 4 +- helm/superset/tests/checksum_test.yaml | 132 ++++ helm/superset/tests/config_test.yaml | 60 ++ helm/superset/tests/deprecation_test.yaml | 28 + helm/superset/tests/initscript_test.yaml | 50 ++ helm/superset/tests/secret_env_test.yaml | 43 ++ helm/superset/tests/serviceaccount_test.yaml | 28 + helm/superset/tests/values_defaults_test.yaml | 30 + helm/superset/values.schema.json | 140 ++++ helm/superset/values.yaml | 136 +++- 21 files changed, 1464 insertions(+), 116 deletions(-) create mode 100644 helm/superset/UPGRADING.md create mode 100644 helm/superset/tests/checksum_test.yaml create mode 100644 helm/superset/tests/config_test.yaml create mode 100644 helm/superset/tests/deprecation_test.yaml create mode 100644 helm/superset/tests/initscript_test.yaml create mode 100644 helm/superset/tests/secret_env_test.yaml create mode 100644 helm/superset/tests/serviceaccount_test.yaml create mode 100644 helm/superset/tests/values_defaults_test.yaml create mode 100644 helm/superset/values.schema.json diff --git a/helm/superset/Chart.yaml b/helm/superset/Chart.yaml index 4e5a823e86d1..cc4ecb5ef978 100644 --- a/helm/superset/Chart.yaml +++ b/helm/superset/Chart.yaml @@ -29,7 +29,7 @@ maintainers: - name: craig-rueda email: craig@craigrueda.com url: https://github.com/craig-rueda -version: 0.19.0 # See [README](https://github.com/apache/superset/blob/master/helm/superset/README.md#versioning) for version details. +version: 0.20.0 # See [README](https://github.com/apache/superset/blob/master/helm/superset/README.md#versioning) for version details. dependencies: - name: postgresql version: 16.7.27 diff --git a/helm/superset/README.md b/helm/superset/README.md index 59835dabc351..1a006e82f912 100644 --- a/helm/superset/README.md +++ b/helm/superset/README.md @@ -23,7 +23,7 @@ NOTE: This file is generated by helm-docs: https://github.com/norwoodj/helm-docs # superset -![Version: 0.19.0](https://img.shields.io/badge/Version-0.19.0-informational?style=flat-square) +![Version: 0.20.0](https://img.shields.io/badge/Version-0.20.0-informational?style=flat-square) Apache Superset is a modern, enterprise-ready business intelligence web application @@ -74,10 +74,42 @@ Alternatively, perform a fresh install. This is a one-time migration; subsequent |-----|------|---------|-------------| | affinity | object | `{}` | | | bootstrapScript | string | see `values.yaml` | Install additional packages and do any other bootstrap configuration in this script For production clusters it's recommended to build own image with this step done in CI | +| cache | object | `{"asyncQueries":{"keyPrefix":"qc-","timeout":86400},"cacheDb":1,"cacheUrl":null,"celeryDb":0,"celeryUrl":null,"defaultTimeout":86400,"driver":"","enabled":true,"host":null,"keyPrefix":"superset_","password":null,"port":6379,"resultsBackendKeyPrefix":"superset_results","sentinel":null,"ssl":{"ca_certs":null,"certfile":null,"enabled":false,"keyfile":null,"ssl_cert_reqs":"required"},"user":""}` | Redis cache configuration for Superset Redis is optional but recommended for caching and Celery. If redis.enabled (chart dependency) is true, defaults point to the chart's Redis instance. | +| cache.asyncQueries | object | `{"keyPrefix":"qc-","timeout":86400}` | Async queries configuration | +| cache.cacheDb | int | `1` | Redis database number for cache | +| cache.cacheUrl | string | `nil` | Full Redis cache URL (overrides host/port/user/pass if set) | +| cache.celeryDb | int | `0` | Redis database number for Celery | +| cache.celeryUrl | string | `nil` | Full Redis Celery URL (overrides host/port/user/pass if set) | +| cache.defaultTimeout | int | `86400` | Default cache timeout in seconds | +| cache.driver | string | `""` | Custom Redis driver (e.g. TLS/managed variants); overrides the redis proto in URLs when set. Ports the legacy supersetNode.connections.redis_driver escape hatch. | +| cache.enabled | bool | `true` | Enable Redis-based features (cache, Celery). Set to false to disable Redis usage entirely. | +| cache.host | string | `nil` | Redis host (default: {{ .Release.Name }}-redis-headless) | +| cache.keyPrefix | string | `"superset_"` | Cache key prefix | +| cache.password | string | `nil` | Redis password | +| cache.port | int | `6379` | Redis port | +| cache.resultsBackendKeyPrefix | string | `"superset_results"` | Results backend key prefix | +| cache.sentinel | string | `nil` | Redis Sentinel configuration (optional) | +| cache.ssl | object | `{"ca_certs":null,"certfile":null,"enabled":false,"keyfile":null,"ssl_cert_reqs":"required"}` | Redis SSL configuration | +| cache.user | string | `""` | Redis user (optional, for Redis ACL) | +| cluster | object | `{"databaseServiceName":null,"domain":".svc.cluster.local","redisServiceName":null,"websocketServiceName":null}` | Kubernetes cluster configuration Used for constructing service URLs between chart components | +| cluster.databaseServiceName | string | `nil` | Database service name (default: {{ .Release.Name }}-postgresql) Override if using a different service name for the database | +| cluster.domain | string | `".svc.cluster.local"` | Kubernetes cluster domain (default: .svc.cluster.local) Override if using a custom cluster domain | +| cluster.redisServiceName | string | `nil` | Redis service name (default: {{ .Release.Name }}-redis-headless) Override if using a different service name for Redis | +| cluster.websocketServiceName | string | `nil` | WebSocket service name (default: {{ .Release.Name }}-ws) Override if using a different service name for the WebSocket service | +| config | object | `{}` | Superset configuration properties Set any configuration property from superset/config.py here See https://github.com/apache/superset/blob/master/superset/config.py for all available options | | configFromSecret | string | `"{{ template \"superset.fullname\" . }}-config"` | The name of the secret which we will use to generate a superset_config.py file Note: this secret must have the key superset_config.py in it and can include other files as well | | configMountPath | string | `"/app/pythonpath"` | | | configOverrides | object | `{}` | A dictionary of overrides to append at the end of superset_config.py - the name does not matter WARNING: the order is not guaranteed Files can be passed as helm --set-file configOverrides.my-override=my-file.py | | configOverridesFiles | object | `{}` | Same as above but the values are files | +| database | object | `{"driver":"postgresql+psycopg2","host":null,"name":null,"password":null,"port":5432,"ssl":{"enabled":false,"mode":"require"},"uri":null,"user":null}` | Database connection configuration for the Superset metadata database | +| database.driver | string | `"postgresql+psycopg2"` | Database driver (used when uri is not set) | +| database.host | string | `nil` | Database host (default: {{ .Release.Name }}-postgresql) | +| database.name | string | `nil` | Database name (default: superset, resolved via superset.db.name) | +| database.password | string | `nil` | Database password (default: superset, resolved via superset.db.password) ⚠️ CHANGE THIS for production | +| database.port | int | `5432` | Database port | +| database.ssl | object | `{"enabled":false,"mode":"require"}` | Database SSL configuration | +| database.uri | string | `nil` | Full database URI (overrides host/port/user/pass/name if set) Example: "postgresql+psycopg2://user:pass@host:5432/dbname" | +| database.user | string | `nil` | Database user (default: superset, resolved via superset.db.user) | | envFromSecret | string | `"{{ template \"superset.fullname\" . }}-env"` | The name of the secret which we will use to populate env vars in deployed pods This can be useful for secret keys, etc. | | envFromSecrets | list | `[]` | This can be a list of templated strings | | extraConfigMountPath | string | `"/app/configs"` | | @@ -89,7 +121,9 @@ Alternatively, perform a fresh install. This is a one-time migration; subsequent | extraSecrets | object | `{}` | Extra files to be mounted as Secrets on the path specified in `configMountPath` | | extraVolumeMounts | list | `[]` | | | extraVolumes | list | `[]` | | +| featureFlags | object | `{}` | Feature flags configuration See https://github.com/apache/superset/blob/master/RESOURCES/FEATURE_FLAGS.md | | fullnameOverride | string | `nil` | Provide a name to override the full names of resources | +| globalPodAnnotations | object | `{}` | Global pod annotations to be added to all pods Use this to set annotations that apply to all Superset components Component-specific podAnnotations will be merged with these global annotations | | hostAliases | list | `[]` | Custom hostAliases for all superset pods # https://kubernetes.io/docs/tasks/network/customize-hosts-file-for-pods/ | | image.pullPolicy | string | `"IfNotPresent"` | | | image.repository | string | `"apachesuperset.docker.scarf.sh/apache/superset"` | | @@ -116,7 +150,7 @@ Alternatively, perform a fresh install. This is a one-time migration; subsequent | init.enabled | bool | `true` | | | init.extraContainers | list | `[]` | Launch additional containers into init job pod | | init.initContainers | list | a container waiting for postgres | List of initContainers | -| init.initscript | string | a script to create admin user and initialize roles | A Superset init script | +| init.initscript | string | unused; kept for backwards-compatibility only | DEPRECATED: this field is no longer used by the chart. The init script is rendered entirely from the internal `superset.initScript` template (which runs `superset db upgrade`, `superset init`, admin creation, and examples). Any customization placed here is silently ignored. See UPGRADING.md. | | init.jobAnnotations."helm.sh/hook" | string | `"post-install,post-upgrade"` | | | init.jobAnnotations."helm.sh/hook-delete-policy" | string | `"before-hook-creation"` | | | init.loadExamples | bool | `false` | | @@ -143,6 +177,7 @@ Alternatively, perform a fresh install. This is a one-time migration; subsequent | service.type | string | `"ClusterIP"` | | | serviceAccount.annotations | object | `{}` | | | serviceAccount.create | bool | `false` | Create custom service account for Superset. If create: true and serviceAccountName is not provided, `superset.fullname` will be used. | +| serviceAccount.name | string | `""` | Service account name to use (if not specified, defaults to release name + chart name) | | serviceAccountName | string | `nil` | Specify service account name to be used | | supersetCeleryBeat.affinity | object | `{}` | Affinity to be added to supersetCeleryBeat deployment | | supersetCeleryBeat.command | list | a `celery beat` command | Command | @@ -214,20 +249,7 @@ Alternatively, perform a fresh install. This is a one-time migration; subsequent | supersetNode.autoscaling.minReplicas | int | `1` | | | supersetNode.autoscaling.targetCPUUtilizationPercentage | int | `80` | | | supersetNode.command | list | See `values.yaml` | Startup command | -| supersetNode.connections.db_host | string | `"{{ .Release.Name }}-postgresql"` | | -| supersetNode.connections.db_name | string | `"superset"` | | -| supersetNode.connections.db_pass | string | `"superset"` | | -| supersetNode.connections.db_port | string | `"5432"` | | -| supersetNode.connections.db_type | string | `"postgresql"` | Database type for Superset metadata (Supported types: "postgresql", "mysql") | -| supersetNode.connections.db_user | string | `"superset"` | | -| supersetNode.connections.redis_cache_db | string | `"1"` | | -| supersetNode.connections.redis_celery_db | string | `"0"` | | -| supersetNode.connections.redis_driver | string | `""` | | -| supersetNode.connections.redis_host | string | `"{{ .Release.Name }}-redis-headless"` | Change in case of bringing your own redis and then also set redis.enabled:false | -| supersetNode.connections.redis_port | string | `"6379"` | | -| supersetNode.connections.redis_ssl.enabled | bool | `false` | | -| supersetNode.connections.redis_ssl.ssl_cert_reqs | string | `"CERT_NONE"` | | -| supersetNode.connections.redis_user | string | `""` | | +| supersetNode.connections | object | `{}` | | | supersetNode.containerSecurityContext | object | `{}` | | | supersetNode.deploymentAdditionalPodSpec | object | `{}` | Custom pod spec to be added to supersetNode deployment | | supersetNode.deploymentAnnotations | object | `{}` | Annotations to be added to supersetNode deployment | diff --git a/helm/superset/UPGRADING.md b/helm/superset/UPGRADING.md new file mode 100644 index 000000000000..eaf0b2ff1695 --- /dev/null +++ b/helm/superset/UPGRADING.md @@ -0,0 +1,161 @@ + + +# Upgrading the Superset Helm Chart + +## Upgrading to chart 0.20.0 + +Chart 0.20.0 introduces a structured connection schema. The old keys listed below are **DEPRECATED** — they still work (auto-mapped to the new keys at render time) and a deprecation warning is printed in `helm install`/`helm upgrade` NOTES, but they will be removed in a future release. Migrate as soon as possible. + +### 1. Connection schema: `supersetNode.connections.*` → `database.*` / `cache.*` + +**Before (deprecated — still honored):** + +```yaml +supersetNode: + connections: + db_host: "pg-host" + db_port: "5432" + db_user: "superset" + db_pass: "superset" + db_name: "superset" + redis_host: "redis-host" + redis_port: "6379" + redis_cache_db: "1" + redis_celery_db: "0" + redis_driver: "rediss" +``` + +**After (new structured keys — recommended):** + +```yaml +database: + host: "pg-host" + port: 5432 + user: "superset" + password: "superset" + name: "superset" + +cache: + host: "redis-host" + port: 6379 + cacheDb: 1 + celeryDb: 0 + driver: "rediss" # optional: custom Redis driver / TLS variant +``` + +Key-by-key mapping summary: + +| Old key | New key | +|---|---| +| `supersetNode.connections.db_host` | `database.host` | +| `supersetNode.connections.db_port` | `database.port` | +| `supersetNode.connections.db_user` | `database.user` | +| `supersetNode.connections.db_pass` | `database.password` | +| `supersetNode.connections.db_name` | `database.name` | +| `supersetNode.connections.redis_host` | `cache.host` | +| `supersetNode.connections.redis_port` | `cache.port` | +| `supersetNode.connections.redis_cache_db` | `cache.cacheDb` | +| `supersetNode.connections.redis_celery_db` | `cache.celeryDb` | +| `supersetNode.connections.redis_driver` | `cache.driver` | + +### 2. Service account: root `serviceAccountName` → `serviceAccount.name` + +**Before (deprecated — still honored):** + +```yaml +serviceAccountName: my-sa +``` + +**After (new key — recommended):** + +```yaml +serviceAccount: + name: my-sa +``` + +### 3. Init script: `init.initscript` is deprecated and replaced by the built-in template + +> **This is a behavior change.** The chart no longer uses `init.initscript`. The init script is rendered +> entirely from an internal chart template (`superset.initScript`), which runs the full initialization +> sequence: +> +> 1. `superset db upgrade` — applies all pending database schema migrations +> 2. `superset init` — initializes roles and permissions +> 3. Admin user creation (when `init.createAdmin: true`) +> 4. Example data loading (when `init.loadExamples: true`) +> 5. Datasource import (when `import_datasources.yaml` is present) +> +> A future PR will optionally split the database migration step into a dedicated upgrade Job for +> zero-downtime deployments. Until that PR lands, migrations run as part of the init Job above. + +If you customized `init.initscript` in your `values.yaml`, that customization is silently ignored. +Move any customizations to `config` or `configOverrides`: + +```yaml +# Move custom Python config here: +config: + MY_SETTING: "value" + +configOverrides: + my_custom_override: | + # Python snippet appended to superset_config.py + MY_SETTING = "value" +``` + +If your use case cannot be covered by `config` or `configOverrides`, please open an issue so the maintainers +can evaluate extending the template. + +### 4. New top-level sections: `config.*` and `featureFlags.*` + +Two new sections provide direct Superset configuration passthrough without needing raw `configOverrides`. + +**`config.*` — direct Superset config properties:** + +```yaml +config: + SECRET_KEY: "$(SUPERSET_SECRET_KEY)" + ROW_LIMIT: 50000 + WTF_CSRF_ENABLED: true + SQLALCHEMY_POOL_SIZE: 10 +``` + +Each key is injected verbatim into `superset_config.py`. String values are quoted; non-string values +(integers, booleans) are rendered as-is. + +**`featureFlags.*` — feature flag overrides:** + +```yaml +featureFlags: + ALERT_REPORTS: true + DASHBOARD_RBAC: true + ENABLE_TEMPLATE_PROCESSING: false +``` + +This is equivalent to setting `FEATURE_FLAGS = {...}` in `superset_config.py`, but is more readable and +schema-validated. + +### 5. `values.schema.json` — early validation + +The chart now ships a `values.schema.json` that validates the values you provide. Wrong types (e.g., a string +where an integer is expected) or unknown keys in structured sections will cause `helm install`/`helm upgrade` +to fail immediately with a descriptive error instead of producing a broken deployment. + +If you get a validation error after upgrading, check that your overridden values match the types documented in +`values.yaml` and the schema. diff --git a/helm/superset/templates/NOTES.txt b/helm/superset/templates/NOTES.txt index a029356f853b..e98076241e31 100644 --- a/helm/superset/templates/NOTES.txt +++ b/helm/superset/templates/NOTES.txt @@ -35,3 +35,9 @@ echo "Visit http://127.0.0.1:8088 to use your application" kubectl port-forward service/superset 8088:8088 --namespace {{ .Release.Namespace }} {{- end }} +{{- $warnings := include "superset.deprecationWarnings" . }} +{{- if trim $warnings }} + +⚠️ DEPRECATION WARNINGS +{{ $warnings }} +{{- end }} diff --git a/helm/superset/templates/_helpers.tpl b/helm/superset/templates/_helpers.tpl index 4533798eb7f8..8ac5dc6f4db1 100644 --- a/helm/superset/templates/_helpers.tpl +++ b/helm/superset/templates/_helpers.tpl @@ -47,11 +47,12 @@ If release name contains chart name it will be used as a full name. Create the name of the service account to use */}} {{- define "superset.serviceAccountName" -}} - {{- if .Values.serviceAccount.create -}} - {{- default (include "superset.fullname" .) .Values.serviceAccountName -}} - {{- else -}} - {{- default "default" .Values.serviceAccountName -}} - {{- end -}} +{{- $name := coalesce .Values.serviceAccount.name .Values.serviceAccountName -}} +{{- if .Values.serviceAccount.create -}} +{{- default (include "superset.fullname" .) $name -}} +{{- else -}} +{{- default "default" $name -}} +{{- end -}} {{- end -}} {{/* @@ -105,80 +106,564 @@ app.kubernetes.io/component: {{ .component }} {{- end -}} -{{- define "superset-config" }} +{{/* +Coalescing resolvers for DB and Redis connection parameters. +Each resolver checks (in order): + 1. New top-level database.* / cache.* values + 2. Legacy supersetNode.connections.* keys (deprecation path, using safe index to avoid errors on absent maps) + 3. cluster.* service name overrides + 4. Hard-coded defaults derived from the release name +Call with root context: {{ include "superset.db.host" . }} +*/}} + +{{/* +Helper to safely read .Values.supersetNode.connections. without erroring when maps are absent. +*/}} +{{- define "_superset.legacyConn" -}} +{{- $sn := index .Values "supersetNode" | default dict -}} +{{- $conn := index $sn "connections" | default dict -}} +{{- $conn | toJson -}} +{{- end -}} + +{{- define "superset.db.host" -}} +{{- $conn := include "_superset.legacyConn" . | fromJson -}} +{{- tpl (coalesce .Values.database.host (index $conn "db_host") .Values.cluster.databaseServiceName (printf "%s-postgresql" .Release.Name)) $ -}} +{{- end -}} + +{{- define "superset.db.port" -}} +{{- $conn := include "_superset.legacyConn" . | fromJson -}} +{{- if .Values.database.port -}} +{{- .Values.database.port | toString -}} +{{- else -}} +{{- coalesce (index $conn "db_port") "5432" -}} +{{- end -}} +{{- end -}} + +{{- define "superset.db.user" -}} +{{- $conn := include "_superset.legacyConn" . | fromJson -}} +{{- coalesce .Values.database.user (index $conn "db_user") "superset" -}} +{{- end -}} + +{{- define "superset.db.password" -}} +{{- $conn := include "_superset.legacyConn" . | fromJson -}} +{{- coalesce .Values.database.password (index $conn "db_pass") "superset" -}} +{{- end -}} + +{{- define "superset.db.name" -}} +{{- $conn := include "_superset.legacyConn" . | fromJson -}} +{{- coalesce .Values.database.name (index $conn "db_name") "superset" -}} +{{- end -}} + +{{- define "superset.db.driver" -}} +{{- $conn := include "_superset.legacyConn" . | fromJson -}} +{{- coalesce .Values.database.driver (index $conn "db_type") "postgresql+psycopg2" -}} +{{- end -}} + +{{- define "superset.redis.host" -}} +{{- $conn := include "_superset.legacyConn" . | fromJson -}} +{{- tpl (coalesce .Values.cache.host (index $conn "redis_host") .Values.cluster.redisServiceName (printf "%s-redis-headless" .Release.Name)) $ -}} +{{- end -}} + +{{- define "superset.redis.port" -}} +{{- $conn := include "_superset.legacyConn" . | fromJson -}} +{{- if .Values.cache.port -}} +{{- .Values.cache.port | toString -}} +{{- else -}} +{{- coalesce (index $conn "redis_port") "6379" -}} +{{- end -}} +{{- end -}} + +{{- define "superset.redis.user" -}} +{{- $conn := include "_superset.legacyConn" . | fromJson -}} +{{- coalesce .Values.cache.user (index $conn "redis_user") "" -}} +{{- end -}} + +{{- define "superset.redis.password" -}} +{{- $conn := include "_superset.legacyConn" . | fromJson -}} +{{- coalesce .Values.cache.password (index $conn "redis_password") "" -}} +{{- end -}} + +{{- define "superset.redis.cacheDb" -}} +{{- $conn := include "_superset.legacyConn" . | fromJson -}} +{{- coalesce (.Values.cache.cacheDb | toString) (index $conn "redis_cache_db") "1" -}} +{{- end -}} + +{{- define "superset.redis.celeryDb" -}} +{{- $conn := include "_superset.legacyConn" . | fromJson -}} +{{- coalesce (.Values.cache.celeryDb | toString) (index $conn "redis_celery_db") "0" -}} +{{- end -}} + +{{- define "superset.redis.proto" -}} +{{- if .Values.cache.driver }}{{ .Values.cache.driver }}{{- else if .Values.cache.ssl.enabled }}rediss{{- else }}redis{{- end -}} +{{- end -}} + +{{- define "superset.config" }} +{{- /* SECURITY: Validate admin password is set if admin creation is enabled */}} +{{- if and .Values.init.createAdmin (or (not .Values.init.adminUser.password) (eq .Values.init.adminUser.password "")) }} +{{- fail "SECURITY ERROR: init.createAdmin is true but init.adminUser.password is empty. You must set a secure password using --set init.adminUser.password='your-password' or via external secret." }} +{{- end }} + import os +{{- if or .Values.config.cacheConfig .Values.config.dataCacheConfig .Values.config.resultsBackend .Values.config.celeryConfig .Values.cache.enabled }} from flask_caching.backends.rediscache import RedisCache +{{- end }} def env(key, default=None): return os.getenv(key, default) -# Redis Base URL -{{- if .Values.supersetNode.connections.redis_password }} -REDIS_BASE_URL=f"{env('REDIS_DRIVER') or env('REDIS_PROTO')}://{env('REDIS_USER', '')}:{env('REDIS_PASSWORD')}@{env('REDIS_HOST')}:{env('REDIS_PORT')}" +{{- /* Database Configuration - Superset always requires a database */}} +{{- if .Values.database.uri }} +SQLALCHEMY_DATABASE_URI = {{ .Values.database.uri | quote }} +{{- else }} +{{- $driver := include "superset.db.driver" . }} +{{- $sslParams := "" }} +{{- if and (hasKey .Values.database "ssl") .Values.database.ssl.enabled }} +{{- $sslMode := .Values.database.ssl.mode | default "require" }} +{{- $sslParams = printf "?sslmode=%s" $sslMode }} +{{- end }} +SQLALCHEMY_DATABASE_URI = f"{{ $driver }}://{{ include "superset.db.user" . }}:{{ include "superset.db.password" . }}@{{ include "superset.db.host" . }}:{{ include "superset.db.port" . }}/{{ include "superset.db.name" . }}{{ $sslParams }}" +{{- end }} +{{- if hasKey .Values.config "SQLALCHEMY_TRACK_MODIFICATIONS" }} +SQLALCHEMY_TRACK_MODIFICATIONS = {{ .Values.config.SQLALCHEMY_TRACK_MODIFICATIONS | toString | title }} {{- else }} -REDIS_BASE_URL=f"{env('REDIS_DRIVER') or env('REDIS_PROTO')}://{env('REDIS_HOST')}:{env('REDIS_PORT')}" +SQLALCHEMY_TRACK_MODIFICATIONS = False {{- end }} -# Redis URL Params -{{- if .Values.supersetNode.connections.redis_ssl.enabled }} -REDIS_URL_PARAMS = f"?ssl_cert_reqs={env('REDIS_SSL_CERT_REQS')}" +{{- /* Redis Configuration - only if Redis cache is configured */}} +{{- if .Values.cache.enabled }} +{{- if .Values.cache.cacheUrl }} +CACHE_REDIS_URL = {{ .Values.cache.cacheUrl | quote }} +{{- else }} +{{- $redisHost := include "superset.redis.host" . }} +{{- $redisUser := include "superset.redis.user" . }} +{{- $redisPort := include "superset.redis.port" . }} +{{- $redisPassword := include "superset.redis.password" . }} +{{- $useSSL := and (hasKey .Values.cache "ssl") .Values.cache.ssl.enabled }} +{{- if $redisPassword }} +{{- if $redisUser }} +REDIS_BASE_URL = f"{{ include "superset.redis.proto" . }}://{{ $redisUser }}:{{ $redisPassword }}@{{ $redisHost }}:{{ $redisPort }}" +{{- else }} +REDIS_BASE_URL = f"{{ include "superset.redis.proto" . }}://:{{ $redisPassword }}@{{ $redisHost }}:{{ $redisPort }}" +{{- end }} +{{- else }} +REDIS_BASE_URL = f"{{ include "superset.redis.proto" . }}://{{ $redisHost }}:{{ $redisPort }}" +{{- end }} +{{- if $useSSL }} +{{- $sslCertReqs := .Values.cache.ssl.ssl_cert_reqs | default "required" }} +REDIS_URL_PARAMS = f"?ssl_cert_reqs={{ $sslCertReqs }}" {{- else }} REDIS_URL_PARAMS = "" -{{- end}} - -# Build Redis URLs -CACHE_REDIS_URL = f"{REDIS_BASE_URL}/{env('REDIS_DB', 1)}{REDIS_URL_PARAMS}" -CELERY_REDIS_URL = f"{REDIS_BASE_URL}/{env('REDIS_CELERY_DB', 0)}{REDIS_URL_PARAMS}" +{{- end }} +{{- $cacheDb := include "superset.redis.cacheDb" . }} +CACHE_REDIS_URL = f"{REDIS_BASE_URL}/{{ $cacheDb }}{REDIS_URL_PARAMS}" +{{- end }} +{{- if .Values.cache.celeryUrl }} +CELERY_REDIS_URL = {{ .Values.cache.celeryUrl | quote }} +{{- else if not .Values.cache.cacheUrl }} +{{- $celeryDb := include "superset.redis.celeryDb" . }} +CELERY_REDIS_URL = f"{REDIS_BASE_URL}/{{ $celeryDb }}{REDIS_URL_PARAMS}" +{{- else }} +{{- if or .Values.config.celeryConfig (not .Values.cache.enabled) }} +{{- /* Custom celeryConfig provided or cache disabled - OK */}} +{{- else }} +{{- fail "CONFIGURATION ERROR: cache.cacheUrl is set but cache.celeryUrl is not set. When using cacheUrl, you must also set celeryUrl for Celery to work. Alternatively, set config.celeryConfig to provide a custom Celery configuration." }} +{{- end }} +{{- end }} +{{- end }} -MAPBOX_API_KEY = env('MAPBOX_API_KEY', '') +{{- /* Cache Configuration */}} +{{- if .Values.config.cacheConfig }} +CACHE_CONFIG = {{ .Values.config.cacheConfig | toJson | indent 2 }} +{{- else if .Values.cache.enabled }} CACHE_CONFIG = { - 'CACHE_TYPE': 'RedisCache', - 'CACHE_DEFAULT_TIMEOUT': 300, - 'CACHE_KEY_PREFIX': 'superset_', - 'CACHE_REDIS_URL': CACHE_REDIS_URL, + 'CACHE_TYPE': 'RedisCache', + 'CACHE_DEFAULT_TIMEOUT': {{ .Values.cache.defaultTimeout | default (.Values.config.cacheDefaultTimeout | default 86400) | int }}, + 'CACHE_KEY_PREFIX': {{ .Values.cache.keyPrefix | default "superset_" | quote }}, + 'CACHE_REDIS_URL': CACHE_REDIS_URL, } +{{- end }} + +{{- if .Values.config.dataCacheConfig }} +DATA_CACHE_CONFIG = {{ .Values.config.dataCacheConfig | toJson | indent 2 }} +{{- else if .Values.config.cacheConfig }} DATA_CACHE_CONFIG = CACHE_CONFIG +{{- else if .Values.cache.enabled }} +DATA_CACHE_CONFIG = CACHE_CONFIG +{{- end }} +{{- /* SQLLAB_ASYNC_TIME_LIMIT_SEC - Required for async_queries module import (default: 6 hours) */}} +{{- if .Values.config.SQLLAB_ASYNC_TIME_LIMIT_SEC }} +SQLLAB_ASYNC_TIME_LIMIT_SEC = {{ .Values.config.SQLLAB_ASYNC_TIME_LIMIT_SEC | int }} +{{- else }} +from datetime import timedelta +SQLLAB_ASYNC_TIME_LIMIT_SEC = int(timedelta(hours=6).total_seconds()) +{{- end }} -if os.getenv("SQLALCHEMY_DATABASE_URI"): - SQLALCHEMY_DATABASE_URI = os.getenv("SQLALCHEMY_DATABASE_URI") -else: - {{- if eq .Values.supersetNode.connections.db_type "postgresql" }} - SQLALCHEMY_DATABASE_URI = f"postgresql+psycopg2://{os.getenv('DB_USER')}:{os.getenv('DB_PASS')}@{os.getenv('DB_HOST')}:{os.getenv('DB_PORT')}/{os.getenv('DB_NAME')}" - {{- else if eq .Values.supersetNode.connections.db_type "mysql" }} - SQLALCHEMY_DATABASE_URI = f"mysql+mysqldb://{os.getenv('DB_USER')}:{os.getenv('DB_PASS')}@{os.getenv('DB_HOST')}:{os.getenv('DB_PORT')}/{os.getenv('DB_NAME')}" - {{- else }} - {{ fail (printf "Unsupported database type: %s. Please use 'postgresql' or 'mysql'." .Values.supersetNode.connections.db_type) }} - {{- end }} +{{- /* Celery Configuration */}} +{{- if .Values.config.celeryConfig }} +{{- if kindIs "string" .Values.config.celeryConfig }} +{{ .Values.config.celeryConfig }} +{{- else }} +class CeleryConfig: +{{- range $key, $value := .Values.config.celeryConfig }} + {{ $key }} = {{ $value | toJson }} +{{- end }} + +{{- if hasKey .Values.config.celeryConfig "imports" }} +CELERY_IMPORTS = CeleryConfig.imports +{{- end }} +CELERY_CONFIG = CeleryConfig +{{- end }} +{{- else if .Values.cache.enabled }} +from celery.schedules import crontab +from datetime import timedelta class CeleryConfig: - imports = ("superset.sql_lab", ) - broker_url = CELERY_REDIS_URL - result_backend = CELERY_REDIS_URL + imports = ( + "superset.sql_lab", + "superset.tasks.scheduler", + "superset.tasks.thumbnails", + "superset.tasks.cache", + ) + broker_connection_retry_on_startup = True + worker_prefetch_multiplier = 10 + task_acks_late = True + broker_url = CELERY_REDIS_URL + result_backend = CELERY_REDIS_URL + task_annotations = { + "sql_lab.get_sql_results": { + "rate_limit": "100/s", + }, + } + beat_schedule = { + "reports.scheduler": { + "task": "reports.scheduler", + "schedule": crontab(minute="*", hour="*"), + "options": {"expires": int(timedelta(weeks=1).total_seconds())}, + }, + "reports.prune_log": { + "task": "reports.prune_log", + "schedule": crontab(minute=0, hour=0), + }, + } +CELERY_IMPORTS = CeleryConfig.imports CELERY_CONFIG = CeleryConfig +{{- end }} + +{{- /* Celery Worker Health Check - File-based health probes for Kubernetes */}} +{{- if and .Values.supersetWorker.healthCheck .Values.supersetWorker.healthCheck.enabled }} +# Celery Worker Health Check Configuration +import threading +from celery import bootsteps +from celery.signals import worker_ready, worker_shutdown, worker_init + +_readiness_file = {{ .Values.supersetWorker.healthCheck.readinessFile | default "/tmp/celery_worker_ready" | quote }} +_liveness_file = {{ .Values.supersetWorker.healthCheck.livenessFile | default "/tmp/celery_worker_alive" | quote }} +_heartbeat_interval = {{ .Values.supersetWorker.healthCheck.livenessHeartbeatInterval | default 10 | int }} +_liveness_thread = None +_liveness_stop_event = None + +@worker_ready.connect +def create_ready_file(sender, **kwargs): + try: + open(_readiness_file, 'w').close() + except Exception as e: + print(f"Warning: Could not create readiness file: {e}") + +@worker_shutdown.connect +def remove_ready_file(sender, **kwargs): + global _liveness_thread, _liveness_stop_event + if _liveness_stop_event: + _liveness_stop_event.set() + if _liveness_thread: + _liveness_thread.join(timeout=5) + try: + if os.path.exists(_readiness_file): + os.remove(_readiness_file) + if os.path.exists(_liveness_file): + os.remove(_liveness_file) + except Exception as e: + print(f"Warning: Could not remove health check files: {e}") + +@worker_init.connect +def start_liveness_heartbeat(sender, **kwargs): + global _liveness_thread, _liveness_stop_event + _liveness_stop_event = threading.Event() + + def update_liveness(): + while not _liveness_stop_event.is_set(): + try: + with open(_liveness_file, 'w') as f: + f.write(str(os.getpid())) + except Exception as e: + print(f"Warning: Could not update liveness file: {e}") + _liveness_stop_event.wait(_heartbeat_interval) + + _liveness_thread = threading.Thread(target=update_liveness, daemon=True) + _liveness_thread.start() +{{- else }} +CELERY_WORKER_HEALTH_CHECK_ENABLED = False +{{- end }} + +{{- /* Results Backend */}} +{{- $redisHostForBackend := include "superset.redis.host" . }} +{{- $redisPortForBackend := include "superset.redis.port" . }} +{{- $redisPasswordForBackend := include "superset.redis.password" . }} +{{- if .Values.config.resultsBackend }} +{{- if kindIs "string" .Values.config.resultsBackend }} +RESULTS_BACKEND = {{ .Values.config.resultsBackend }} +{{- else }} +RESULTS_BACKEND = RedisCache( + host={{ $redisHostForBackend | quote }}, + {{- if $redisPasswordForBackend }} + password={{ $redisPasswordForBackend | quote }}, + {{- end }} + port={{ $redisPortForBackend | int }}, + key_prefix={{ .Values.cache.resultsBackendKeyPrefix | default "superset_results" | quote }}, + {{- if and (hasKey .Values.cache "ssl") .Values.cache.ssl.enabled }} + ssl=True, + ssl_cert_reqs={{ .Values.cache.ssl.ssl_cert_reqs | default "required" | quote }}, + {{- end }} +) +{{- end }} +{{- else if .Values.cache.enabled }} RESULTS_BACKEND = RedisCache( - host=env('REDIS_HOST'), - {{- if .Values.supersetNode.connections.redis_password }} - password=env('REDIS_PASSWORD'), - {{- end }} - port=env('REDIS_PORT'), - key_prefix='superset_results', - {{- if .Values.supersetNode.connections.redis_ssl.enabled }} - ssl=True, - ssl_cert_reqs=env('REDIS_SSL_CERT_REQS'), - {{- end }} + host={{ $redisHostForBackend | quote }}, + {{- if $redisPasswordForBackend }} + password={{ $redisPasswordForBackend | quote }}, + {{- end }} + port={{ $redisPortForBackend | int }}, + key_prefix={{ .Values.cache.resultsBackendKeyPrefix | default "superset_results" | quote }}, + {{- if and (hasKey .Values.cache "ssl") .Values.cache.ssl.enabled }} + ssl=True, + ssl_cert_reqs={{ .Values.cache.ssl.ssl_cert_reqs | default "required" | quote }}, + {{- end }} ) +{{- end }} + +{{- /* Global Async Queries Cache Backend */}} +{{- $redisUserForGaq := include "superset.redis.user" . }} +{{- if .Values.config.GLOBAL_ASYNC_QUERIES_CACHE_BACKEND }} +GLOBAL_ASYNC_QUERIES_CACHE_BACKEND = {{ .Values.config.GLOBAL_ASYNC_QUERIES_CACHE_BACKEND | toJson | indent 2 }} +{{- else if .Values.cache.enabled }} +GLOBAL_ASYNC_QUERIES_CACHE_BACKEND = { + "CACHE_TYPE": "RedisCache", + "CACHE_REDIS_HOST": {{ $redisHostForBackend | quote }}, + "CACHE_REDIS_PORT": {{ $redisPortForBackend | int }}, + {{- if $redisUserForGaq }} + "CACHE_REDIS_USER": {{ $redisUserForGaq | quote }}, + {{- end }} + {{- if $redisPasswordForBackend }} + "CACHE_REDIS_PASSWORD": {{ $redisPasswordForBackend | quote }}, + {{- else }} + "CACHE_REDIS_PASSWORD": "", + {{- end }} + "CACHE_REDIS_DB": {{ .Values.cache.asyncQueries.db | default .Values.cache.cacheDb | default 0 | int }}, + "CACHE_KEY_PREFIX": {{ .Values.cache.asyncQueries.keyPrefix | default "qc-" | quote }}, + "CACHE_DEFAULT_TIMEOUT": {{ .Values.cache.asyncQueries.timeout | default 86400 | int }}, + {{- if and .Values.cache.sentinel .Values.cache.sentinel.enabled }} + {{- if .Values.cache.sentinel.sentinels }} + "CACHE_REDIS_SENTINELS": {{ .Values.cache.sentinel.sentinels | toJson }}, + {{- else }} + {{- fail "CONFIGURATION ERROR: cache.sentinel.enabled is true but cache.sentinel.sentinels is not set. You must provide Sentinel host(s) in cache.sentinel.sentinels (e.g., [['sentinel-host', 26379]])." }} + {{- end }} + "CACHE_REDIS_SENTINEL_MASTER": {{ .Values.cache.sentinel.master | default "mymaster" | quote }}, + {{- if .Values.cache.sentinel.password }} + "CACHE_REDIS_SENTINEL_PASSWORD": {{ .Values.cache.sentinel.password | quote }}, + {{- else }} + "CACHE_REDIS_SENTINEL_PASSWORD": None, + {{- end }} + {{- end }} + {{- if and (hasKey .Values.cache "ssl") .Values.cache.ssl.enabled }} + "CACHE_REDIS_SSL": True, + "CACHE_REDIS_SSL_CERTFILE": {{ if .Values.cache.ssl.certfile }}{{ .Values.cache.ssl.certfile | quote }}{{ else }}None{{ end }}, + "CACHE_REDIS_SSL_KEYFILE": {{ if .Values.cache.ssl.keyfile }}{{ .Values.cache.ssl.keyfile | quote }}{{ else }}None{{ end }}, + "CACHE_REDIS_SSL_CERT_REQS": {{ .Values.cache.ssl.ssl_cert_reqs | default "required" | quote }}, + "CACHE_REDIS_SSL_CA_CERTS": {{ if .Values.cache.ssl.ca_certs }}{{ .Values.cache.ssl.ca_certs | quote }}{{ else }}None{{ end }}, + {{- else }} + "CACHE_REDIS_SSL": False, + "CACHE_REDIS_SSL_CERTFILE": None, + "CACHE_REDIS_SSL_KEYFILE": None, + "CACHE_REDIS_SSL_CERT_REQS": {{ .Values.cache.ssl.ssl_cert_reqs | default "required" | quote }}, + "CACHE_REDIS_SSL_CA_CERTS": None, + {{- end }} +} +{{- end }} + +{{- /* Global Async Queries Results Backend */}} +{{- if .Values.config.GLOBAL_ASYNC_QUERIES_RESULTS_BACKEND }} +GLOBAL_ASYNC_QUERIES_RESULTS_BACKEND = {{ .Values.config.GLOBAL_ASYNC_QUERIES_RESULTS_BACKEND | toJson | indent 2 }} +{{- else if .Values.cache.enabled }} +GLOBAL_ASYNC_QUERIES_RESULTS_BACKEND = { + "backend": "redis", + "host": {{ $redisHostForBackend | quote }}, + "port": {{ $redisPortForBackend | int }}, + "prefix": {{ .Values.cache.asyncQueries.keyPrefix | default "qc-" | quote }}, + "db": {{ .Values.cache.asyncQueries.db | default .Values.cache.cacheDb | default 0 | int }}, + {{- if $redisPasswordForBackend }} + "password": {{ $redisPasswordForBackend | quote }}, + {{- end }} +} +{{- end }} + +{{- /* Feature Flags */}} +{{- if .Values.featureFlags }} +FEATURE_FLAGS = { +{{- range $key, $value := .Values.featureFlags }} +{{- if kindIs "bool" $value }} + "{{ $key }}": {{ if $value }}True{{ else }}False{{ end }}, +{{- else if kindIs "string" $value }} + "{{ $key }}": {{ $value | quote }}, +{{- else if kindIs "float64" $value }} + "{{ $key }}": {{ $value }}, +{{- else if kindIs "int" $value }} + "{{ $key }}": {{ $value }}, +{{- else }} + "{{ $key }}": {{ $value | toJson }}, +{{- end }} +{{- end }} +} +{{- end }} + +{{- /* FAB Security API - Required for List Roles view in 6.0.0+ */}} +{{- if not (hasKey .Values.config "FAB_ADD_SECURITY_API") }} +FAB_ADD_SECURITY_API = True +{{- end }} +{{- if not (hasKey .Values.config "FAB_ADD_SECURITY_VIEWS") }} +FAB_ADD_SECURITY_VIEWS = True +{{- end }} + +{{- /* Global Async Queries Transport - Auto-configure for websockets if enabled */}} +{{- if .Values.config.GLOBAL_ASYNC_QUERIES_TRANSPORT }} +GLOBAL_ASYNC_QUERIES_TRANSPORT = {{ .Values.config.GLOBAL_ASYNC_QUERIES_TRANSPORT | quote }} +{{- else if .Values.supersetWebsockets.enabled }} +GLOBAL_ASYNC_QUERIES_TRANSPORT = "ws" +{{- else }} +GLOBAL_ASYNC_QUERIES_TRANSPORT = "polling" +{{- end }} + +{{- /* Global Async Queries WebSocket URL */}} +{{- $wsUrl := "" }} +{{- if .Values.config.GLOBAL_ASYNC_QUERIES_WEBSOCKET_URL }} +{{- $wsUrl = .Values.config.GLOBAL_ASYNC_QUERIES_WEBSOCKET_URL }} +GLOBAL_ASYNC_QUERIES_WEBSOCKET_URL = {{ $wsUrl | quote }} +{{- else if and .Values.supersetWebsockets.enabled .Values.supersetWebsockets.websocketUrl }} +{{- $wsUrl = .Values.supersetWebsockets.websocketUrl }} +GLOBAL_ASYNC_QUERIES_WEBSOCKET_URL = {{ $wsUrl | quote }} +{{- else if .Values.supersetWebsockets.enabled }} +{{- $wsServiceName := .Values.cluster.websocketServiceName }} +{{- if not $wsServiceName }} +{{- $wsServiceName = printf "%s-ws" (include "superset.fullname" .) }} +{{- end }} +{{- $wsPort := .Values.supersetWebsockets.service.port | default 8080 }} +{{- $wsPath := "/ws" }} +{{- $clusterDomain := .Values.cluster.domain | default ".svc.cluster.local" }} +{{- $wsUrl = printf "ws://%s.%s%s:%d%s" $wsServiceName .Release.Namespace $clusterDomain $wsPort $wsPath }} +GLOBAL_ASYNC_QUERIES_WEBSOCKET_URL = {{ $wsUrl | quote }} +{{- end }} + +{{- /* Global Async Queries JWT Secret */}} +{{- if .Values.config.GLOBAL_ASYNC_QUERIES_JWT_SECRET }} +GLOBAL_ASYNC_QUERIES_JWT_SECRET = {{ .Values.config.GLOBAL_ASYNC_QUERIES_JWT_SECRET | quote }} +{{- else if and .Values.supersetWebsockets.enabled .Values.supersetWebsockets.config.jwtSecret }} +GLOBAL_ASYNC_QUERIES_JWT_SECRET = {{ .Values.supersetWebsockets.config.jwtSecret | quote }} +{{- end }} + +{{- /* Global Async Queries JWT Cookie Settings */}} +{{- if hasKey .Values.config "GLOBAL_ASYNC_QUERIES_JWT_COOKIE_SECURE" }} +GLOBAL_ASYNC_QUERIES_JWT_COOKIE_SECURE = {{ .Values.config.GLOBAL_ASYNC_QUERIES_JWT_COOKIE_SECURE | toString | title }} +{{- else if and .Values.supersetWebsockets.enabled (or (hasPrefix "wss://" $wsUrl) .Values.ingress.tls) }} +GLOBAL_ASYNC_QUERIES_JWT_COOKIE_SECURE = True +{{- else if .Values.supersetWebsockets.enabled }} +GLOBAL_ASYNC_QUERIES_JWT_COOKIE_SECURE = False +{{- end }} + +{{- if .Values.config.GLOBAL_ASYNC_QUERIES_JWT_COOKIE_SAMESITE }} +GLOBAL_ASYNC_QUERIES_JWT_COOKIE_SAMESITE = {{ .Values.config.GLOBAL_ASYNC_QUERIES_JWT_COOKIE_SAMESITE | quote }} +{{- else if .Values.supersetWebsockets.enabled }} +GLOBAL_ASYNC_QUERIES_JWT_COOKIE_SAMESITE = "Lax" +{{- end }} + +{{- if .Values.config.GLOBAL_ASYNC_QUERIES_JWT_COOKIE_NAME }} +GLOBAL_ASYNC_QUERIES_JWT_COOKIE_NAME = {{ .Values.config.GLOBAL_ASYNC_QUERIES_JWT_COOKIE_NAME | quote }} +{{- else if and .Values.supersetWebsockets.enabled .Values.supersetWebsockets.config.jwtCookieName }} +GLOBAL_ASYNC_QUERIES_JWT_COOKIE_NAME = {{ .Values.supersetWebsockets.config.jwtCookieName | quote }} +{{- end }} + +{{- /* Content Security Policy (CSP) */}} +{{- if and .Values.supersetWebsockets.enabled $wsUrl (not (hasKey .Values.config "TALISMAN_CONFIG")) }} +TALISMAN_CONFIG = { + "content_security_policy": { + "base-uri": ["'self'"], + "default-src": ["'self'"], + "img-src": [ + "'self'", + "blob:", + "data:", + "https://apachesuperset.gateway.scarf.sh", + "https://static.scarf.sh/", + "ows.terrestris.de", + "https://cdn.document360.io", + ], + "worker-src": ["'self'", "blob:"], + "connect-src": [ + "'self'", + {{ $wsUrl | quote }}, + "https://api.mapbox.com", + "https://events.mapbox.com", + "https://tile.openstreetmap.org", + "https://tile.osm.ch", + ], + "object-src": "'none'", + "style-src": [ + "'self'", + "'unsafe-inline'", + ], + "script-src": ["'self'", "'strict-dynamic'"], + }, + "content_security_policy_nonce_in": ["script-src"], + {{- if or (hasPrefix "wss://" $wsUrl) .Values.ingress.tls }} + "force_https": True, + "session_cookie_secure": True, + {{- else }} + "force_https": False, + "session_cookie_secure": False, + {{- end }} +} +{{- end }} -{{ if .Values.configOverrides }} -# Overrides +{{- /* General Configuration - iterate through all config values */}} +{{- range $key, $value := .Values.config }} +{{- if and (ne $key "cacheConfig") (ne $key "dataCacheConfig") (ne $key "celeryConfig") (ne $key "resultsBackend") (ne $key "GLOBAL_ASYNC_QUERIES_CACHE_BACKEND") (ne $key "GLOBAL_ASYNC_QUERIES_RESULTS_BACKEND") (ne $key "GLOBAL_ASYNC_QUERIES_TRANSPORT") (ne $key "GLOBAL_ASYNC_QUERIES_WEBSOCKET_URL") (ne $key "GLOBAL_ASYNC_QUERIES_JWT_SECRET") (ne $key "GLOBAL_ASYNC_QUERIES_JWT_COOKIE_SECURE") (ne $key "GLOBAL_ASYNC_QUERIES_JWT_COOKIE_SAMESITE") (ne $key "GLOBAL_ASYNC_QUERIES_JWT_COOKIE_NAME") (ne $key "TALISMAN_CONFIG") (ne $key "SQLLAB_ASYNC_TIME_LIMIT_SEC") (ne $key "SQLALCHEMY_TRACK_MODIFICATIONS") }} +{{- if kindIs "map" $value }} +{{ $key }} = {{ $value | toJson | indent 2 }} +{{- else if kindIs "slice" $value }} +{{ $key }} = {{ $value | toJson }} +{{- else if kindIs "bool" $value }} +{{ $key }} = {{ if $value }}True{{ else }}False{{ end }} +{{- else if kindIs "string" $value }} +{{- if or (hasPrefix "f\"" $value) (hasPrefix "F\"" $value) (hasPrefix "r\"" $value) (hasPrefix "R\"" $value) (hasPrefix "b\"" $value) (hasPrefix "B\"" $value) }} +{{ $key }} = {{ $value }} +{{- else }} +{{ $key }} = {{ $value | quote }} +{{- end }} +{{- else }} +{{ $key }} = {{ $value | toJson }} +{{- end }} +{{- end }} +{{- end }} + +{{- /* Custom Config Overrides */}} +{{- if .Values.configOverrides }} +# Custom Overrides {{- range $key, $value := .Values.configOverrides }} # {{ $key }} {{ tpl $value $ }} {{- end }} {{- end }} -{{ if .Values.configOverridesFiles }} +{{- if .Values.configOverridesFiles }} # Overrides from files {{- $files := .Files }} {{- range $key, $value := .Values.configOverridesFiles }} @@ -187,7 +672,54 @@ RESULTS_BACKEND = RedisCache( {{- end }} {{- end }} +{{- end -}} + +{{- define "superset.initScript" -}} +#!/bin/sh +set -eu +echo "Upgrading DB schema..." +superset db upgrade +echo "Initializing roles and permissions..." +superset init +echo "Init job: Creating admin user and loading initial data..." +{{- if .Values.init.createAdmin }} +echo "Creating admin user..." +superset fab create-admin \ + --username {{ .Values.init.adminUser.username | quote }} \ + --firstname {{ .Values.init.adminUser.firstname | quote }} \ + --lastname {{ .Values.init.adminUser.lastname | quote }} \ + --email {{ .Values.init.adminUser.email | quote }} \ + --password {{ .Values.init.adminUser.password | quote }} \ + || true +{{- else }} +echo "Skipping admin creation (init.createAdmin=false)" {{- end }} +{{- if .Values.init.loadExamples }} +echo "Loading examples..." +superset load_examples +{{- else }} +echo "Skipping examples (init.loadExamples=false)" +{{- end }} +if [ -f "{{ .Values.extraConfigMountPath }}/import_datasources.yaml" ]; then + echo "Importing database connections..." + superset import_datasources -p {{ .Values.extraConfigMountPath }}/import_datasources.yaml +fi +echo "Init job complete." +{{- end -}} + +{{/* +Deprecation warnings — returns a newline-separated list of active deprecation messages, +or empty string when no deprecated keys are set. Rendered in NOTES.txt after install/upgrade. +*/}} +{{- define "superset.deprecationWarnings" -}} +{{- $conn := include "_superset.legacyConn" . | fromJson -}} +{{- if gt (len (keys $conn)) 0 }} +- supersetNode.connections.* is deprecated; use database.* and cache.* (auto-mapped for now). See UPGRADING.md +{{- end }} +{{- if .Values.serviceAccountName }} +- root serviceAccountName is deprecated; use serviceAccount.name (auto-mapped for now). See UPGRADING.md +{{- end }} +{{- end -}} {{- define "supersetNode.selectorLabels" -}} app.kubernetes.io/name: {{ include "superset.name" . }} diff --git a/helm/superset/templates/deployment-beat.yaml b/helm/superset/templates/deployment-beat.yaml index be285659450b..53f6a5fb61ef 100644 --- a/helm/superset/templates/deployment-beat.yaml +++ b/helm/superset/templates/deployment-beat.yaml @@ -37,9 +37,12 @@ spec: template: metadata: annotations: - checksum/superset_config.py: {{ include "superset-config" . | sha256sum }} + checksum/superset_config.py: {{ include "superset.config" . | sha256sum }} checksum/superset_bootstrap.sh: {{ tpl .Values.bootstrapScript . | sha256sum }} - checksum/connections: {{ .Values.supersetNode.connections | toYaml | sha256sum }} + checksum/database: {{ .Values.database | toYaml | sha256sum }} + checksum/redis: {{ .Values.cache | toYaml | sha256sum }} + checksum/config: {{ .Values.config | toYaml | sha256sum }} + checksum/featureFlags: {{ .Values.featureFlags | toYaml | sha256sum }} checksum/extraConfigs: {{ .Values.extraConfigs | toYaml | sha256sum }} checksum/extraSecrets: {{ .Values.extraSecrets | toYaml | sha256sum }} checksum/extraSecretEnv: {{ .Values.extraSecretEnv | toYaml | sha256sum }} @@ -52,6 +55,9 @@ spec: {{- if .Values.supersetCeleryBeat.podAnnotations }} {{- toYaml .Values.supersetCeleryBeat.podAnnotations | nindent 8 }} {{- end }} + {{- with .Values.globalPodAnnotations }} + {{- toYaml . | nindent 8 }} + {{- end }} labels: {{- include "supersetCeleryBeat.selectorLabels" . | nindent 8 }} {{- if .Values.supersetCeleryBeat.podLabels }} diff --git a/helm/superset/templates/deployment-flower.yaml b/helm/superset/templates/deployment-flower.yaml index 27e3080eed8d..07bb5d7cbe42 100644 --- a/helm/superset/templates/deployment-flower.yaml +++ b/helm/superset/templates/deployment-flower.yaml @@ -36,11 +36,14 @@ spec: template: metadata: annotations: - checksum/config: {{ include "superset-config" . | sha256sum }} + checksum/config: {{ include "superset.config" . | sha256sum }} checksum/secrets: {{ tpl (toJson .Values.extraSecretEnv) . | sha256sum }} {{- if .Values.supersetCeleryFlower.podAnnotations }} {{- toYaml .Values.supersetCeleryFlower.podAnnotations | nindent 8 }} {{- end }} + {{- with .Values.globalPodAnnotations }} + {{- toYaml . | nindent 8 }} + {{- end }} labels: {{- include "supersetCeleryFlower.selectorLabels" . | nindent 8 }} {{- if .Values.supersetCeleryFlower.podLabels }} diff --git a/helm/superset/templates/deployment-worker.yaml b/helm/superset/templates/deployment-worker.yaml index 60034b7c7210..6c37d3e4e46c 100644 --- a/helm/superset/templates/deployment-worker.yaml +++ b/helm/superset/templates/deployment-worker.yaml @@ -43,9 +43,12 @@ spec: template: metadata: annotations: - checksum/superset_config.py: {{ include "superset-config" . | sha256sum }} + checksum/superset_config.py: {{ include "superset.config" . | sha256sum }} checksum/superset_bootstrap.sh: {{ tpl .Values.bootstrapScript . | sha256sum }} - checksum/connections: {{ .Values.supersetNode.connections | toYaml | sha256sum }} + checksum/database: {{ .Values.database | toYaml | sha256sum }} + checksum/redis: {{ .Values.cache | toYaml | sha256sum }} + checksum/config: {{ .Values.config | toYaml | sha256sum }} + checksum/featureFlags: {{ .Values.featureFlags | toYaml | sha256sum }} checksum/extraConfigs: {{ .Values.extraConfigs | toYaml | sha256sum }} checksum/extraSecrets: {{ .Values.extraSecrets | toYaml | sha256sum }} checksum/extraSecretEnv: {{ .Values.extraSecretEnv | toYaml | sha256sum }} @@ -58,6 +61,9 @@ spec: {{- if .Values.supersetWorker.podAnnotations }} {{- toYaml .Values.supersetWorker.podAnnotations | nindent 8 }} {{- end }} + {{- with .Values.globalPodAnnotations }} + {{- toYaml . | nindent 8 }} + {{- end }} labels: {{- include "supersetWorker.selectorLabels" . | nindent 8 }} {{- if .Values.supersetWorker.podLabels }} diff --git a/helm/superset/templates/deployment-ws.yaml b/helm/superset/templates/deployment-ws.yaml index d7b15de3e2bf..8968fe74a3b5 100644 --- a/helm/superset/templates/deployment-ws.yaml +++ b/helm/superset/templates/deployment-ws.yaml @@ -44,6 +44,9 @@ spec: {{- if .Values.supersetWebsockets.podAnnotations }} {{- toYaml .Values.supersetWebsockets.podAnnotations | nindent 8 }} {{- end }} + {{- with .Values.globalPodAnnotations }} + {{- toYaml . | nindent 8 }} + {{- end }} labels: {{- include "supersetWebsockets.selectorLabels" . | nindent 8 }} {{- if .Values.supersetWebsockets.podLabels }} diff --git a/helm/superset/templates/deployment.yaml b/helm/superset/templates/deployment.yaml index 1686af945a39..229ed8a36f4b 100644 --- a/helm/superset/templates/deployment.yaml +++ b/helm/superset/templates/deployment.yaml @@ -44,10 +44,13 @@ spec: metadata: annotations: # Force reload on config changes - checksum/superset_config.py: {{ include "superset-config" . | sha256sum }} - checksum/superset_init.sh: {{ tpl .Values.init.initscript . | sha256sum }} + checksum/superset_config.py: {{ include "superset.config" . | sha256sum }} + checksum/superset_init.sh: {{ include "superset.initScript" . | sha256sum }} checksum/superset_bootstrap.sh: {{ tpl .Values.bootstrapScript . | sha256sum }} - checksum/connections: {{ .Values.supersetNode.connections | toYaml | sha256sum }} + checksum/database: {{ .Values.database | toYaml | sha256sum }} + checksum/redis: {{ .Values.cache | toYaml | sha256sum }} + checksum/config: {{ .Values.config | toYaml | sha256sum }} + checksum/featureFlags: {{ .Values.featureFlags | toYaml | sha256sum }} checksum/extraConfigs: {{ .Values.extraConfigs | toYaml | sha256sum }} checksum/extraSecrets: {{ .Values.extraSecrets | toYaml | sha256sum }} checksum/extraSecretEnv: {{ .Values.extraSecretEnv | toYaml | sha256sum }} @@ -60,6 +63,9 @@ spec: {{- if .Values.supersetNode.podAnnotations }} {{- toYaml .Values.supersetNode.podAnnotations | nindent 8 }} {{- end }} + {{- with .Values.globalPodAnnotations }} + {{- toYaml . | nindent 8 }} + {{- end }} labels: {{- include "supersetNode.selectorLabels" . | nindent 8 }} {{- if .Values.supersetNode.podLabels }} diff --git a/helm/superset/templates/secret-env.yaml b/helm/superset/templates/secret-env.yaml index b16cd7550305..14dbe55ed674 100644 --- a/helm/superset/templates/secret-env.yaml +++ b/helm/superset/templates/secret-env.yaml @@ -26,24 +26,28 @@ metadata: {{- include "superset.labels" . | nindent 4 }} type: Opaque stringData: - REDIS_HOST: {{ tpl .Values.supersetNode.connections.redis_host . | quote }} - REDIS_USER: {{ .Values.supersetNode.connections.redis_user | quote }} - {{- if .Values.supersetNode.connections.redis_password }} - REDIS_PASSWORD: {{ .Values.supersetNode.connections.redis_password | quote }} + DB_HOST: {{ include "superset.db.host" . | quote }} + DB_PORT: {{ include "superset.db.port" . | quote }} + DB_USER: {{ include "superset.db.user" . | quote }} + DB_PASS: {{ include "superset.db.password" . | quote }} + DB_NAME: {{ include "superset.db.name" . | quote }} + {{- if .Values.database.ssl.enabled }} + DB_SSL_MODE: {{ .Values.database.ssl.mode | default "require" | quote }} + {{- end }} + {{- if .Values.cache.enabled }} + REDIS_HOST: {{ include "superset.redis.host" . | quote }} + REDIS_PORT: {{ include "superset.redis.port" . | quote }} + REDIS_USER: {{ include "superset.redis.user" . | quote }} + {{- if include "superset.redis.password" . }} + REDIS_PASSWORD: {{ include "superset.redis.password" . | quote }} + {{- end }} + REDIS_DB: {{ include "superset.redis.cacheDb" . | quote }} + REDIS_CELERY_DB: {{ include "superset.redis.celeryDb" . | quote }} + REDIS_PROTO: {{ include "superset.redis.proto" . | quote }} + {{- if .Values.cache.driver }} + REDIS_DRIVER: {{ .Values.cache.driver | quote }} {{- end }} - REDIS_PORT: {{ .Values.supersetNode.connections.redis_port | quote }} - REDIS_PROTO: {{ if .Values.supersetNode.connections.redis_ssl.enabled }}"rediss"{{ else }}"redis"{{ end }} - REDIS_DRIVER: {{ .Values.supersetNode.connections.redis_driver | quote }} - REDIS_DB: {{ .Values.supersetNode.connections.redis_cache_db | quote }} - REDIS_CELERY_DB: {{ .Values.supersetNode.connections.redis_celery_db | quote }} - {{- if .Values.supersetNode.connections.redis_ssl.enabled }} - REDIS_SSL_CERT_REQS: {{ .Values.supersetNode.connections.redis_ssl.ssl_cert_reqs | default "CERT_NONE" | quote }} {{- end }} - DB_HOST: {{ tpl .Values.supersetNode.connections.db_host . | quote }} - DB_PORT: {{ .Values.supersetNode.connections.db_port | quote }} - DB_USER: {{ .Values.supersetNode.connections.db_user | quote }} - DB_PASS: {{ .Values.supersetNode.connections.db_pass | quote }} - DB_NAME: {{ .Values.supersetNode.connections.db_name | quote }} {{- if .Values.extraSecretEnv }} {{- range $key, $value := .Values.extraSecretEnv }} {{ $key }}: {{ $value | quote }} diff --git a/helm/superset/templates/secret-superset-config.yaml b/helm/superset/templates/secret-superset-config.yaml index 4a3d5d0ee853..6052cc4611e4 100644 --- a/helm/superset/templates/secret-superset-config.yaml +++ b/helm/superset/templates/secret-superset-config.yaml @@ -27,9 +27,9 @@ metadata: type: Opaque stringData: superset_config.py: | - {{- include "superset-config" . | nindent 4 }} + {{- include "superset.config" . | nindent 4 }} superset_init.sh: | - {{- tpl .Values.init.initscript . | nindent 4 }} + {{- include "superset.initScript" . | nindent 4 }} superset_bootstrap.sh: | {{- tpl .Values.bootstrapScript . | nindent 4 }} diff --git a/helm/superset/tests/checksum_test.yaml b/helm/superset/tests/checksum_test.yaml new file mode 100644 index 000000000000..84981c7b19b6 --- /dev/null +++ b/helm/superset/tests/checksum_test.yaml @@ -0,0 +1,132 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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: config checksums + global pod annotations +templates: + - deployment.yaml + - deployment-worker.yaml + - deployment-beat.yaml + - deployment-flower.yaml + - deployment-ws.yaml +tests: + - it: main deployment has new config checksums, not the old connections checksum + template: deployment.yaml + asserts: + - exists: + path: 'spec.template.metadata.annotations["checksum/database"]' + - exists: + path: 'spec.template.metadata.annotations["checksum/config"]' + - notExists: + path: 'spec.template.metadata.annotations["checksum/connections"]' + - it: main deployment applies globalPodAnnotations + template: deployment.yaml + set: + globalPodAnnotations: + team: data + asserts: + - equal: + path: 'spec.template.metadata.annotations.team' + value: data + - it: globalPodAnnotations coexist with per-component podAnnotations on deployment.yaml + template: deployment.yaml + set: + globalPodAnnotations: + global: "yes" + supersetNode: + podAnnotations: + component: "yes" + asserts: + - equal: + path: 'spec.template.metadata.annotations.global' + value: "yes" + - equal: + path: 'spec.template.metadata.annotations.component' + value: "yes" + - it: worker deployment has config and database checksums + template: deployment-worker.yaml + asserts: + - exists: + path: 'spec.template.metadata.annotations["checksum/database"]' + - exists: + path: 'spec.template.metadata.annotations["checksum/config"]' + - it: worker deployment applies globalPodAnnotations + template: deployment-worker.yaml + set: + globalPodAnnotations: + team: data + asserts: + - equal: + path: 'spec.template.metadata.annotations.team' + value: data + - it: beat deployment has config and database checksums + template: deployment-beat.yaml + set: + supersetCeleryBeat: + enabled: true + asserts: + - exists: + path: 'spec.template.metadata.annotations["checksum/database"]' + - exists: + path: 'spec.template.metadata.annotations["checksum/config"]' + - it: beat deployment applies globalPodAnnotations + template: deployment-beat.yaml + set: + supersetCeleryBeat: + enabled: true + globalPodAnnotations: + team: data + asserts: + - equal: + path: 'spec.template.metadata.annotations.team' + value: data + - it: flower deployment has config checksum + template: deployment-flower.yaml + set: + supersetCeleryFlower: + enabled: true + asserts: + - exists: + path: 'spec.template.metadata.annotations["checksum/config"]' + - it: flower deployment applies globalPodAnnotations + template: deployment-flower.yaml + set: + supersetCeleryFlower: + enabled: true + globalPodAnnotations: + team: data + asserts: + - equal: + path: 'spec.template.metadata.annotations.team' + value: data + - it: websockets deployment has wsconfig checksum + template: deployment-ws.yaml + set: + supersetWebsockets: + enabled: true + asserts: + - exists: + path: 'spec.template.metadata.annotations["checksum/wsconfig"]' + - it: websockets deployment applies globalPodAnnotations + template: deployment-ws.yaml + set: + supersetWebsockets: + enabled: true + globalPodAnnotations: + team: data + asserts: + - equal: + path: 'spec.template.metadata.annotations.team' + value: data diff --git a/helm/superset/tests/config_test.yaml b/helm/superset/tests/config_test.yaml new file mode 100644 index 000000000000..0e653ff008bc --- /dev/null +++ b/helm/superset/tests/config_test.yaml @@ -0,0 +1,60 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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: superset_config.py rendering +templates: + - secret-superset-config.yaml +tests: + - it: builds SQLALCHEMY_DATABASE_URI from new schema + set: {database: {host: pg.svc, port: 5432, user: su, password: pw, name: sset}} + asserts: + - matchRegex: {path: 'stringData["superset_config.py"]', pattern: 'postgresql\+psycopg2://su:pw@pg\.svc:5432/sset'} + - it: honors database.uri override + set: {database.uri: "postgresql://x:y@h:5432/d"} + asserts: + - matchRegex: {path: 'stringData["superset_config.py"]', pattern: 'SQLALCHEMY_DATABASE_URI = "postgresql://x:y@h:5432/d"'} + - it: maps legacy connection keys into the URI (deprecation path) + set: {supersetNode.connections: {db_host: legacy, db_port: "5432", db_user: u, db_pass: p, db_name: n}} + asserts: + - matchRegex: {path: 'stringData["superset_config.py"]', pattern: '@legacy:5432/n'} + - it: SQLALCHEMY_TRACK_MODIFICATIONS defaults False + asserts: + - matchRegex: {path: 'stringData["superset_config.py"]', pattern: 'SQLALCHEMY_TRACK_MODIFICATIONS = False'} + - it: renders feature flags + set: {featureFlags: {ALERT_REPORTS: true}} + asserts: + - matchRegex: {path: 'stringData["superset_config.py"]', pattern: '"ALERT_REPORTS": True'} + - it: SQLALCHEMY_TRACK_MODIFICATIONS user-set true renders Python True + set: {config: {SQLALCHEMY_TRACK_MODIFICATIONS: true}} + asserts: + - matchRegex: {path: 'stringData["superset_config.py"]', pattern: 'SQLALCHEMY_TRACK_MODIFICATIONS = True'} + - it: celeryConfig without imports does not emit CELERY_IMPORTS + set: {config: {celeryConfig: {broker_url: "redis://x"}}} + asserts: + - notMatchRegex: {path: 'stringData["superset_config.py"]', pattern: 'CELERY_IMPORTS'} + - it: sentinel disabled does not fail render + set: {cache: {enabled: true, sentinel: {enabled: false}}} + asserts: + - hasDocuments: + count: 1 + - it: ssl certfile path is quoted + set: {cache: {enabled: true, ssl: {enabled: true, certfile: "/etc/ssl/cert.pem"}}} + asserts: + - matchRegex: {path: 'stringData["superset_config.py"]', pattern: '"CACHE_REDIS_SSL_CERTFILE": "/etc/ssl/cert.pem"'} + - it: explicit JWT cookie secure false is respected + set: {supersetWebsockets: {enabled: true}, config: {GLOBAL_ASYNC_QUERIES_JWT_COOKIE_SECURE: false}} + asserts: + - matchRegex: {path: 'stringData["superset_config.py"]', pattern: 'GLOBAL_ASYNC_QUERIES_JWT_COOKIE_SECURE = False'} diff --git a/helm/superset/tests/deprecation_test.yaml b/helm/superset/tests/deprecation_test.yaml new file mode 100644 index 000000000000..5e9183b23593 --- /dev/null +++ b/helm/superset/tests/deprecation_test.yaml @@ -0,0 +1,28 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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: deprecation path does not fail render +templates: + - secret-superset-config.yaml +tests: + - it: legacy supersetNode.connections still renders (no fail) + set: {supersetNode.connections.db_host: legacy} + asserts: + - hasDocuments: {count: 1} + - it: legacy root serviceAccountName still renders (no fail) + set: {serviceAccountName: my-sa} + asserts: + - hasDocuments: {count: 1} diff --git a/helm/superset/tests/initscript_test.yaml b/helm/superset/tests/initscript_test.yaml new file mode 100644 index 000000000000..f1906bf47e78 --- /dev/null +++ b/helm/superset/tests/initscript_test.yaml @@ -0,0 +1,50 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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: superset_init.sh rendering +templates: + - secret-superset-config.yaml +tests: + - it: renders superset db upgrade in init script + asserts: + - matchRegex: + path: 'stringData["superset_init.sh"]' + pattern: 'superset db upgrade' + - it: renders superset init in init script + asserts: + - matchRegex: + path: 'stringData["superset_init.sh"]' + pattern: 'superset init' + - it: renders db upgrade before admin creation + set: + init: + createAdmin: true + adminUser: + username: admin + firstname: Admin + lastname: User + email: admin@example.com + password: adminpass + asserts: + - matchRegex: + path: 'stringData["superset_init.sh"]' + pattern: 'superset db upgrade' + - matchRegex: + path: 'stringData["superset_init.sh"]' + pattern: 'superset init' + - matchRegex: + path: 'stringData["superset_init.sh"]' + pattern: 'fab create-admin' diff --git a/helm/superset/tests/secret_env_test.yaml b/helm/superset/tests/secret_env_test.yaml new file mode 100644 index 000000000000..1f5e62d2370b --- /dev/null +++ b/helm/superset/tests/secret_env_test.yaml @@ -0,0 +1,43 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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: secret-env schema + deprecation mapping +templates: + - secret-env.yaml +tests: + - it: emits DB_* and REDIS_* from new schema + set: + database: {host: pg.svc, port: 5432, user: su, password: pw, name: superset} + cache: {enabled: true, host: rd.svc, port: 6379, cacheDb: 1, celeryDb: 0} + asserts: + - equal: {path: 'stringData.DB_HOST', value: pg.svc} + - equal: {path: 'stringData.DB_PORT', value: "5432"} + - equal: {path: 'stringData.REDIS_HOST', value: rd.svc} + - it: maps legacy supersetNode.connections.* (deprecation path, no fail) + set: + supersetNode.connections.db_host: legacy-pg + supersetNode.connections.redis_host: legacy-rd + asserts: + - equal: {path: 'stringData.DB_HOST', value: legacy-pg} + - equal: {path: 'stringData.REDIS_HOST', value: legacy-rd} + - it: emits REDIS_DRIVER when cache.driver set + set: {cache: {enabled: true, driver: "rediss+managed"}} + asserts: + - equal: {path: 'stringData.REDIS_DRIVER', value: "rediss+managed"} + - it: omits redis vars when cache disabled + set: {cache.enabled: false} + asserts: + - notExists: {path: 'stringData.REDIS_HOST'} diff --git a/helm/superset/tests/serviceaccount_test.yaml b/helm/superset/tests/serviceaccount_test.yaml new file mode 100644 index 000000000000..ca5922e3645a --- /dev/null +++ b/helm/superset/tests/serviceaccount_test.yaml @@ -0,0 +1,28 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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: serviceAccountName resolution +templates: + - serviceaccount.yaml +tests: + - it: uses serviceAccount.name when set + set: {serviceAccount: {create: true, name: my-sa}} + asserts: + - equal: {path: 'metadata.name', value: my-sa} + - it: falls back to legacy root serviceAccountName (deprecation path) + set: {serviceAccount: {create: true}, serviceAccountName: legacy-sa} + asserts: + - equal: {path: 'metadata.name', value: legacy-sa} diff --git a/helm/superset/tests/values_defaults_test.yaml b/helm/superset/tests/values_defaults_test.yaml new file mode 100644 index 000000000000..1cef6e4bcf43 --- /dev/null +++ b/helm/superset/tests/values_defaults_test.yaml @@ -0,0 +1,30 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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: values defaults +templates: + - secret-env.yaml +tests: + - it: renders with new database defaults + set: + database.host: db.example + cache.enabled: true + asserts: + - hasDocuments: + count: 1 + - equal: + path: 'stringData.DB_HOST' + value: db.example diff --git a/helm/superset/values.schema.json b/helm/superset/values.schema.json new file mode 100644 index 000000000000..7d9103e6baef --- /dev/null +++ b/helm/superset/values.schema.json @@ -0,0 +1,140 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": true, + "properties": { + "database": { + "type": "object", + "additionalProperties": true, + "properties": { + "uri": { + "type": ["string", "null"] + }, + "driver": { + "type": "string" + }, + "host": { + "type": ["string", "null"] + }, + "port": { + "type": "integer" + }, + "user": { + "type": ["string", "null"] + }, + "password": { + "type": ["string", "null"] + }, + "name": { + "type": ["string", "null"] + }, + "ssl": { + "type": "object", + "additionalProperties": true, + "properties": { + "enabled": { + "type": "boolean" + }, + "mode": { + "type": "string" + } + } + } + } + }, + "cache": { + "type": "object", + "additionalProperties": true, + "properties": { + "enabled": { + "type": "boolean" + }, + "driver": { + "type": "string" + }, + "host": { + "type": ["string", "null"] + }, + "port": { + "type": "integer" + }, + "cacheDb": { + "type": "integer" + }, + "celeryDb": { + "type": "integer" + }, + "ssl": { + "type": "object", + "additionalProperties": true, + "properties": { + "enabled": { + "type": "boolean" + } + } + } + } + }, + "cluster": { + "type": "object", + "additionalProperties": true, + "properties": { + "domain": { + "type": "string" + }, + "databaseServiceName": { + "type": ["string", "null"] + }, + "redisServiceName": { + "type": ["string", "null"] + }, + "websocketServiceName": { + "type": ["string", "null"] + } + } + }, + "config": { + "type": "object", + "additionalProperties": true + }, + "featureFlags": { + "type": "object", + "additionalProperties": true + }, + "serviceAccount": { + "type": "object", + "additionalProperties": true, + "properties": { + "create": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "annotations": { + "type": "object", + "additionalProperties": true + } + } + }, + "globalPodAnnotations": { + "type": "object", + "additionalProperties": true + }, + "image": { + "type": "object", + "additionalProperties": true, + "properties": { + "repository": { + "type": "string" + }, + "tag": { + "type": ["string", "null"] + }, + "pullPolicy": { + "type": "string" + } + } + } + } +} diff --git a/helm/superset/values.yaml b/helm/superset/values.yaml index a9f8073bf029..61ebffe12e7a 100644 --- a/helm/superset/values.yaml +++ b/helm/superset/values.yaml @@ -28,9 +28,30 @@ nameOverride: ~ # -- Provide a name to override the full names of resources fullnameOverride: ~ +# -- Kubernetes cluster configuration +# Used for constructing service URLs between chart components +cluster: + # -- Kubernetes cluster domain (default: .svc.cluster.local) + # Override if using a custom cluster domain + domain: ".svc.cluster.local" + # -- Database service name (default: {{ .Release.Name }}-postgresql) + # Override if using a different service name for the database + databaseServiceName: ~ + # -- Redis service name (default: {{ .Release.Name }}-redis-headless) + # Override if using a different service name for Redis + redisServiceName: ~ + # -- WebSocket service name (default: {{ .Release.Name }}-ws) + # Override if using a different service name for the WebSocket service + websocketServiceName: ~ + # -- Labels to be added to all resources extraLabels: {} +# -- Global pod annotations to be added to all pods +# Use this to set annotations that apply to all Superset components +# Component-specific podAnnotations will be merged with these global annotations +globalPodAnnotations: {} + # -- User ID directive. This user must have enough permissions to run the bootstrap script # Running containers as root is not recommended in production. Change this to another UID - e.g. 1000 to be more secure runAsUser: 0 @@ -46,6 +67,8 @@ serviceAccountName: ~ serviceAccount: # -- Create custom service account for Superset. If create: true and serviceAccountName is not provided, `superset.fullname` will be used. create: false + # -- Service account name to use (if not specified, defaults to release name + chart name) + name: "" annotations: {} # -- Install additional packages and do any other bootstrap configuration in this script @@ -244,6 +267,88 @@ hostAliases: [] # - nodns.my.lan # ip: 18.27.36.45 +# -- Database connection configuration for the Superset metadata database +database: + # -- Full database URI (overrides host/port/user/pass/name if set) + # Example: "postgresql+psycopg2://user:pass@host:5432/dbname" + uri: ~ + # -- Database driver (used when uri is not set) + driver: "postgresql+psycopg2" + # -- Database host (default: {{ .Release.Name }}-postgresql) + host: ~ + # -- Database port + port: 5432 + # -- Database user (default: superset, resolved via superset.db.user) + user: ~ + # -- Database password (default: superset, resolved via superset.db.password) + # ⚠️ CHANGE THIS for production + password: ~ + # -- Database name (default: superset, resolved via superset.db.name) + name: ~ + # -- Database SSL configuration + ssl: + enabled: false + mode: require + +# -- Redis cache configuration for Superset +# Redis is optional but recommended for caching and Celery. +# If redis.enabled (chart dependency) is true, defaults point to the chart's Redis instance. +cache: + # -- Enable Redis-based features (cache, Celery). Set to false to disable Redis usage entirely. + enabled: true + # -- Full Redis cache URL (overrides host/port/user/pass if set) + cacheUrl: ~ + # -- Full Redis Celery URL (overrides host/port/user/pass if set) + celeryUrl: ~ + # -- Redis host (default: {{ .Release.Name }}-redis-headless) + host: ~ + # -- Redis port + port: 6379 + # -- Redis user (optional, for Redis ACL) + user: "" + # -- Redis password + password: ~ + # -- Redis database number for cache + cacheDb: 1 + # -- Redis database number for Celery + celeryDb: 0 + # -- Cache key prefix + keyPrefix: "superset_" + # -- Default cache timeout in seconds + defaultTimeout: 86400 + # -- Results backend key prefix + resultsBackendKeyPrefix: "superset_results" + # -- Async queries configuration + asyncQueries: + keyPrefix: "qc-" + timeout: 86400 + # -- Redis Sentinel configuration (optional) + sentinel: ~ + # -- Redis SSL configuration + ssl: + enabled: false + ssl_cert_reqs: required + certfile: ~ + keyfile: ~ + ca_certs: ~ + # -- Custom Redis driver (e.g. TLS/managed variants); overrides the redis proto in URLs when set. + # Ports the legacy supersetNode.connections.redis_driver escape hatch. + driver: "" + +# -- Superset configuration properties +# Set any configuration property from superset/config.py here +# See https://github.com/apache/superset/blob/master/superset/config.py for all available options +config: {} + # SECRET_KEY: "your-secret-key-here" + # ROW_LIMIT: 50000 + # DEBUG: false + +# -- Feature flags configuration +# See https://github.com/apache/superset/blob/master/RESOURCES/FEATURE_FLAGS.md +featureFlags: {} + # ALERT_REPORTS: true + # DASHBOARD_RBAC: true + # Superset node configuration supersetNode: replicas: @@ -270,28 +375,8 @@ supersetNode: - "/bin/sh" - "-c" - ". {{ .Values.configMountPath }}/superset_bootstrap.sh; exec /usr/bin/run-server.sh" - connections: - # -- Change in case of bringing your own redis and then also set redis.enabled:false - redis_host: "{{ .Release.Name }}-redis-headless" - redis_port: "6379" - redis_user: "" - # redis_password: superset - redis_cache_db: "1" - redis_celery_db: "0" - # Or SSL port is usually 6380 - # Update following for using Redis with SSL - redis_ssl: - enabled: false - ssl_cert_reqs: CERT_NONE - redis_driver: "" - # You need to change below configuration incase bringing own PostgresSQL instance and also set postgresql.enabled:false - # -- Database type for Superset metadata (Supported types: "postgresql", "mysql") - db_type: "postgresql" - db_host: "{{ .Release.Name }}-postgresql" - db_port: "5432" - db_user: superset - db_pass: superset - db_name: superset + # DEPRECATED: use database.* and cache.* instead (still honored if set). See UPGRADING.md + connections: {} env: {} # -- If true, forces deployment to reload on each upgrade forceReload: false @@ -870,8 +955,11 @@ init: requests: cpu: "250m" memory: "128Mi" - # -- A Superset init script - # @default -- a script to create admin user and initialize roles + # -- DEPRECATED: this field is no longer used by the chart. + # The init script is rendered entirely from the internal `superset.initScript` template + # (which runs `superset db upgrade`, `superset init`, admin creation, and examples). + # Any customization placed here is silently ignored. See UPGRADING.md. + # @default -- unused; kept for backwards-compatibility only initscript: |- #!/bin/sh set -eu