This is an automated email from the ASF dual-hosted git repository.
villebro pushed a commit to branch main
in repository
https://gitbox.apache.org/repos/asf/superset-kubernetes-operator.git
The following commit(s) were added to refs/heads/main by this push:
new 7989084 refactor(config): mount superset_config.py at /app/pythonpath
(#29)
7989084 is described below
commit 7989084f5effe63201889079f13948fc348ae0cf
Author: Ville Brofeldt <[email protected]>
AuthorDate: Fri May 8 10:14:49 2026 -0700
refactor(config): mount superset_config.py at /app/pythonpath (#29)
* refactor(config): mount superset_config.py at /app/pythonpath
* fix docs references
---
AGENTS.md | 4 ++--
docs/architecture.md | 2 +-
docs/internals.md | 2 +-
internal/common/names.go | 5 +----
internal/controller/config_test.go | 5 +----
internal/controller/reconcile_test.go | 16 ++--------------
internal/controller/superset_controller.go | 5 -----
internal/resolution/resolver_test.go | 14 +++++++-------
8 files changed, 15 insertions(+), 38 deletions(-)
diff --git a/AGENTS.md b/AGENTS.md
index 2a49870..9faabde 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -105,7 +105,7 @@ The operator uses a **two-tier CRD architecture** where the
parent `Superset` re
- **Celery worker configuration**: `spec.celeryWorker.celery` controls Celery
worker command args. Presets set concurrency and pool. Operator constructs the
`celery worker` command from resolved fields. `disabled` preset uses the
hardcoded fallback command.
- **SQLAlchemy engine options**: `spec.sqlaEngineOptions` sets the baseline;
per-component `sqlaEngineOptions` on webServer, celeryWorker, celeryBeat,
mcpServer, lifecycle tasks replaces the top-level entirely (override
semantics). Presets: `disabled` (no rendering), `conservative` (NullPool),
`balanced` (pool_size=1, max_overflow=-1), `performance` (pool_size=workers),
`aggressive` (pool_size=workers×threads). CeleryBeat and lifecycle tasks always
default to NullPool. Pool sizing is com [...]
- **Environment modes**: `environment: dev` allows inline `secretKey`,
`metastore.uri`, `metastore.password`, `valkey.password`,
`lifecycle.adminUser`, and `lifecycle.loadExamples`. `environment: prod`
(default) rejects these via CRD validation; use `secretKeyFrom`,
`metastore.uriFrom`, `metastore.passwordFrom`, or `valkey.passwordFrom` to
reference Kubernetes Secrets (operator injects `valueFrom.secretKeyRef` env
vars).
-- **Env var tiers**: Operator-internal transport vars
(`SUPERSET_OPERATOR__SECRET_KEY`, `SUPERSET_OPERATOR__DB_URI`,
`SUPERSET_OPERATOR__DB_HOST`, `SUPERSET_OPERATOR__VALKEY_HOST`,
`SUPERSET_OPERATOR__FORCE_RELOAD`, etc.) and standard env vars (`PYTHONPATH`).
+- **Env var tiers**: Operator-internal transport vars
(`SUPERSET_OPERATOR__SECRET_KEY`, `SUPERSET_OPERATOR__DB_URI`,
`SUPERSET_OPERATOR__DB_HOST`, `SUPERSET_OPERATOR__VALKEY_HOST`,
`SUPERSET_OPERATOR__FORCE_RELOAD`, etc.).
- **SECRET_KEY validation**: CEL requires either `secretKey` (dev mode) or
`secretKeyFrom` (any mode) to be set.
- **Deployment builder**: All child controllers use `buildDeploymentSpec()`
with flat `FlatComponentSpec`. Reads all fields from the merged
`DeploymentTemplate` hierarchy. No parent lookup needed.
- **Generic child reconciler**: 6 child controllers (all except
SupersetLifecycleTask) use a shared `ChildReconciler` with a `ChildCR`
interface. Each child CRD type implements accessor methods (`GetFlatSpec`,
`GetConfig`, `GetService`, etc.).
@@ -120,7 +120,7 @@ The operator uses a **two-tier CRD architecture** where the
parent `Superset` re
- **Lifecycle tasks**: `spec.lifecycle` on the parent CRD (type
`LifecycleSpec`) defines two sequential tasks: "migrate" (`superset db
upgrade`) and "init" (`superset init`). Each produces a `SupersetLifecycleTask`
child CR named `{parentName}-migrate` and `{parentName}-init`. Tasks run as
bare Pods (restartPolicy: Never) with exponential backoff on failure. Each task
has an independent `strategy`: `VersionChange` (default, runs only on image
changes), `Always` (runs every reconcile), or [...]
- **CRD validation**: All validation uses CEL (`x-kubernetes-validations`) on
CRD types — no admission webhooks. Rules cover: environment mode restrictions,
secret mutual exclusivity, metastore/valkey validation, networking constraints,
monitoring constraints. Defaults (repository, pullPolicy, environment) use
kubebuilder default markers.
- **Metrics**: Operator exposes controller-runtime default metrics (reconcile
counts, durations, leader election) on HTTPS :8443 with Kubernetes auth/authz.
No custom metrics — controller-runtime defaults are sufficient. Superset
instance monitoring via optional `spec.monitoring.serviceMonitor` (creates a
Prometheus ServiceMonitor targeting the web-server component using unstructured
objects; gracefully skips if CRD is absent).
-- **Config mount path**: `/app/superset/config` for superset_config.py.
+- **Config mount path**: `/app/pythonpath` for superset_config.py.
- **All Go files must have the Apache 2.0 copyright header** (see
`hack/boilerplate.go.txt`)
## Naming Conventions
diff --git a/docs/architecture.md b/docs/architecture.md
index ff2e5a9..3bef2cf 100644
--- a/docs/architecture.md
+++ b/docs/architecture.md
@@ -276,7 +276,7 @@ never appear in ConfigMaps or CRD status fields.
### Config Mount Structure
-- `/app/superset/config/` — ConfigMap with `superset_config.py`
+- `/app/pythonpath/` — ConfigMap with `superset_config.py`
---
diff --git a/docs/internals.md b/docs/internals.md
index 3b28d7f..eccca5a 100644
--- a/docs/internals.md
+++ b/docs/internals.md
@@ -278,7 +278,7 @@ the standard Kubernetes mechanism for projecting files into
containers:
The rendered config is already stored on the child CR's `spec.config` field, so
the ConfigMap is technically a derived resource. The child controller creates
it
-from the spec and mounts it at `/app/superset/config/`.
+from the spec and mounts it at `/app/pythonpath/`.
### What Each Component Creates
diff --git a/internal/common/names.go b/internal/common/names.go
index c7b814e..cb3697f 100644
--- a/internal/common/names.go
+++ b/internal/common/names.go
@@ -76,7 +76,7 @@ const (
// Config paths.
const (
ConfigVolumeName = "superset-config"
- ConfigMountPath = "/app/superset/config"
+ ConfigMountPath = "/app/pythonpath"
)
// Init task names.
@@ -110,9 +110,6 @@ const (
EnvAdminFirstName = "SUPERSET_OPERATOR__ADMIN_FIRST_NAME"
EnvAdminLastName = "SUPERSET_OPERATOR__ADMIN_LAST_NAME"
EnvAdminEmail = "SUPERSET_OPERATOR__ADMIN_EMAIL"
-
- // Standard env vars.
- EnvPythonPath = "PYTHONPATH"
)
// ChildName constructs a child resource name from parent name and suffix.
diff --git a/internal/controller/config_test.go
b/internal/controller/config_test.go
index fe77faf..8d7543b 100644
--- a/internal/controller/config_test.go
+++ b/internal/controller/config_test.go
@@ -282,9 +282,6 @@ func TestBuildOperatorInjected(t *testing.T) {
}
envMap := envSliceToMap(injected.Env)
- if envMap["PYTHONPATH"] == "" {
- t.Error("expected PYTHONPATH env var")
- }
if envMap["SUPERSET_OPERATOR__SECRET_KEY"] != "test" {
t.Errorf("expected SUPERSET_OPERATOR__SECRET_KEY=test from
configEnvVars, got %s", envMap["SUPERSET_OPERATOR__SECRET_KEY"])
}
@@ -292,7 +289,7 @@ func TestBuildOperatorInjected(t *testing.T) {
t.Errorf("expected SUPERSET_OPERATOR__FORCE_RELOAD=v2, got %s",
envMap["SUPERSET_OPERATOR__FORCE_RELOAD"])
}
- // Empty config: no volumes, no mounts, no PYTHONPATH.
+ // Empty config: no volumes, no mounts.
empty := buildOperatorInjected("", "child", "", nil)
if len(empty.Volumes) != 0 {
t.Errorf("expected 0 volumes for empty config, got %d",
len(empty.Volumes))
diff --git a/internal/controller/reconcile_test.go
b/internal/controller/reconcile_test.go
index 0521f23..06013bb 100644
--- a/internal/controller/reconcile_test.go
+++ b/internal/controller/reconcile_test.go
@@ -444,18 +444,9 @@ func TestReconcile_AllComponents_FullFeatures(t
*testing.T) {
t.Error("celery worker config should contain WORKER_SETTING")
}
- // WebsocketServer: no PYTHONPATH (Node.js).
+ // WebsocketServer: no config volume (Node.js).
wss := &supersetv1alpha1.SupersetWebsocketServer{}
_ = c.Get(ctx, types.NamespacedName{Name: "full", Namespace:
"default"}, wss)
- hasPythonPath := false
- for _, env := range specEnv(wss.Spec.FlatComponentSpec) {
- if env.Name == "PYTHONPATH" {
- hasPythonPath = true
- }
- }
- if hasPythonPath {
- t.Error("websocket server should not have PYTHONPATH env var")
- }
// MCP server: config should contain MCP_SETTING.
ms := &supersetv1alpha1.SupersetMcpServer{}
@@ -464,7 +455,7 @@ func TestReconcile_AllComponents_FullFeatures(t *testing.T)
{
t.Error("mcp server config should contain MCP_SETTING")
}
- // All Python components should have PYTHONPATH and SECRET_KEY env vars.
+ // All Python components should have SECRET_KEY env vars.
cb := &supersetv1alpha1.SupersetCeleryBeat{}
_ = c.Get(ctx, types.NamespacedName{Name: "full", Namespace:
"default"}, cb)
cf := &supersetv1alpha1.SupersetCeleryFlower{}
@@ -486,9 +477,6 @@ func TestReconcile_AllComponents_FullFeatures(t *testing.T)
{
for _, env := range pc.envs {
envMap[env.Name] = env.Value
}
- if envMap["PYTHONPATH"] == "" {
- t.Errorf("%s should have PYTHONPATH env var", pc.name)
- }
if envMap["SUPERSET_OPERATOR__SECRET_KEY"] != "test-secret-key"
{
t.Errorf("%s should have SUPERSET_OPERATOR__SECRET_KEY
env var in dev mode", pc.name)
}
diff --git a/internal/controller/superset_controller.go
b/internal/controller/superset_controller.go
index bc2d259..c48a518 100644
--- a/internal/controller/superset_controller.go
+++ b/internal/controller/superset_controller.go
@@ -1128,11 +1128,6 @@ func buildOperatorInjected(renderedConfig, childName,
forceReload string, config
MountPath: configMountPath,
ReadOnly: true,
})
-
- injected.Env = append(injected.Env, corev1.EnvVar{
- Name: naming.EnvPythonPath,
- Value: configMountPath,
- })
}
// Add config-derived env vars (secret key, metastore fields, etc.).
diff --git a/internal/resolution/resolver_test.go
b/internal/resolution/resolver_test.go
index c2db8b3..635c135 100644
--- a/internal/resolution/resolver_test.go
+++ b/internal/resolution/resolver_test.go
@@ -189,7 +189,7 @@ func TestResolveChildSpec_ComponentMergesWithTopLevel(t
*testing.T) {
"app.kubernetes.io/component": "celery-worker",
}
operator := &OperatorInjected{
- Env: []corev1.EnvVar{{Name: "PYTHONPATH", Value:
"/app/superset/config"}},
+ Env: []corev1.EnvVar{{Name: "SUPERSET_OPERATOR__SECRET_KEY",
Value: "test"}},
}
result := ResolveChildSpec(ComponentCeleryWorker, topLevel, component,
operatorLabels, operator)
@@ -280,8 +280,8 @@ func TestResolveChildSpec_ComponentMergesWithTopLevel(t
*testing.T) {
if envMap["COMP_ONLY"] != "yes" {
t.Error("component COMP_ONLY should be present")
}
- if envMap["PYTHONPATH"] != "/app/superset/config" {
- t.Error("operator PYTHONPATH should be present")
+ if envMap["SUPERSET_OPERATOR__SECRET_KEY"] != "test" {
+ t.Error("operator SUPERSET_OPERATOR__SECRET_KEY should be
present")
}
// Command: from component (no inheritance)
@@ -348,7 +348,7 @@ func TestResolveChildSpec_OperatorInjectedMerged(t
*testing.T) {
},
}},
},
- VolumeMounts: []corev1.VolumeMount{{Name: "config",
MountPath: "/app/superset/config"}},
+ VolumeMounts: []corev1.VolumeMount{{Name: "config",
MountPath: "/app/pythonpath"}},
InitContainers: []corev1.Container{{Name: "op-init", Image:
"init:op"}},
}
@@ -377,7 +377,7 @@ func TestResolveChildSpec_OperatorInjectedMerged(t
*testing.T) {
}
// VolumeMounts: operator injected
- if len(ct.VolumeMounts) != 1 || ct.VolumeMounts[0].MountPath !=
"/app/superset/config" {
+ if len(ct.VolumeMounts) != 1 || ct.VolumeMounts[0].MountPath !=
"/app/pythonpath" {
t.Error("expected operator volume mount")
}
@@ -408,7 +408,7 @@ func TestResolveChildSpec_OperatorVolumesWinOnConflict(t
*testing.T) {
},
}},
},
- VolumeMounts: []corev1.VolumeMount{{Name: "superset-config",
MountPath: "/app/superset/config"}},
+ VolumeMounts: []corev1.VolumeMount{{Name: "superset-config",
MountPath: "/app/pythonpath"}},
}
result := ResolveChildSpec(ComponentWebServer, topLevel, nil, nil,
operator)
@@ -425,7 +425,7 @@ func TestResolveChildSpec_OperatorVolumesWinOnConflict(t
*testing.T) {
if len(ct.VolumeMounts) != 1 {
t.Fatalf("expected 1 volume mount (merged by name), got %d",
len(ct.VolumeMounts))
}
- if ct.VolumeMounts[0].MountPath != "/app/superset/config" {
+ if ct.VolumeMounts[0].MountPath != "/app/pythonpath" {
t.Errorf("expected operator mount path to win, got %s",
ct.VolumeMounts[0].MountPath)
}
}