codeant-ai-for-open-source[bot] commented on code in PR #38597:
URL: https://github.com/apache/superset/pull/38597#discussion_r2923771521
##########
helm/superset/templates/_helpers.tpl:
##########
@@ -61,83 +75,569 @@ Create chart name and version as used by the chart label.
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 |
trimSuffix "-" -}}
{{- end -}}
+{{/*
+Common labels for all resources - follows Kubernetes recommended labels
+https://kubernetes.io/docs/concepts/overview/working-with-objects/common-labels/
+*/}}
+{{- define "superset.labels" -}}
+helm.sh/chart: {{ include "superset.chart" . }}
+{{ include "superset.selectorLabels" . }}
+{{- if .Chart.AppVersion }}
+app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
+{{- end }}
+app.kubernetes.io/managed-by: {{ .Release.Service }}
+app.kubernetes.io/part-of: superset
+{{- if .Values.extraLabels }}
+{{ toYaml .Values.extraLabels }}
+{{- end }}
+{{- end -}}
+
+{{/*
+Selector labels - used by selectors and matchLabels
+*/}}
+{{- define "superset.selectorLabels" -}}
+app.kubernetes.io/name: {{ include "superset.name" . }}
+app.kubernetes.io/instance: {{ .Release.Name }}
+{{- end -}}
+
+{{/*
+Component labels - extends superset.labels with component-specific labels
+Usage: {{ include "superset.componentLabels" (dict "component" "web" "root" .)
}}
+*/}}
+{{- define "superset.componentLabels" -}}
+{{ include "superset.labels" .root }}
+app.kubernetes.io/component: {{ .component }}
+{{- end -}}
+
+{{/*
+Component selector labels - for matchLabels with component
+Usage: {{ include "superset.componentSelectorLabels" (dict "component" "web"
"root" .) }}
+*/}}
+{{- define "superset.componentSelectorLabels" -}}
+app.kubernetes.io/name: {{ include "superset.name" .root }}
+app.kubernetes.io/instance: {{ .Release.Name }}
+app.kubernetes.io/component: {{ .component }}
+{{- end -}}
+
+
+{{- define "superset.config" }}
+{{- /* Check for deprecated configuration values */}}
+{{- include "superset.checkDeprecatedValues" . }}
+{{- /* SECURITY: Validate admin password is set if admin creation is enabled
*/}}
+{{- /* Note: JWT secret validation is in deployment-ws.yaml since websocket
config is in a separate secret */}}
+{{- 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 }}
+{{- /* PRODUCTION: Validate resource limits are set for production deployments
*/}}
+{{- if and (not .Values.resources.limits) (not .Values.resources.requests) }}
+{{- /* Note: This is a warning - pre-install validation job will also check
this */}}
+{{- /* Resource limits are critical for production to prevent resource
exhaustion */}}
+{{- end }}
-{{- define "superset-config" }}
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_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 }}
-REDIS_BASE_URL=f"{env('REDIS_PROTO')}://{env('REDIS_HOST')}:{env('REDIS_PORT')}"
+{{- /* Determine database host - use explicit host, or default to service name
*/}}
+{{- $dbHost := .Values.database.host }}
+{{- if not $dbHost }}
+{{- if .Values.cluster.databaseServiceName }}
+{{- $dbHost = .Values.cluster.databaseServiceName }}
+{{- else }}
+{{- $dbHost = printf "%s-postgresql" .Release.Name }}
+{{- end }}
+{{- end }}
+{{- $driver := .Values.database.driver | default "postgresql+psycopg2" }}
+{{- $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 }}://{{ .Values.database.user }}:{{
.Values.database.password }}@{{ $dbHost }}:{{ .Values.database.port }}/{{
.Values.database.name }}{{ $sslParams }}"
+{{- end }}
+{{- if hasKey .Values.config "SQLALCHEMY_TRACK_MODIFICATIONS" }}
+SQLALCHEMY_TRACK_MODIFICATIONS = {{
.Values.config.SQLALCHEMY_TRACK_MODIFICATIONS | lower }}
Review Comment:
**Suggestion:** Rendering booleans with `| lower` produces Python literals
`true`/`false`, which are invalid in Python config files and will crash
Superset config loading when this option is set. Render explicit `True`/`False`
instead. [type error]
<details>
<summary><b>Severity Level:</b> Critical 🚨</summary>
```mdx
- ❌ Web and Celery pods fail loading generated config.
- ⚠️ Helm value override for this setting becomes unsafe.
```
</details>
```suggestion
SQLALCHEMY_TRACK_MODIFICATIONS = {{ if
.Values.config.SQLALCHEMY_TRACK_MODIFICATIONS }}True{{ else }}False{{ end }}
```
<details>
<summary><b>Steps of Reproduction ✅ </b></summary>
```mdx
1. Set `config.SQLALCHEMY_TRACK_MODIFICATIONS: true` in Helm values; this
key is
explicitly consumed at `helm/superset/templates/_helpers.tpl:167`.
2. Run `helm install` so `secret-superset-config.yaml` renders
`superset_config.py` via
`include "superset.config"` at
`helm/superset/templates/secret-superset-config.yaml:41`.
3. Deployments mount that generated config Secret at `/app/pythonpath`
(`deployment.yaml:143-146`, `deployment-worker.yaml:141-144`,
`deployment-beat.yaml:130-133`, `deployment-flower.yaml:118-121`).
4. On startup commands (`values.yaml:632-636`, `777-780`, `893-897`,
`983-987`),
Superset/Celery import config and hit `SQLALCHEMY_TRACK_MODIFICATIONS =
true`, which is
invalid Python identifier usage (`true` instead of `True`), causing startup
failure.
```
</details>
<details>
<summary><b>Prompt for AI Agent 🤖 </b></summary>
```mdx
This is a comment left during a code review.
**Path:** helm/superset/templates/_helpers.tpl
**Line:** 167:167
**Comment:**
*Type Error: Rendering booleans with `| lower` produces Python literals
`true`/`false`, which are invalid in Python config files and will crash
Superset config loading when this option is set. Render explicit `True`/`False`
instead.
Validate the correctness of the flagged issue. If correct, How can I resolve
this? If you propose a fix, implement it and please make it concise.
```
</details>
<a
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F38597&comment_hash=02fc8c8ce79e0e557e473df0413fdaf23d2b6e0a2cf2c21e126bef27b02bef82&reaction=like'>👍</a>
| <a
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F38597&comment_hash=02fc8c8ce79e0e557e473df0413fdaf23d2b6e0a2cf2c21e126bef27b02bef82&reaction=dislike'>👎</a>
##########
helm/superset/templates/_helpers.tpl:
##########
@@ -61,83 +75,569 @@ Create chart name and version as used by the chart label.
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 |
trimSuffix "-" -}}
{{- end -}}
+{{/*
+Common labels for all resources - follows Kubernetes recommended labels
+https://kubernetes.io/docs/concepts/overview/working-with-objects/common-labels/
+*/}}
+{{- define "superset.labels" -}}
+helm.sh/chart: {{ include "superset.chart" . }}
+{{ include "superset.selectorLabels" . }}
+{{- if .Chart.AppVersion }}
+app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
+{{- end }}
+app.kubernetes.io/managed-by: {{ .Release.Service }}
+app.kubernetes.io/part-of: superset
+{{- if .Values.extraLabels }}
+{{ toYaml .Values.extraLabels }}
+{{- end }}
+{{- end -}}
+
+{{/*
+Selector labels - used by selectors and matchLabels
+*/}}
+{{- define "superset.selectorLabels" -}}
+app.kubernetes.io/name: {{ include "superset.name" . }}
+app.kubernetes.io/instance: {{ .Release.Name }}
+{{- end -}}
+
+{{/*
+Component labels - extends superset.labels with component-specific labels
+Usage: {{ include "superset.componentLabels" (dict "component" "web" "root" .)
}}
+*/}}
+{{- define "superset.componentLabels" -}}
+{{ include "superset.labels" .root }}
+app.kubernetes.io/component: {{ .component }}
+{{- end -}}
+
+{{/*
+Component selector labels - for matchLabels with component
+Usage: {{ include "superset.componentSelectorLabels" (dict "component" "web"
"root" .) }}
+*/}}
+{{- define "superset.componentSelectorLabels" -}}
+app.kubernetes.io/name: {{ include "superset.name" .root }}
+app.kubernetes.io/instance: {{ .Release.Name }}
+app.kubernetes.io/component: {{ .component }}
+{{- end -}}
+
+
+{{- define "superset.config" }}
+{{- /* Check for deprecated configuration values */}}
+{{- include "superset.checkDeprecatedValues" . }}
+{{- /* SECURITY: Validate admin password is set if admin creation is enabled
*/}}
+{{- /* Note: JWT secret validation is in deployment-ws.yaml since websocket
config is in a separate secret */}}
+{{- 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 }}
+{{- /* PRODUCTION: Validate resource limits are set for production deployments
*/}}
+{{- if and (not .Values.resources.limits) (not .Values.resources.requests) }}
+{{- /* Note: This is a warning - pre-install validation job will also check
this */}}
+{{- /* Resource limits are critical for production to prevent resource
exhaustion */}}
+{{- end }}
-{{- define "superset-config" }}
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_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 }}
-REDIS_BASE_URL=f"{env('REDIS_PROTO')}://{env('REDIS_HOST')}:{env('REDIS_PORT')}"
+{{- /* Determine database host - use explicit host, or default to service name
*/}}
+{{- $dbHost := .Values.database.host }}
+{{- if not $dbHost }}
+{{- if .Values.cluster.databaseServiceName }}
+{{- $dbHost = .Values.cluster.databaseServiceName }}
+{{- else }}
+{{- $dbHost = printf "%s-postgresql" .Release.Name }}
+{{- end }}
+{{- end }}
+{{- $driver := .Values.database.driver | default "postgresql+psycopg2" }}
+{{- $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 }}://{{ .Values.database.user }}:{{
.Values.database.password }}@{{ $dbHost }}:{{ .Values.database.port }}/{{
.Values.database.name }}{{ $sslParams }}"
+{{- end }}
+{{- if hasKey .Values.config "SQLALCHEMY_TRACK_MODIFICATIONS" }}
+SQLALCHEMY_TRACK_MODIFICATIONS = {{
.Values.config.SQLALCHEMY_TRACK_MODIFICATIONS | lower }}
+{{- else }}
+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 }}
+{{- /* Automatically use rediss (SSL) protocol when SSL is enabled, otherwise
use redis */}}
+{{- /* Determine Redis host - use explicit host, or default to service name
*/}}
+{{- $redisHost := .Values.cache.host }}
+{{- if not $redisHost }}
+{{- if .Values.cluster.redisServiceName }}
+{{- $redisHost = .Values.cluster.redisServiceName }}
+{{- else }}
+{{- $redisHost = printf "%s-redis-headless" .Release.Name }}
+{{- end }}
+{{- end }}
+{{- $redisUser := .Values.cache.user | default "" }}
+{{- $redisPort := .Values.cache.port }}
+{{- $redisPassword := .Values.cache.password }}
+{{- $useSSL := and (hasKey .Values.cache "ssl") .Values.cache.ssl.enabled }}
+{{- if $redisPassword }}
+{{- if $redisUser }}
+REDIS_BASE_URL = f"{{ if $useSSL }}rediss{{ else }}redis{{ end }}://{{
$redisUser }}:{{ $redisPassword }}@{{ $redisHost }}:{{ $redisPort }}"
+{{- else }}
+REDIS_BASE_URL = f"{{ if $useSSL }}rediss{{ else }}redis{{ end }}://:{{
$redisPassword }}@{{ $redisHost }}:{{ $redisPort }}"
+{{- end }}
+{{- else }}
+REDIS_BASE_URL = f"{{ if $useSSL }}rediss{{ else }}redis{{ end }}://{{
$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 := .Values.cache.cacheDb | default 1 }}
+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 := .Values.cache.celeryDb | default 0 }}
+CELERY_REDIS_URL = f"{REDIS_BASE_URL}/{{ $celeryDb }}{REDIS_URL_PARAMS}"
+{{- else }}
+{{- /* SECURITY: If cacheUrl is set but celeryUrl is not, Celery will fail.
Validate this. */}}
+{{- 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) */}}
+{{- /* This MUST be set before Celery config imports async_queries, as it
accesses current_app.config at module level */}}
+{{- 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 }}
-SQLALCHEMY_TRACK_MODIFICATIONS = True
+CELERY_IMPORTS = CeleryConfig.imports
+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",
+ # NOTE: async_queries is temporarily excluded due to a bug where it
accesses current_app.config
+ # at module import time without an app context. This causes
worker/beat/flower to crash.
+ # TODO: Re-enable when Superset fixes the issue or provides a
workaround
+ # "superset.tasks.async_queries", # REQUIRED for GAQ
+ )
+ 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
*/}}
+{{- /* See:
https://medium.com/ambient-innovation/health-checks-for-celery-in-kubernetes-cf3274a3e106
*/}}
+{{- /* NOTE: These signals only fire for Celery workers, not beat or flower
*/}}
+{{- if and .Values.supersetWorker.healthCheck
.Values.supersetWorker.healthCheck.enabled }}
+# Celery Worker Health Check Configuration
+# File paths are injected at deploy time from values.yaml
+# NOTE: worker_ready/worker_shutdown signals only fire for workers, not beat
+import threading
+from celery import bootsteps
+from celery.signals import worker_ready, worker_shutdown, worker_init
+
+# File paths for health check probes (from values.yaml)
+_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
+
+# Readiness Probe: Create/remove file based on worker state
+# These signals only fire for workers, safe to register globally
+@worker_ready.connect
+def create_ready_file(sender, **kwargs):
+ """Create readiness file when Celery worker is ready to process tasks"""
+ try:
+ open(_readiness_file, 'w').close()
+ print(f"Celery worker ready - created {_readiness_file}")
+ except Exception as e:
+ print(f"Warning: Could not create readiness file: {e}")
+
+@worker_shutdown.connect
+def remove_ready_file(sender, **kwargs):
+ """Remove readiness file when Celery worker is shutting down"""
+ global _liveness_thread, _liveness_stop_event
+ # Stop the liveness heartbeat thread
+ if _liveness_stop_event:
+ _liveness_stop_event.set()
+ if _liveness_thread:
+ _liveness_thread.join(timeout=5)
+ # Remove health check files
+ try:
+ if os.path.exists(_readiness_file):
+ os.remove(_readiness_file)
+ print(f"Celery worker shutdown - removed {_readiness_file}")
+ if os.path.exists(_liveness_file):
+ os.remove(_liveness_file)
+ print(f"Celery worker shutdown - removed {_liveness_file}")
+ except Exception as e:
+ print(f"Warning: Could not remove health check files: {e}")
+
+# Liveness Probe: Start heartbeat thread when worker initializes
+# worker_init only fires for workers, not beat
+@worker_init.connect
+def start_liveness_heartbeat(sender, **kwargs):
+ """Start the liveness heartbeat thread when worker initializes"""
+ 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()
+ print(f"Celery liveness heartbeat started - updating {_liveness_file}
every {_heartbeat_interval}s")
+{{- else }}
+CELERY_WORKER_HEALTH_CHECK_ENABLED = False
+{{- end }}
+
+{{- /* Results Backend */}}
+{{- if .Values.config.resultsBackend }}
+{{- if kindIs "string" .Values.config.resultsBackend }}
+RESULTS_BACKEND = {{ .Values.config.resultsBackend }}
+{{- else }}
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={{ .Values.cache.host | quote }},
+ {{- if .Values.cache.password }}
+ password={{ .Values.cache.password | quote }},
+ {{- end }}
+ port={{ .Values.cache.port | 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={{ .Values.cache.host | quote }},
+ {{- if .Values.cache.password }}
+ password={{ .Values.cache.password | quote }},
+ {{- end }}
+ port={{ .Values.cache.port | 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 - Required when using
GLOBAL_ASYNC_QUERIES feature flag */}}
+{{- 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": {{ .Values.cache.host | quote }},
+ "CACHE_REDIS_PORT": {{ .Values.cache.port | int }},
+ "CACHE_REDIS_USER": {{ .Values.cache.user | default "" | quote }},
+ {{- if .Values.cache.password }}
+ "CACHE_REDIS_PASSWORD": {{ .Values.cache.password | 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 .Values.cache.sentinel }}
+ {{- 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": {{ .Values.cache.ssl.certfile | default "None"
}},
+ "CACHE_REDIS_SSL_KEYFILE": {{ .Values.cache.ssl.keyfile | default "None"
}},
+ "CACHE_REDIS_SSL_CERT_REQS": {{ .Values.cache.ssl.ssl_cert_reqs | default
"required" | quote }},
+ "CACHE_REDIS_SSL_CA_CERTS": {{ .Values.cache.ssl.ca_certs | default "None"
}},
+ {{- 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 - Required when using
GLOBAL_ASYNC_QUERIES feature flag */}}
+{{- 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": {{ .Values.cache.host | quote }},
+ "port": {{ .Values.cache.port | int }},
+ "prefix": {{ .Values.cache.asyncQueries.keyPrefix | default "qc-" | quote
}},
+ "db": {{ .Values.cache.asyncQueries.db | default .Values.cache.cacheDb |
default 0 | int }},
+ {{- if .Values.cache.password }}
+ "password": {{ .Values.cache.password | 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 - Auto-configure if websockets are
enabled */}}
+{{- $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 }}
+{{- /* Default: Use service name - user should override with external URL
accessible from browser */}}
+{{- $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 - Required when using
GLOBAL_ASYNC_QUERIES feature flag */}}
+{{- /* Must match the JWT secret in the websocket server config.json */}}
+{{- 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 - Important for HTTPS/WSS */}}
+{{- /* SECURE: Must be True when using HTTPS/WSS, otherwise browser won't send
the cookie */}}
+{{- if .Values.config.GLOBAL_ASYNC_QUERIES_JWT_COOKIE_SECURE }}
Review Comment:
**Suggestion:** The condition checks truthiness instead of key presence, so
explicitly setting the secure-cookie option to `false` is ignored and
overridden by auto-detection logic. Use `hasKey` so explicit false values are
honored. [logic error]
<details>
<summary><b>Severity Level:</b> Major ⚠️</summary>
```mdx
- ⚠️ Explicit false override for JWT cookie secure ignored.
- ⚠️ Websocket GAQ cookie behavior may not match values.
```
</details>
```suggestion
{{- if hasKey .Values.config "GLOBAL_ASYNC_QUERIES_JWT_COOKIE_SECURE" }}
```
<details>
<summary><b>Steps of Reproduction ✅ </b></summary>
```mdx
1. Enable websockets and set `config.GLOBAL_ASYNC_QUERIES_JWT_COOKIE_SECURE:
false`
intentionally.
2. During render, `_helpers.tpl` checks truthiness at `542`; explicit
`false` skips the
explicit-value branch.
3. Control falls into auto-detection branch (`544-549`) which can force
`GLOBAL_ASYNC_QUERIES_JWT_COOKIE_SECURE = True` when TLS/wss indicators are
present.
4. Generated `superset_config.py` (via `secret-superset-config.yaml:41`) no
longer
reflects user intent, affecting websocket auth cookie behavior.
```
</details>
<details>
<summary><b>Prompt for AI Agent 🤖 </b></summary>
```mdx
This is a comment left during a code review.
**Path:** helm/superset/templates/_helpers.tpl
**Line:** 542:542
**Comment:**
*Logic Error: The condition checks truthiness instead of key presence,
so explicitly setting the secure-cookie option to `false` is ignored and
overridden by auto-detection logic. Use `hasKey` so explicit false values are
honored.
Validate the correctness of the flagged issue. If correct, How can I resolve
this? If you propose a fix, implement it and please make it concise.
```
</details>
<a
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F38597&comment_hash=bd157a7139b288611ac16ff3cf3c7672d73ab092f26fa824f6d3d83fd50cc7be&reaction=like'>👍</a>
| <a
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F38597&comment_hash=bd157a7139b288611ac16ff3cf3c7672d73ab092f26fa824f6d3d83fd50cc7be&reaction=dislike'>👎</a>
##########
helm/superset/templates/_helpers.tpl:
##########
@@ -61,83 +75,569 @@ Create chart name and version as used by the chart label.
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 |
trimSuffix "-" -}}
{{- end -}}
+{{/*
+Common labels for all resources - follows Kubernetes recommended labels
+https://kubernetes.io/docs/concepts/overview/working-with-objects/common-labels/
+*/}}
+{{- define "superset.labels" -}}
+helm.sh/chart: {{ include "superset.chart" . }}
+{{ include "superset.selectorLabels" . }}
+{{- if .Chart.AppVersion }}
+app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
+{{- end }}
+app.kubernetes.io/managed-by: {{ .Release.Service }}
+app.kubernetes.io/part-of: superset
+{{- if .Values.extraLabels }}
+{{ toYaml .Values.extraLabels }}
+{{- end }}
+{{- end -}}
+
+{{/*
+Selector labels - used by selectors and matchLabels
+*/}}
+{{- define "superset.selectorLabels" -}}
+app.kubernetes.io/name: {{ include "superset.name" . }}
+app.kubernetes.io/instance: {{ .Release.Name }}
+{{- end -}}
+
+{{/*
+Component labels - extends superset.labels with component-specific labels
+Usage: {{ include "superset.componentLabels" (dict "component" "web" "root" .)
}}
+*/}}
+{{- define "superset.componentLabels" -}}
+{{ include "superset.labels" .root }}
+app.kubernetes.io/component: {{ .component }}
+{{- end -}}
+
+{{/*
+Component selector labels - for matchLabels with component
+Usage: {{ include "superset.componentSelectorLabels" (dict "component" "web"
"root" .) }}
+*/}}
+{{- define "superset.componentSelectorLabels" -}}
+app.kubernetes.io/name: {{ include "superset.name" .root }}
+app.kubernetes.io/instance: {{ .Release.Name }}
+app.kubernetes.io/component: {{ .component }}
+{{- end -}}
+
+
+{{- define "superset.config" }}
+{{- /* Check for deprecated configuration values */}}
+{{- include "superset.checkDeprecatedValues" . }}
+{{- /* SECURITY: Validate admin password is set if admin creation is enabled
*/}}
+{{- /* Note: JWT secret validation is in deployment-ws.yaml since websocket
config is in a separate secret */}}
+{{- 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 }}
+{{- /* PRODUCTION: Validate resource limits are set for production deployments
*/}}
+{{- if and (not .Values.resources.limits) (not .Values.resources.requests) }}
+{{- /* Note: This is a warning - pre-install validation job will also check
this */}}
+{{- /* Resource limits are critical for production to prevent resource
exhaustion */}}
+{{- end }}
-{{- define "superset-config" }}
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_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 }}
-REDIS_BASE_URL=f"{env('REDIS_PROTO')}://{env('REDIS_HOST')}:{env('REDIS_PORT')}"
+{{- /* Determine database host - use explicit host, or default to service name
*/}}
+{{- $dbHost := .Values.database.host }}
+{{- if not $dbHost }}
+{{- if .Values.cluster.databaseServiceName }}
+{{- $dbHost = .Values.cluster.databaseServiceName }}
+{{- else }}
+{{- $dbHost = printf "%s-postgresql" .Release.Name }}
+{{- end }}
+{{- end }}
+{{- $driver := .Values.database.driver | default "postgresql+psycopg2" }}
+{{- $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 }}://{{ .Values.database.user }}:{{
.Values.database.password }}@{{ $dbHost }}:{{ .Values.database.port }}/{{
.Values.database.name }}{{ $sslParams }}"
+{{- end }}
+{{- if hasKey .Values.config "SQLALCHEMY_TRACK_MODIFICATIONS" }}
+SQLALCHEMY_TRACK_MODIFICATIONS = {{
.Values.config.SQLALCHEMY_TRACK_MODIFICATIONS | lower }}
+{{- else }}
+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 }}
+{{- /* Automatically use rediss (SSL) protocol when SSL is enabled, otherwise
use redis */}}
+{{- /* Determine Redis host - use explicit host, or default to service name
*/}}
+{{- $redisHost := .Values.cache.host }}
+{{- if not $redisHost }}
+{{- if .Values.cluster.redisServiceName }}
+{{- $redisHost = .Values.cluster.redisServiceName }}
+{{- else }}
+{{- $redisHost = printf "%s-redis-headless" .Release.Name }}
+{{- end }}
+{{- end }}
+{{- $redisUser := .Values.cache.user | default "" }}
+{{- $redisPort := .Values.cache.port }}
+{{- $redisPassword := .Values.cache.password }}
+{{- $useSSL := and (hasKey .Values.cache "ssl") .Values.cache.ssl.enabled }}
+{{- if $redisPassword }}
+{{- if $redisUser }}
+REDIS_BASE_URL = f"{{ if $useSSL }}rediss{{ else }}redis{{ end }}://{{
$redisUser }}:{{ $redisPassword }}@{{ $redisHost }}:{{ $redisPort }}"
+{{- else }}
+REDIS_BASE_URL = f"{{ if $useSSL }}rediss{{ else }}redis{{ end }}://:{{
$redisPassword }}@{{ $redisHost }}:{{ $redisPort }}"
+{{- end }}
+{{- else }}
+REDIS_BASE_URL = f"{{ if $useSSL }}rediss{{ else }}redis{{ end }}://{{
$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 := .Values.cache.cacheDb | default 1 }}
+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 := .Values.cache.celeryDb | default 0 }}
+CELERY_REDIS_URL = f"{REDIS_BASE_URL}/{{ $celeryDb }}{REDIS_URL_PARAMS}"
+{{- else }}
+{{- /* SECURITY: If cacheUrl is set but celeryUrl is not, Celery will fail.
Validate this. */}}
+{{- 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) */}}
+{{- /* This MUST be set before Celery config imports async_queries, as it
accesses current_app.config at module level */}}
+{{- 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 }}
-SQLALCHEMY_TRACK_MODIFICATIONS = True
+CELERY_IMPORTS = CeleryConfig.imports
Review Comment:
**Suggestion:** Custom Celery config dictionaries are not guaranteed to
define an `imports` attribute, so directly accessing it raises an attribute
error at runtime; use a safe fallback when missing. [possible bug]
<details>
<summary><b>Severity Level:</b> Critical 🚨</summary>
```mdx
- ❌ Custom Celery dict config can crash app bootstrap.
- ⚠️ Worker/beat/flower and web share same broken config.
```
</details>
```suggestion
CELERY_IMPORTS = getattr(CeleryConfig, "imports", ())
```
<details>
<summary><b>Steps of Reproduction ✅ </b></summary>
```mdx
1. Provide custom dict-style `config.celeryConfig` (documented at
`helm/superset/values.yaml:126-128`) without an `imports` key.
2. Render/install chart; `_helpers.tpl` builds `class CeleryConfig` then
immediately
evaluates `CELERY_IMPORTS = CeleryConfig.imports` at
`helm/superset/templates/_helpers.tpl:263`.
3. `superset_config.py` is generated through
`secret-superset-config.yaml:41` and mounted
into all runtime pods (`deployment*.yaml` mount paths under
`/app/pythonpath`).
4. When process starts, module-level access to missing
`CeleryConfig.imports` raises
`AttributeError`, aborting config load before app init
(`superset/initialization/__init__.py:127` expects valid `CELERY_CONFIG`).
```
</details>
<details>
<summary><b>Prompt for AI Agent 🤖 </b></summary>
```mdx
This is a comment left during a code review.
**Path:** helm/superset/templates/_helpers.tpl
**Line:** 263:263
**Comment:**
*Possible Bug: Custom Celery config dictionaries are not guaranteed to
define an `imports` attribute, so directly accessing it raises an attribute
error at runtime; use a safe fallback when missing.
Validate the correctness of the flagged issue. If correct, How can I resolve
this? If you propose a fix, implement it and please make it concise.
```
</details>
<a
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F38597&comment_hash=9ebc4bde251c76bd1444dc620178bc18ac48b26dd048030ca65b50aa9762c5cc&reaction=like'>👍</a>
| <a
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F38597&comment_hash=9ebc4bde251c76bd1444dc620178bc18ac48b26dd048030ca65b50aa9762c5cc&reaction=dislike'>👎</a>
##########
helm/superset/templates/_helpers.tpl:
##########
@@ -61,83 +75,569 @@ Create chart name and version as used by the chart label.
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 |
trimSuffix "-" -}}
{{- end -}}
+{{/*
+Common labels for all resources - follows Kubernetes recommended labels
+https://kubernetes.io/docs/concepts/overview/working-with-objects/common-labels/
+*/}}
+{{- define "superset.labels" -}}
+helm.sh/chart: {{ include "superset.chart" . }}
+{{ include "superset.selectorLabels" . }}
+{{- if .Chart.AppVersion }}
+app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
+{{- end }}
+app.kubernetes.io/managed-by: {{ .Release.Service }}
+app.kubernetes.io/part-of: superset
+{{- if .Values.extraLabels }}
+{{ toYaml .Values.extraLabels }}
+{{- end }}
+{{- end -}}
+
+{{/*
+Selector labels - used by selectors and matchLabels
+*/}}
+{{- define "superset.selectorLabels" -}}
+app.kubernetes.io/name: {{ include "superset.name" . }}
+app.kubernetes.io/instance: {{ .Release.Name }}
+{{- end -}}
+
+{{/*
+Component labels - extends superset.labels with component-specific labels
+Usage: {{ include "superset.componentLabels" (dict "component" "web" "root" .)
}}
+*/}}
+{{- define "superset.componentLabels" -}}
+{{ include "superset.labels" .root }}
+app.kubernetes.io/component: {{ .component }}
+{{- end -}}
+
+{{/*
+Component selector labels - for matchLabels with component
+Usage: {{ include "superset.componentSelectorLabels" (dict "component" "web"
"root" .) }}
+*/}}
+{{- define "superset.componentSelectorLabels" -}}
+app.kubernetes.io/name: {{ include "superset.name" .root }}
+app.kubernetes.io/instance: {{ .Release.Name }}
+app.kubernetes.io/component: {{ .component }}
+{{- end -}}
+
+
+{{- define "superset.config" }}
+{{- /* Check for deprecated configuration values */}}
+{{- include "superset.checkDeprecatedValues" . }}
+{{- /* SECURITY: Validate admin password is set if admin creation is enabled
*/}}
+{{- /* Note: JWT secret validation is in deployment-ws.yaml since websocket
config is in a separate secret */}}
+{{- 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 }}
+{{- /* PRODUCTION: Validate resource limits are set for production deployments
*/}}
+{{- if and (not .Values.resources.limits) (not .Values.resources.requests) }}
+{{- /* Note: This is a warning - pre-install validation job will also check
this */}}
+{{- /* Resource limits are critical for production to prevent resource
exhaustion */}}
+{{- end }}
-{{- define "superset-config" }}
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_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 }}
-REDIS_BASE_URL=f"{env('REDIS_PROTO')}://{env('REDIS_HOST')}:{env('REDIS_PORT')}"
+{{- /* Determine database host - use explicit host, or default to service name
*/}}
+{{- $dbHost := .Values.database.host }}
+{{- if not $dbHost }}
+{{- if .Values.cluster.databaseServiceName }}
+{{- $dbHost = .Values.cluster.databaseServiceName }}
+{{- else }}
+{{- $dbHost = printf "%s-postgresql" .Release.Name }}
+{{- end }}
+{{- end }}
+{{- $driver := .Values.database.driver | default "postgresql+psycopg2" }}
+{{- $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 }}://{{ .Values.database.user }}:{{
.Values.database.password }}@{{ $dbHost }}:{{ .Values.database.port }}/{{
.Values.database.name }}{{ $sslParams }}"
+{{- end }}
+{{- if hasKey .Values.config "SQLALCHEMY_TRACK_MODIFICATIONS" }}
+SQLALCHEMY_TRACK_MODIFICATIONS = {{
.Values.config.SQLALCHEMY_TRACK_MODIFICATIONS | lower }}
+{{- else }}
+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 }}
+{{- /* Automatically use rediss (SSL) protocol when SSL is enabled, otherwise
use redis */}}
+{{- /* Determine Redis host - use explicit host, or default to service name
*/}}
+{{- $redisHost := .Values.cache.host }}
+{{- if not $redisHost }}
+{{- if .Values.cluster.redisServiceName }}
+{{- $redisHost = .Values.cluster.redisServiceName }}
+{{- else }}
+{{- $redisHost = printf "%s-redis-headless" .Release.Name }}
+{{- end }}
+{{- end }}
+{{- $redisUser := .Values.cache.user | default "" }}
+{{- $redisPort := .Values.cache.port }}
+{{- $redisPassword := .Values.cache.password }}
+{{- $useSSL := and (hasKey .Values.cache "ssl") .Values.cache.ssl.enabled }}
+{{- if $redisPassword }}
+{{- if $redisUser }}
+REDIS_BASE_URL = f"{{ if $useSSL }}rediss{{ else }}redis{{ end }}://{{
$redisUser }}:{{ $redisPassword }}@{{ $redisHost }}:{{ $redisPort }}"
+{{- else }}
+REDIS_BASE_URL = f"{{ if $useSSL }}rediss{{ else }}redis{{ end }}://:{{
$redisPassword }}@{{ $redisHost }}:{{ $redisPort }}"
+{{- end }}
+{{- else }}
+REDIS_BASE_URL = f"{{ if $useSSL }}rediss{{ else }}redis{{ end }}://{{
$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 := .Values.cache.cacheDb | default 1 }}
+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 := .Values.cache.celeryDb | default 0 }}
+CELERY_REDIS_URL = f"{REDIS_BASE_URL}/{{ $celeryDb }}{REDIS_URL_PARAMS}"
+{{- else }}
+{{- /* SECURITY: If cacheUrl is set but celeryUrl is not, Celery will fail.
Validate this. */}}
+{{- 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) */}}
+{{- /* This MUST be set before Celery config imports async_queries, as it
accesses current_app.config at module level */}}
+{{- 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 }}
-SQLALCHEMY_TRACK_MODIFICATIONS = True
+CELERY_IMPORTS = CeleryConfig.imports
+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",
+ # NOTE: async_queries is temporarily excluded due to a bug where it
accesses current_app.config
+ # at module import time without an app context. This causes
worker/beat/flower to crash.
+ # TODO: Re-enable when Superset fixes the issue or provides a
workaround
+ # "superset.tasks.async_queries", # REQUIRED for GAQ
+ )
+ 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
*/}}
+{{- /* See:
https://medium.com/ambient-innovation/health-checks-for-celery-in-kubernetes-cf3274a3e106
*/}}
+{{- /* NOTE: These signals only fire for Celery workers, not beat or flower
*/}}
+{{- if and .Values.supersetWorker.healthCheck
.Values.supersetWorker.healthCheck.enabled }}
+# Celery Worker Health Check Configuration
+# File paths are injected at deploy time from values.yaml
+# NOTE: worker_ready/worker_shutdown signals only fire for workers, not beat
+import threading
+from celery import bootsteps
+from celery.signals import worker_ready, worker_shutdown, worker_init
+
+# File paths for health check probes (from values.yaml)
+_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
+
+# Readiness Probe: Create/remove file based on worker state
+# These signals only fire for workers, safe to register globally
+@worker_ready.connect
+def create_ready_file(sender, **kwargs):
+ """Create readiness file when Celery worker is ready to process tasks"""
+ try:
+ open(_readiness_file, 'w').close()
+ print(f"Celery worker ready - created {_readiness_file}")
+ except Exception as e:
+ print(f"Warning: Could not create readiness file: {e}")
+
+@worker_shutdown.connect
+def remove_ready_file(sender, **kwargs):
+ """Remove readiness file when Celery worker is shutting down"""
+ global _liveness_thread, _liveness_stop_event
+ # Stop the liveness heartbeat thread
+ if _liveness_stop_event:
+ _liveness_stop_event.set()
+ if _liveness_thread:
+ _liveness_thread.join(timeout=5)
+ # Remove health check files
+ try:
+ if os.path.exists(_readiness_file):
+ os.remove(_readiness_file)
+ print(f"Celery worker shutdown - removed {_readiness_file}")
+ if os.path.exists(_liveness_file):
+ os.remove(_liveness_file)
+ print(f"Celery worker shutdown - removed {_liveness_file}")
+ except Exception as e:
+ print(f"Warning: Could not remove health check files: {e}")
+
+# Liveness Probe: Start heartbeat thread when worker initializes
+# worker_init only fires for workers, not beat
+@worker_init.connect
+def start_liveness_heartbeat(sender, **kwargs):
+ """Start the liveness heartbeat thread when worker initializes"""
+ 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()
+ print(f"Celery liveness heartbeat started - updating {_liveness_file}
every {_heartbeat_interval}s")
+{{- else }}
+CELERY_WORKER_HEALTH_CHECK_ENABLED = False
+{{- end }}
+
+{{- /* Results Backend */}}
+{{- if .Values.config.resultsBackend }}
+{{- if kindIs "string" .Values.config.resultsBackend }}
+RESULTS_BACKEND = {{ .Values.config.resultsBackend }}
+{{- else }}
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={{ .Values.cache.host | quote }},
+ {{- if .Values.cache.password }}
+ password={{ .Values.cache.password | quote }},
+ {{- end }}
+ port={{ .Values.cache.port | 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={{ .Values.cache.host | quote }},
+ {{- if .Values.cache.password }}
+ password={{ .Values.cache.password | quote }},
+ {{- end }}
+ port={{ .Values.cache.port | 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 - Required when using
GLOBAL_ASYNC_QUERIES feature flag */}}
+{{- 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": {{ .Values.cache.host | quote }},
+ "CACHE_REDIS_PORT": {{ .Values.cache.port | int }},
+ "CACHE_REDIS_USER": {{ .Values.cache.user | default "" | quote }},
+ {{- if .Values.cache.password }}
+ "CACHE_REDIS_PASSWORD": {{ .Values.cache.password | 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 .Values.cache.sentinel }}
Review Comment:
**Suggestion:** Sentinel configuration is enabled based on map presence
instead of the documented `enabled` flag, so users can accidentally trigger
Sentinel validation/errors even when `enabled: false`; gate this block on the
flag. [logic error]
<details>
<summary><b>Severity Level:</b> Major ⚠️</summary>
```mdx
- ❌ Helm rendering can fail despite sentinel disabled flag.
- ⚠️ Sentinel users hit confusing false-positive validation errors.
```
</details>
```suggestion
{{- if and .Values.cache.sentinel .Values.cache.sentinel.enabled }}
```
<details>
<summary><b>Steps of Reproduction ✅ </b></summary>
```mdx
1. Define a sentinel map in values with `enabled: false` (pattern documented
under
`cache.sentinel` comments at `helm/superset/values.yaml:77-82`).
2. Run `helm install`/`helm template`; `_helpers.tpl` enters sentinel block
because map
presence is truthy at line `430`.
3. Block immediately validates `cache.sentinel.sentinels` and can call
`fail` at
`_helpers.tpl:434-435`, even though sentinel is explicitly disabled.
4. Rendering fails through `include "superset.config"` path
(`secret-superset-config.yaml:41`), blocking release creation/upgrade.
```
</details>
<details>
<summary><b>Prompt for AI Agent 🤖 </b></summary>
```mdx
This is a comment left during a code review.
**Path:** helm/superset/templates/_helpers.tpl
**Line:** 430:430
**Comment:**
*Logic Error: Sentinel configuration is enabled based on map presence
instead of the documented `enabled` flag, so users can accidentally trigger
Sentinel validation/errors even when `enabled: false`; gate this block on the
flag.
Validate the correctness of the flagged issue. If correct, How can I resolve
this? If you propose a fix, implement it and please make it concise.
```
</details>
<a
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F38597&comment_hash=142ef1d5e1d37dae85c6aea8b687673301986b5350bd8b24fff5eb79f4b8dea1&reaction=like'>👍</a>
| <a
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F38597&comment_hash=142ef1d5e1d37dae85c6aea8b687673301986b5350bd8b24fff5eb79f4b8dea1&reaction=dislike'>👎</a>
##########
helm/superset/templates/_helpers.tpl:
##########
@@ -61,83 +75,569 @@ Create chart name and version as used by the chart label.
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 |
trimSuffix "-" -}}
{{- end -}}
+{{/*
+Common labels for all resources - follows Kubernetes recommended labels
+https://kubernetes.io/docs/concepts/overview/working-with-objects/common-labels/
+*/}}
+{{- define "superset.labels" -}}
+helm.sh/chart: {{ include "superset.chart" . }}
+{{ include "superset.selectorLabels" . }}
+{{- if .Chart.AppVersion }}
+app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
+{{- end }}
+app.kubernetes.io/managed-by: {{ .Release.Service }}
+app.kubernetes.io/part-of: superset
+{{- if .Values.extraLabels }}
+{{ toYaml .Values.extraLabels }}
+{{- end }}
+{{- end -}}
+
+{{/*
+Selector labels - used by selectors and matchLabels
+*/}}
+{{- define "superset.selectorLabels" -}}
+app.kubernetes.io/name: {{ include "superset.name" . }}
+app.kubernetes.io/instance: {{ .Release.Name }}
+{{- end -}}
+
+{{/*
+Component labels - extends superset.labels with component-specific labels
+Usage: {{ include "superset.componentLabels" (dict "component" "web" "root" .)
}}
+*/}}
+{{- define "superset.componentLabels" -}}
+{{ include "superset.labels" .root }}
+app.kubernetes.io/component: {{ .component }}
+{{- end -}}
+
+{{/*
+Component selector labels - for matchLabels with component
+Usage: {{ include "superset.componentSelectorLabels" (dict "component" "web"
"root" .) }}
+*/}}
+{{- define "superset.componentSelectorLabels" -}}
+app.kubernetes.io/name: {{ include "superset.name" .root }}
+app.kubernetes.io/instance: {{ .Release.Name }}
+app.kubernetes.io/component: {{ .component }}
+{{- end -}}
+
+
+{{- define "superset.config" }}
+{{- /* Check for deprecated configuration values */}}
+{{- include "superset.checkDeprecatedValues" . }}
+{{- /* SECURITY: Validate admin password is set if admin creation is enabled
*/}}
+{{- /* Note: JWT secret validation is in deployment-ws.yaml since websocket
config is in a separate secret */}}
+{{- 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 }}
+{{- /* PRODUCTION: Validate resource limits are set for production deployments
*/}}
+{{- if and (not .Values.resources.limits) (not .Values.resources.requests) }}
+{{- /* Note: This is a warning - pre-install validation job will also check
this */}}
+{{- /* Resource limits are critical for production to prevent resource
exhaustion */}}
+{{- end }}
-{{- define "superset-config" }}
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_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 }}
-REDIS_BASE_URL=f"{env('REDIS_PROTO')}://{env('REDIS_HOST')}:{env('REDIS_PORT')}"
+{{- /* Determine database host - use explicit host, or default to service name
*/}}
+{{- $dbHost := .Values.database.host }}
+{{- if not $dbHost }}
+{{- if .Values.cluster.databaseServiceName }}
+{{- $dbHost = .Values.cluster.databaseServiceName }}
+{{- else }}
+{{- $dbHost = printf "%s-postgresql" .Release.Name }}
+{{- end }}
+{{- end }}
+{{- $driver := .Values.database.driver | default "postgresql+psycopg2" }}
+{{- $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 }}://{{ .Values.database.user }}:{{
.Values.database.password }}@{{ $dbHost }}:{{ .Values.database.port }}/{{
.Values.database.name }}{{ $sslParams }}"
+{{- end }}
+{{- if hasKey .Values.config "SQLALCHEMY_TRACK_MODIFICATIONS" }}
+SQLALCHEMY_TRACK_MODIFICATIONS = {{
.Values.config.SQLALCHEMY_TRACK_MODIFICATIONS | lower }}
+{{- else }}
+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 }}
+{{- /* Automatically use rediss (SSL) protocol when SSL is enabled, otherwise
use redis */}}
+{{- /* Determine Redis host - use explicit host, or default to service name
*/}}
+{{- $redisHost := .Values.cache.host }}
+{{- if not $redisHost }}
+{{- if .Values.cluster.redisServiceName }}
+{{- $redisHost = .Values.cluster.redisServiceName }}
+{{- else }}
+{{- $redisHost = printf "%s-redis-headless" .Release.Name }}
+{{- end }}
+{{- end }}
+{{- $redisUser := .Values.cache.user | default "" }}
+{{- $redisPort := .Values.cache.port }}
+{{- $redisPassword := .Values.cache.password }}
+{{- $useSSL := and (hasKey .Values.cache "ssl") .Values.cache.ssl.enabled }}
+{{- if $redisPassword }}
+{{- if $redisUser }}
+REDIS_BASE_URL = f"{{ if $useSSL }}rediss{{ else }}redis{{ end }}://{{
$redisUser }}:{{ $redisPassword }}@{{ $redisHost }}:{{ $redisPort }}"
+{{- else }}
+REDIS_BASE_URL = f"{{ if $useSSL }}rediss{{ else }}redis{{ end }}://:{{
$redisPassword }}@{{ $redisHost }}:{{ $redisPort }}"
+{{- end }}
+{{- else }}
+REDIS_BASE_URL = f"{{ if $useSSL }}rediss{{ else }}redis{{ end }}://{{
$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 := .Values.cache.cacheDb | default 1 }}
+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 := .Values.cache.celeryDb | default 0 }}
+CELERY_REDIS_URL = f"{REDIS_BASE_URL}/{{ $celeryDb }}{REDIS_URL_PARAMS}"
+{{- else }}
+{{- /* SECURITY: If cacheUrl is set but celeryUrl is not, Celery will fail.
Validate this. */}}
+{{- 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) */}}
+{{- /* This MUST be set before Celery config imports async_queries, as it
accesses current_app.config at module level */}}
+{{- 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 }}
-SQLALCHEMY_TRACK_MODIFICATIONS = True
+CELERY_IMPORTS = CeleryConfig.imports
+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",
+ # NOTE: async_queries is temporarily excluded due to a bug where it
accesses current_app.config
+ # at module import time without an app context. This causes
worker/beat/flower to crash.
+ # TODO: Re-enable when Superset fixes the issue or provides a
workaround
+ # "superset.tasks.async_queries", # REQUIRED for GAQ
+ )
+ 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
*/}}
+{{- /* See:
https://medium.com/ambient-innovation/health-checks-for-celery-in-kubernetes-cf3274a3e106
*/}}
+{{- /* NOTE: These signals only fire for Celery workers, not beat or flower
*/}}
+{{- if and .Values.supersetWorker.healthCheck
.Values.supersetWorker.healthCheck.enabled }}
+# Celery Worker Health Check Configuration
+# File paths are injected at deploy time from values.yaml
+# NOTE: worker_ready/worker_shutdown signals only fire for workers, not beat
+import threading
+from celery import bootsteps
+from celery.signals import worker_ready, worker_shutdown, worker_init
+
+# File paths for health check probes (from values.yaml)
+_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
+
+# Readiness Probe: Create/remove file based on worker state
+# These signals only fire for workers, safe to register globally
+@worker_ready.connect
+def create_ready_file(sender, **kwargs):
+ """Create readiness file when Celery worker is ready to process tasks"""
+ try:
+ open(_readiness_file, 'w').close()
+ print(f"Celery worker ready - created {_readiness_file}")
+ except Exception as e:
+ print(f"Warning: Could not create readiness file: {e}")
+
+@worker_shutdown.connect
+def remove_ready_file(sender, **kwargs):
+ """Remove readiness file when Celery worker is shutting down"""
+ global _liveness_thread, _liveness_stop_event
+ # Stop the liveness heartbeat thread
+ if _liveness_stop_event:
+ _liveness_stop_event.set()
+ if _liveness_thread:
+ _liveness_thread.join(timeout=5)
+ # Remove health check files
+ try:
+ if os.path.exists(_readiness_file):
+ os.remove(_readiness_file)
+ print(f"Celery worker shutdown - removed {_readiness_file}")
+ if os.path.exists(_liveness_file):
+ os.remove(_liveness_file)
+ print(f"Celery worker shutdown - removed {_liveness_file}")
+ except Exception as e:
+ print(f"Warning: Could not remove health check files: {e}")
+
+# Liveness Probe: Start heartbeat thread when worker initializes
+# worker_init only fires for workers, not beat
+@worker_init.connect
+def start_liveness_heartbeat(sender, **kwargs):
+ """Start the liveness heartbeat thread when worker initializes"""
+ 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()
+ print(f"Celery liveness heartbeat started - updating {_liveness_file}
every {_heartbeat_interval}s")
+{{- else }}
+CELERY_WORKER_HEALTH_CHECK_ENABLED = False
+{{- end }}
+
+{{- /* Results Backend */}}
+{{- if .Values.config.resultsBackend }}
+{{- if kindIs "string" .Values.config.resultsBackend }}
+RESULTS_BACKEND = {{ .Values.config.resultsBackend }}
+{{- else }}
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={{ .Values.cache.host | quote }},
+ {{- if .Values.cache.password }}
+ password={{ .Values.cache.password | quote }},
+ {{- end }}
+ port={{ .Values.cache.port | 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={{ .Values.cache.host | quote }},
+ {{- if .Values.cache.password }}
+ password={{ .Values.cache.password | quote }},
+ {{- end }}
+ port={{ .Values.cache.port | 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 - Required when using
GLOBAL_ASYNC_QUERIES feature flag */}}
+{{- 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": {{ .Values.cache.host | quote }},
+ "CACHE_REDIS_PORT": {{ .Values.cache.port | int }},
+ "CACHE_REDIS_USER": {{ .Values.cache.user | default "" | quote }},
+ {{- if .Values.cache.password }}
+ "CACHE_REDIS_PASSWORD": {{ .Values.cache.password | 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 .Values.cache.sentinel }}
+ {{- 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": {{ .Values.cache.ssl.certfile | default "None"
}},
+ "CACHE_REDIS_SSL_KEYFILE": {{ .Values.cache.ssl.keyfile | default "None"
}},
+ "CACHE_REDIS_SSL_CERT_REQS": {{ .Values.cache.ssl.ssl_cert_reqs | default
"required" | quote }},
+ "CACHE_REDIS_SSL_CA_CERTS": {{ .Values.cache.ssl.ca_certs | default "None"
}},
Review Comment:
**Suggestion:** SSL file path values are emitted without quotes, so any
configured path (for example `/etc/ssl/cert.pem`) is rendered as invalid Python
syntax. Render string paths as quoted strings and keep `None` when unset. [type
error]
<details>
<summary><b>Severity Level:</b> Critical 🚨</summary>
```mdx
- ❌ TLS path configuration can break Python config parsing.
- ⚠️ All components consume same invalid Secret config.
```
</details>
```suggestion
"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_CA_CERTS": {{ if .Values.cache.ssl.ca_certs }}{{
.Values.cache.ssl.ca_certs | quote }}{{ else }}None{{ end }},
```
<details>
<summary><b>Steps of Reproduction ✅ </b></summary>
```mdx
1. Set TLS file paths in values (`cache.ssl.certfile/keyfile/ca_certs`),
whose defaults
are documented at `helm/superset/values.yaml:90-95`.
2. Ensure cache is enabled (`values.yaml:37-38`, default true), which
activates GAQ cache
backend block at `_helpers.tpl:277-317`.
3. Install/upgrade so `secret-superset-config.yaml:41` emits these lines
unquoted at
`_helpers.tpl:445-448` (e.g., `/etc/ssl/cert.pem`).
4. Python parses `/` as operators, producing invalid config syntax and
preventing
Superset/Celery startup across pods mounting `/app/pythonpath` config Secret.
```
</details>
<details>
<summary><b>Prompt for AI Agent 🤖 </b></summary>
```mdx
This is a comment left during a code review.
**Path:** helm/superset/templates/_helpers.tpl
**Line:** 445:448
**Comment:**
*Type Error: SSL file path values are emitted without quotes, so any
configured path (for example `/etc/ssl/cert.pem`) is rendered as invalid Python
syntax. Render string paths as quoted strings and keep `None` when unset.
Validate the correctness of the flagged issue. If correct, How can I resolve
this? If you propose a fix, implement it and please make it concise.
```
</details>
<a
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F38597&comment_hash=db72893bf91528ce325904f5db4fafb2de9720b005e7d47a54a48617168bf463&reaction=like'>👍</a>
| <a
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F38597&comment_hash=db72893bf91528ce325904f5db4fafb2de9720b005e7d47a54a48617168bf463&reaction=dislike'>👎</a>
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]