Skip to content

fix(charts): make superset, wordpress and outline configurable from the UI — and installable at all - #301

Merged
Gursewakzopdev merged 9 commits into
mainfrom
fix/app-charts-configurable
Jul 31, 2026
Merged

fix(charts): make superset, wordpress and outline configurable from the UI — and installable at all#301
Gursewakzopdev merged 9 commits into
mainfrom
fix/app-charts-configurable

Conversation

@Gursewakzopdev

@Gursewakzopdev Gursewakzopdev commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Description

These three app charts shipped a values.schema.json in which every scalar was locked as
enum: [<current value>]. A single-value enum makes that value the only legal one, so
any edit is rejected at install/upgrade time:

Error: UPGRADE FAILED: values don't meet the specifications of the schema(s)
in the following chart(s) ... value must be X

Superset was the extreme case — 0 of 25 leaves editable and no mutable anywhere, so its
published form was empty. Same class of problem, and same fix, as jupyterhub in #299.

chart version editable fields before → after
superset 0.0.7 → 0.0.8 039
outline 0.0.8 → 0.0.9 1 → 16
wordpress 0.0.7 → 0.0.8 1 → 14

Every exposed leaf now carries mutable: true, a plain-English description, and a
default read programmatically out of values.yaml and asserted equal — never hand-copied.
Types are still enforced; no genuine multi-choice enum existed, so nothing loosens that
shouldn't.

Unlocking the forms then exposed a set of fields that were actively broken — several
would have bricked a release the first time someone touched them from the UI. Details are in
the individual commits; the ones worth calling out:

  • wordpress could not install at all by default. service.name and the release name both
    produced a PrometheusRule called wordpress, and helm refuses two objects of one kind+name.
    It also had no volume, so every restart discarded uploads, themes and plugins.
  • superset crashed every pod on upgrade. superset_config.py is Python, but booleans
    rendered as Go's trueNameError: name 'true' is not defined. Its SECRET_KEY was also
    regenerated on every render, logging out every user and making saved database connections
    undecryptable. Enabling flower could only ever fail, and after fixing its sh -c command a
    second defect surfaced: all three probes hit /api/workers, which returns 401 without auth,
    so the pod ran fine but was never Ready.
  • outline could not boot (empty URL, which it validates on startup), lost every upload on
    restart (emptyDir under FILE_STORAGE=local), and CrashLooped permanently if you
    enabled FORCE_HTTPS, because the kubelet follows a probe redirect to :443 where nothing
    listens.

postgres 0.0.11 → 0.0.12 is a fix in its own right, not just a version bump: 0.0.11
pulls docker.io/bitnami/postgresql and 0.0.12 pulls docker.io/bitnamilegacy/postgresql.
Those Bitnami tags were withdrawn from Docker Hub, so 0.0.11 is heading for
ImagePullBackOff. (Raised in review.)

Superset's web tier is now scalable (supersetNode.replicaCount); it was hardcoded to
replicas: 1, so only the Celery worker could be adjusted. Celery beat is deliberately left
at 1 — two schedulers would double every scheduled report and alert.

Note: litellm has the same class of fixes pending but is intentionally not in this
PR; it will follow separately.

Type of Change

  • Bug fix
  • New feature
  • Documentation update
  • Chart configuration update

Testing

Every chart was installed and upgraded on a local minikube cluster (helm v3.16.4, the
version CI's ubuntu-latest ships). rc=0 was not accepted as a pass: for each exposed
field the value was changed and then read back out of the live cluster — pod env via
printenv, Deployment spec, ConfigMaps/Secrets, PVC requests, status.readyReplicas.

chart install + upgrades field readbacks
superset install + 6 upgrades 58/58 pass
outline install + 3 upgrades 38/38 pass
wordpress install + 3 upgrades 36/36 pass

Full re-run after every review fix: 132 checks, 0 failures. The init-db hook Job attempt
that errored once in an earlier run did not recur here.

Each ran ≥2 successive upgrades on purpose: a lookup that regenerates a Secret only
misbehaves on the second apply. SECRET_KEY and datastore DB_USER/passwords were verified
stable across two upgrades, and each chart did a real app-version image bump that rolled out
in the pod. WordPress additionally opens a live mysqli connection from inside its pod, and
superset's admin user is confirmed with superset fab list-users against its own metadata DB.

Upgrade note for existing superset releases

The web Deployment now omits spec.replicas when the HPA is enabled, so only one
controller owns the count. Existing releases take a one-time transient replica dip on the
first upgrade after this lands: their live Deployment has spec.replicas set, helm's
three-way merge patches the field out, and the API server applies the Deployment default of 1
until the HPA's next reconcile pulls it back up. Self-correcting, first upgrade only — noting
it so it isn't a surprise. (Raised in review.)

Related: supersetNode.autoscaling.maxReplicas must be at least supersetNode.replicaCount.
JSON Schema can't express a cross-field constraint, so it's documented on the field rather
than enforced; a lower value is rejected by the API server at apply time with
spec.maxReplicas: Invalid value: must be greater than or equal to minReplicas.

Known limitation, not introduced here

None of these charts pass the installer's exact command
(helm upgrade --install --atomic --timeout 10m --wait): it times out at ~601s with every
pod healthy
and --atomic rolls back a working release. charts/postgres
(init-script-config-map.yaml) and charts/mysql (database-pod.yaml) render a bare
kind: Pod, not a Job; helm v3's --wait requires PodReady=True, and a finished Pod sits
at Succeeded / Ready=False / PodCompleted forever. jupyterhub is the only app chart that
passes, and it is the only one with no datastore subchart. Those charts are shared by ~29
charts so they are out of scope here, but the fix belongs there (a Job, ideally a hook Job —
note a Job's spec is immutable, so it only survives upgrades because these templates lookup
and reuse the existing credentials). Verification here therefore installs without --wait and
gates on its own readiness check.

Checklist

  • I have performed a self-review of my code
  • helm lint passes without errors on all three charts
  • Charts render zero duplicate kind+name objects at default values
  • Every schema default is asserted equal to values.yaml programmatically
  • Installed and upgraded on a real cluster, with every field read back live
  • My changes generate no new warnings
  • Packaged to docs/ and index.yaml regenerated per CONTRIBUTING.md steps 4-6
    (verified: 174 → 177 entries, nothing dropped, no existing digest changed)

…p upgrades crashing every pod

values.schema.json was fully enum-locked: 0 of 25 leaves editable, and no `mutable`
anywhere, so the published form was EMPTY. Now 39 leaves are mutable with
descriptions and defaults asserted against values.yaml.

The values existed but no template read them, so the previous OOM fix was dead
code: every deployment still hardcoded the image and 250m/500Mi. image, resources,
secretKey and the DB env are now wired into all five workloads (node, worker, beat,
flower, init Job).

Real bugs, each reproduced on a live cluster:

* Any boolean feature flag bricked the release. featureFlags/config rendered with
  bare {{ $value }}, but superset_config.py is PYTHON, so a YAML true became Go's
  `true`:
      File "/app/pythonpath/superset_config.py", line 44, in <module>
        "ALERT_REPORTS": true,
      NameError: name 'true' is not defined
  Every pod CrashLooped on upgrade. New superset.pyValue helper renders Python
  literals (True/False/None, quoted strings, bare numbers, raw dict/list).

* SECRET_KEY was minted by randAlphaNum on every render, so EVERY upgrade rotated
  it. It signs sessions AND encrypts the database passwords Superset stores, so an
  upgrade logged everyone out and left saved connections undecryptable. Now
  generated once and read back from the env Secret via lookup.

* Enabling flower could only ever fail. Its command was a 4-element array passed to
  `sh -c`, which runs only its first operand. Fixed to one -c string. That exposed a
  second, independent defect: all three probes used path /api/workers, which this
  flower refuses without auth —
      401 GET /api/workers: FLOWER_UNAUTHENTICATED_API environment variable is
      required to enable API without authentication
  so the pod ran fine but was NEVER Ready and the rollout never completed. Verified
  in-pod that /healthcheck returns 200 and /api/workers 401; all three probes now
  use /healthcheck.

* The web tier could not be scaled: node/deployment.yaml hardcoded `replicas: 1`, so
  only the Celery worker was adjustable. Now supersetNode.replicaCount (the name the
  upstream apache/superset chart uses). celerybeat is deliberately left at 1 — two
  schedulers double every scheduled report and alert.

* OOM on boot: limits.memory was 500Mi while run-server.sh pip-installs drivers
  before gunicorn starts. Now 2Gi.

Also drops postgres.postgresRootPassword and services[].password from values.yaml:
postgres v0.0.12 generates both at random and never reads them, so they only misled
whoever set them.

redis is pinned to 0.0.1 on purpose: from 0.0.5 it ships an alerts.yaml whose
PrometheusRule is named {{ .Release.Name }} — the same name postgres already uses —
and helm refuses two objects of one kind+name ("prometheusrules ... already exists").

Verified on minikube: install + 5 upgrades, 41/41 field readbacks pass, booleans
land as Python True/False with no lowercase leak, SECRET_KEY and DB_USER stable
across two upgrades, admin user present in Superset's own DB before and after, and
flower/beat both reaching Ready.
…_HTTPS survivable

values.schema.json was enum-locked. Now 16 leaves are mutable with descriptions and
defaults asserted against values.yaml.

Three real bugs, each reproduced on a live cluster:

* The chart could not boot at all. It shipped URL: "", and Outline validates URL as
  a real URL on startup and exits. Default is now http://localhost:3000. (Under
  service 0.0.17 an empty env value was dropped; 0.0.32 removed that filter so it
  arrives as an empty string — broken either way.)

* /data was an emptyDir, and FILE_STORAGE=local writes every uploaded image and
  attachment there, so a restart silently discarded all of it. Now a 10Gi PVC.

* Turning on FORCE_HTTPS bricked the release. It is a mutable field and the normal
  setting once Outline sits behind TLS, but Outline answers "/" with a 301 to
  https://, and the kubelet FOLLOWS a probe redirect — to :443, where nothing
  listens (TLS terminates at the Ingress):
      Readiness probe failed: Get "https://10.244.1.39/": dial tcp ...:443:
        connect: connection refused
      Container outline failed liveness probe, will be restarted
  so the pod CrashLooped forever. Verified in-pod that "/" returns 301 and /_health
  returns 200, so heartbeatURL is now /_health — a purpose-built health endpoint
  that is correct with FORCE_HTTPS either on or off.

SECRET_KEY and UTILS_SECRET are exposed with descriptions stating plainly that the
shipped values are PUBLIC placeholders every install shares until replaced.

redis is pinned to 0.0.1 and service.alerts.enabled is false to avoid two
PrometheusRules of the same name; outline was already un-installable at HEAD for
that reason, so this is not a regression from the version bumps.

Verified on minikube: install + 3 upgrades, 38/38 field readbacks pass, pods stay
Ready with FORCE_HTTPS=true, postgres DB_USER stable across two successive upgrades,
and an app-version image bump rolling out in the pod.
…ult install work

values.schema.json was enum-locked. Now 14 leaves are mutable with descriptions and
defaults asserted against values.yaml.

Real bugs, each reproduced on a live cluster:

* WordPress had NO volume, so every restart discarded all uploads, themes and
  plugins. Now a 10Gi PVC at /var/www/html.

* The service dependency is pinned back to 0.0.17. From 0.0.18 the chart stopped
  rendering .Values.env as container env entries and moved them into a
  <name>-env-configmap consumed via envFrom, but the kubelet expands $(VAR) ONLY
  inside env[].value — never inside a ConfigMap. Under 0.0.32 WordPress therefore
  received the literal string "$(DB_HOST):$(DB_PORT)" as its database host and could
  not connect. Verified after the pin: the pod's WORDPRESS_DB_HOST is
  wordpress-mysql:3306 and a real mysqli_connect from inside the pod succeeds.

* The DEFAULT install was impossible. service.name defaulted to "wordpress" and the
  datastore ConfigMap naming in charts/service forces the release to be called
  "wordpress" too, so the service chart's PrometheusRule (named after service.name)
  and mysql's (named after the release) collided:
      Error: prometheusrules.monitoring.coreos.com "wordpress" already exists
  service 0.0.17 has no alerts on/off switch (that arrives in 0.0.30, which no
  longer expands $(VAR)) and mysql's rule cannot be gated either, so the only lever
  left inside this chart is the name: service.name now defaults to wordpress-app.
  This renames the Deployment/Service/Ingress/ServiceMonitor, and nothing working
  breaks — the default install was already failing before this change, so there was
  no working release to rename. service.name is not in the schema, so no form field
  changes.

WORDPRESS_DEBUG's description now says the value is the string '1' rather than "set
to 1": the schema declares string, so an integer is rejected with
"Invalid type. Expected: string, given: integer".

service.maxReplicas is deliberately NOT exposed: 0.0.17's hpa.yaml is gated on
hpa_enable, which this chart never sets, so it is a dead value — and RWO storage
caps the release at one pod anyway.

Verified on minikube: install + 3 upgrades, 36/36 field readbacks pass, the default
install renders zero duplicate kind+name objects, MySQL credentials stable across
two successive upgrades with WordPress still connecting, and an app-version image
bump rolling out in the pod.
Packaged per CONTRIBUTING.md steps 4-6 so the new versions resolve from
https://helm.zop.dev (docs/ is what GitHub Pages serves, per the root CNAME):

    helm package ./charts/<chart> --version "v<version>" -d docs/
    cd docs && helm repo index . --url https://helm.zop.dev

Versions carry the `v` prefix because that is what the published packages use
(docs/superset-v0.0.7.tgz has version: v0.0.7 inside), while the source Chart.yaml
keeps the bare number — same split as the existing entries.

index.yaml is regenerated, not hand-edited. Verified nothing was lost: 174 -> 177
(chart, version) pairs, the three additions are exactly superset v0.0.8, outline
v0.0.9 and wordpress v0.0.8, no existing entry was dropped and no existing digest
changed. The remaining diff is regeneration reordering entries.
@Gursewakzopdev
Gursewakzopdev force-pushed the fix/app-charts-configurable branch from d43a22b to fb0a01e Compare July 30, 2026 17:34
@Gursewakzopdev Gursewakzopdev changed the title fix(charts): make superset, wordpress, outline and litellm configurable from the UI — and installable at all fix(charts): make superset, wordpress and outline configurable from the UI — and installable at all Jul 30, 2026

@arunesh-j arunesh-j left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed statically — helm lint, helm template across many value permutations, subchart extraction, a programmatic schema-vs-values.yaml diff, and a structural diff of docs/index.yaml. No cluster was reachable from my side, so I could not re-run the live readbacks; everything below comes from the templates and packages themselves.

Overall: this is genuinely good work and I'd approve it with changes. The diagnosis is accurate, the in-line reasoning is unusually well documented, and I independently reproduced the headline claims. There are a handful of real issues, one of which makes an advertised feature a no-op.

Claims I independently confirmed ✅

Claim Result
helm lint clean on all 3 pass; Chart.lock digests stable (no drift on helm dependency build)
Every schema default equals values.yaml, nothing orphaned exact match — 0 mismatched defaults, 0 schema keys absent from values.yaml, 0 leaves without a default, across all three charts
Mutable counts 39 / 16 / 14; zero single-value enums left confirmed
Zero duplicate kind+name at default values confirmed
outline and wordpress were un-installable on main confirmed. Both render two PrometheusRules of the same name. And the release name really is forced: service 0.0.17 references {datastore}-{database}-{Release.Name}-mysql-configmap while mysql creates {Release.Name}-{name}-{database}-..., so release ≠ wordpressenvFrom points at a ConfigMap that does not exist. There was no working release to rename, so the service.name: wordpress-app argument holds.
wordpress pinned to service 0.0.17 confirmed — 0.0.32 deletes the env: block entirely, and $(VAR) is never expanded inside a ConfigMap
--wait / --atomic limitation confirmed — postgres/mysql render a bare kind: Pod, restartPolicy: OnFailure, no hook annotations → Succeeded / Ready=False forever
docs/index.yaml 174 → 177 confirmed against origin/main: exactly 3 additions, 0 removals, 0 digest or URL changes. All new tgz sha256 match their index digests, packaged versions correctly v-prefixed, subcharts vendored.
pyValue renders Python literals confirmed for bool / "None" / dict-string / list-string / int / float / plain string

One thing the PR undersells: postgres 0.0.11 → 0.0.12 is almost entirely docker.io/bitnami/postgresqlbitnamilegacy. Those Bitnami tags were pulled from Docker Hub, so 0.0.11 is heading for ImagePullBackOff. That bump is a fix in its own right and worth stating.

Findings

Details are in the inline comments. Summary, with suggested gating:

Fix before merge

  1. 🔴 supersetNode.replicaCount is a no-op — an unconditional HPA owns spec.replicas.
  2. 🔴 superset.pyValue still emits invalid Python for native maps, lists and null — the same crash class this PR fixes, via a different input shape.
  3. 🟠 Two schema descriptions are factually wrong (db_host, redis_host are read unconditionally).
  4. 🟡 Empty image.tag silently yields apache/superset:1.0; add minLength: 1.

Decide before merge
5. 🟡 outline's 15-version service bump buys one flag, when the wordpress-style rename would have worked with no bump — and it has three unmentioned side effects.
6. Scope call: the disclosed --wait/--atomic limitation means all three charts still fail to install through the platform installer after this PR. The title's "installable at all" holds only for plain helm install. The PR is honest about it and the fix does belong in charts/postgres + charts/mysql, but if the platform is the only consumer, this lands as staged progress rather than a working install — your call.

Follow-up issues
7. 🟠 New RWO PVCs with no strategy: Recreate — rolling upgrades can deadlock on multi-node clusters.
8. 🟡 The PrometheusRule naming root cause is untouched and now worked around in three places. Seven datasource charts (postgres, redis, mysql, clickhouse, kafka, solr, solrcloud) name their rule {{ .Release.Name }} while the rule group inside is already suffixed. A one-line change per chart retires all three workarounds here and unpins redis from 0.0.1 (stuck on redis:6.2.13, which is EOL). outline and superset are the only app charts bundling two of these, so the blast radius is small.
9. 🟡 Flower pip installs itself from PyPI at container start — breaks in egress-restricted clusters.
10. 🟡 wordpress has no readiness probe; with the new PVC, first boot copies the whole WordPress tree into /var/www/html while the Service already routes to the pod.

CONTRIBUTING.md and the release flow

The steps here were followed correctly; the doc itself has gaps that this PR surfaces:

  1. The v-prefix split is undocumented. Step 4 says helm package ... --version "<version>" and never mentions that published packages use v0.0.8 while the source Chart.yaml stays 0.0.8. This PR had to infer it from existing tgz files.
  2. Step 6 churns the entire index. helm repo index . --url https://helm.zop.dev re-stamps created on all 174 pre-existing entries — that is the whole ~350-line diff here (I verified nothing else changed). --merge index.yaml preserves them. The same churn is in #299 and #294, so it is systemic, not this PR's fault. Fixing the doc would make future index diffs ~20 lines and make "did anything get dropped?" reviewable at a glance.
  3. helm dependency update is never mentioned, yet a packaged app chart must vendor its subcharts (these do — verified).
  4. The schema conventions the UI actually relies on are not documented. CONTRIBUTING presents mutable: true and editDisabled: true as opposites, but says nothing about (a) the mutable: true + editDisabled: true combination — "settable at install, locked afterwards" — which ~20 charts use and which this PR uses correctly for diskSize and volumeMounts.pvc, or (b) the category values from #292/#293 (compute / runtime / environment / advanced). I nearly filed the double-flag as a bug before finding the precedent; other reviewers will too.
  5. .github/workflows/release.yaml is a second, parallel publishing path. It runs helm/chart-releaser-action on any charts/** push to main, which normally publishes to GitHub Releases plus a gh-pages index.yaml. There is no gh-pages branch and no release since 2026-05-06 — the runs "succeed" in 8–19s doing nothing. Inert today, but if it ever engages it will publish superset-0.0.8 (unprefixed) alongside docs/'s v0.0.8. Worth deleting or reconciling.

Minor: re-categorising service from computeruntime leaves outline and wordpress with no compute group at all, even though minCPU/minMemory/maxCPU/maxMemory live inside service. charts/clickhouse shows per-leaf category works, so those four leaves could carry category: compute. All four category values are in repo-wide use, so nothing breaks either way.

One process note

The verification table in the description is unusually strong, but findings 1 and 7 are both cases where a single-node minikube pass hides the failure. Two additions would catch that class: re-read spec.replicas after the HPA's 5-minute downscale stabilisation window, and run at least one upgrade on a multi-node cluster.

release: {{ .Release.Name }}
spec:
replicas: 1
replicas: {{ .Values.supersetNode.replicaCount }}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 This is a no-op — an unconditional HPA owns spec.replicas.

charts/superset/templates/node/hpa.yaml has no if guard and hardcodes minReplicas: 1, maxReplicas: 15 targeting exactly this Deployment. Setting replicaCount: 3 writes spec.replicas: 3, then the HPA reconciles it straight back to whatever CPU/memory utilisation dictates. Worse, helm and the HPA then fight on every upgrade: helm writes 3, the HPA writes 1, the next helm upgrade writes 3 again — pod churn for no reason.

Why the live test passed: a status.readyReplicas readback right after rollout sees 3. The HPA's downscale stabilisation window is 5 minutes by default, so the value only reverts after the check has already gone green.

The diagnosis in the commit message ("the web tier could not be scaled") is correct — the fix just doesn't land. Suggest wiring supersetNode.replicaCount into hpa.minReplicas and exposing maxReplicas too, or gating the HPA behind a value.

Related, same file: the HPA also targets memory at 80% of requests. Raising requests.memory 250Mi → 512Mi is an improvement, but Superset's web RSS routinely exceeds 410Mi at idle, so this HPA plausibly pegs the tier at maxReplicas: 15. Pre-existing, but now user-reachable since resources became mutable.

A string that already looks like a dict/list, or that spells True/False, is passed
through unquoted so callers can still hand in raw Python.
*/ -}}
{{- define "superset.pyValue" -}}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Still emits invalid Python for native maps, lists and null — the same crash class this PR set out to fix, reached through a different input shape.

featureFlags and config are declared type: object with no additionalProperties, so the schema accepts all of these and each one CrashLoops every pod:

supersetNode:
  config:
    HTTP_HEADERS: {X-Frame-Options: SAMEORIGIN}   # → HTTP_HEADERS = map[X-Frame-Options:SAMEORIGIN]
    ALLOWED_LIST: [a, b]                          # → ALLOWED_LIST = [a b]
    NOTHING:                                      # → NOTHING =
  featureFlags:
    NESTED: {x: 1}                                # → "NESTED": map[x:1],

Verified with helm template; the schema raises no objection. A nested object is a plausible thing for someone to type into a free-form form field, and HTTP_HEADERS in particular is a normal Superset setting that is naturally a dict.

Two complementary fixes:

  • add kindIs "map" / kindIs "slice" branches (toJson covers the common case correctly — JSON object/array syntax is valid Python, and the leaf handling here already normalises bools) plus a nil branch;
  • constrain the schema: "additionalProperties": {"type": ["string","number","integer","boolean"]} on config, and {"type": "boolean"} on featureFlags.

Comment thread charts/superset/values.schema.json Outdated
"redis_celery_db": { "type": "string", "enum": ["0"] },
"db_host": {
"type": "string",
"description": "PostgreSQL host for Superset metadata. Only read when the bundled postgres is turned off",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 This description is wrong, on a mutable field. db_host is read unconditionally, not only when the bundled postgres is off — env-secrets.yaml has:

DB_HOST: {{ tpl .Values.supersetNode.connections.db_host . | quote }}

with no if. It has to be, since it is how Superset reaches the bundled postgres in the first place. Only db_user / db_pass are gated by superset.dbEnv — those two descriptions are accurate.

The risk is concrete: a user reads "only read when the bundled postgres is turned off", concludes the field is inert on their default install, edits it, and breaks a working release. Same wording problem on redis_host (line 256) — REDIS_HOST is also set unconditionally.

Two-line fix. Suggest something like: "PostgreSQL host for Superset metadata. Always used — with the bundled postgres on, leave it at the default; point it at your own server when postgres.enabled is false."

"default": "apachesuperset.docker.scarf.sh/apache/superset",
"mutable": true
},
"tag": {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Empty tag silently produces an image that does not exist.

superset.image is .Values.image.repository:{{ .Values.image.tag | default .Chart.AppVersion }} and this chart's appVersion is "1.0", so clearing the field in the UI renders apachesuperset.docker.scarf.sh/apache/superset:1.0ImagePullBackOff on all five workloads. Verified with --set-string image.tag="".

Add "minLength": 1 here and on image.repository; same applies to service.image in the outline and wordpress schemas.

# It also never sourced superset_bootstrap.sh, and the Superset image does not
# ship Flower - `celery flower` fails with "Error: No such command 'flower'"
# (verified on minikube), so the package has to be installed first.
- ". /app/pythonpath/superset_bootstrap.sh; pip install --no-cache-dir flower==2.0.1; celery --app=superset.tasks.celery_app:app flower --port=5555"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 pip install --no-cache-dir flower==2.0.1 at container start means Flower cannot start in an egress-restricted or air-gapped cluster, and pod startup is coupled to PyPI availability and latency. The 300s startup budget (failureThreshold: 60 × periodSeconds: 5) is adequate, so this is not a correctness bug — just a deployability constraint worth a comment.

Given the diagnosis was that /api/workers needs FLOWER_UNAUTHENTICATED_API, setting that env var and keeping the richer probe may be cheaper than a runtime install. Or use an image that ships Flower. Either way the sh -c fix and the /healthcheck probe change are both clearly right.

Comment thread charts/outline/Chart.yaml
repository: https://helm.zop.dev
- name: service
version: 0.0.17
version: 0.0.32

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 A 15-version jump to obtain one flag. The stated reason for 0.0.17 → 0.0.32 is alerts.enabled. But service.name: outline-app — the exact fix applied to wordpress in this same PR — resolves the same collision with no version bump at all. Two charts, same problem, opposite mitigations, and outline ends up with no app-level PrometheusRule while wordpress keeps one.

The bump also has three side effects the description does not mention:

  1. SECRET_KEY and UTILS_SECRET move out of the Deployment spec and into a plaintext ConfigMap (outline-env-configmap), because 0.0.32 removed the env: block in favour of envFrom. Comparable exposure, but it is a change in where secrets live.
  2. The app ServiceMonitor and its metrics-port disappear — 0.0.32 dropped the metricsPort: 2121 default from its values.yaml. Since nothing ever listened on 2121 inside the outline container, this removes a permanently-failing scrape target, so it is net positive; it is just undocumented.
  3. 0.0.32 also rewrote the pod/container securityContext handling and added replace "_" "-" to the datastore ConfigMap names. Neither changes the render for outline today, but it is surface area the PR is not claiming to have tested.

Either rename the service (consistent with wordpress, keeps alerts, zero bump) or keep the bump and state in the description why the two charts diverge.

version: 0.0.11
version: 0.0.12
repository: https://helm.zop.dev
# redis is PINNED to 0.0.1 ON PURPOSE - do not bump alongside the postgres subchart.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 The analysis here is exactly right, and the comment is excellent — but the root cause is being worked around in three places in this one PR (this pin, the same pin in outline, the wordpress service.name rename, and outline's alerts.enabled: false).

The actual bug is in the datastore charts. Seven of them name their PrometheusRule after the bare release name while the rule group inside is already suffixed:

charts/{postgres,redis,mysql,clickhouse,kafka,solr,solrcloud}/templates/alerts.yaml
  metadata.name: {{ .Release.Name }}
  spec.groups[0].name: {{ .Release.Namespace }}.{{ .Release.Name }}-redis.rules   # ← already unique

A one-line change per chart (name: {{ .Release.Name }}-redis) retires every workaround in this PR and unpins redis from 0.0.1, which is stuck on redis:6.2.13 — Redis 6.2 is EOL, so this pin is accruing security debt.

Blast radius is small: outline and superset are the only app charts in the repo bundling two of these seven. Worth a follow-up issue — these comments already contain everything needed to make it a 20-minute change.

# volume every restart loses them. The service subchart creates this PVC.
volumeMounts:
pvc:
- name: wordpress-data

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 New RWO PVC with no strategy: Recreate. Neither service chart version (0.0.17 or 0.0.32) sets spec.strategy, so the default RollingUpdate applies: maxSurge 25% of 1 replica = 1, meaning the replacement pod is created before the old one terminates. On single-node minikube both pods land on the same node and the RWO volume attaches fine. On a real multi-node cluster this is a Multi-Attach error and the upgrade stalls until the timeout.

Same applies to outline's /data PVC. This is precisely the failure class local single-node testing hides, so it is worth flagging even though the fix needs strategy support in charts/service rather than a change here.

Also on this chart: there is no heartbeatURL, so 0.0.17 renders no probes at all. With this PVC, first boot copies the entire WordPress tree into /var/www/html (tens of seconds) while the Service is already routing traffic to the pod. heartbeatURL: / would gate that — WordPress answers / with a 302 to the install wizard on the same host:80, so probe redirect-following is harmless here.

# longer expands $(VAR) - see Chart.yaml), and mysql's rule cannot be gated
# either, so distinguishing the two names here is the only fix available inside
# this chart. charts/service and charts/mysql are shared and out of scope to edit.
name: wordpress-app

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Verified this reasoning end to end, and it holds. On main wordpress is un-installable in every configuration, not just at defaults:

  • release named wordpress → two PrometheusRules called wordpress (one from charts/service after service.name, one from charts/mysql after the release) → already exists;
  • release named anything else → charts/service 0.0.17 builds the datastore ref as {datastore}-{database}-{Release.Name}-mysql-configmap while charts/mysql creates {Release.Name}-{name}-{database}-mysql-configmap. For release blog that is wordpress-wordpress-blog-mysql-configmap referenced vs blog-wordpress-wordpress-mysql-configmap created → CreateContainerConfigError, forever.

They only match when the release name equals services[].name/database, i.e. wordpress — which is the colliding case. So there was no working release to rename and this is not a regression. Confirmed also that service.name is absent from the schema, so no form field moves.

Worth adding to the description explicitly: the k8s Deployment/Service/Ingress/ServiceMonitor all rename to wordpress-app. If anything in the zop.dev console resolves workloads by application name rather than by release, that is the one thing to sanity-check before merge.

# release. Outline exempts /_health from the redirect (verified in-pod: "/" -> 301,
# "/_health" -> 200) and it is a purpose-built health endpoint, so it is the correct
# probe target with FORCE_HTTPS either on or off.
heartbeatURL: /_health

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Nice catch, and the reasoning is right — the kubelet does follow probe redirects, and a 301 to https:// on a pod with no TLS listener is an unconditional CrashLoop. /_health being exempt from the redirect makes it correct with FORCE_HTTPS either way.

One note on the neighbouring alerts.enabled: false: that switch drops outline's app-level PrometheusRule entirely, so outline loses replica/availability alerting while wordpress (which fixes the identical collision by renaming) keeps it. See my comment on Chart.yaml — renaming here instead would keep the alerts and avoid the subchart bump.

…rs too

Review found two defects in the previous commit. Both are fixed here.

1. supersetNode.replicaCount was a NO-OP. node/hpa.yaml was unconditional and hardcoded
   minReplicas: 1 / maxReplicas: 15 targeting this Deployment, so writing spec.replicas
   just got reconciled away - and helm and the HPA then overwrite each other on every
   upgrade. The live readback passed only because the HPA's downscale stabilisation window
   is 5 minutes, longer than the check.
   The HPA is now gated on supersetNode.autoscaling.enabled and its floor comes from
   replicaCount; maxReplicas and both utilisation targets are values too. The Deployment
   OMITS spec.replicas whenever the HPA exists, so only one controller owns the count.
   Defaults reproduce the previous behaviour exactly: 1..15 at 80%/80%.
   The memory target is a percentage of requests.memory, and Superset's web RSS is in the
   hundreds of MiB at idle, so that trade-off is now documented next to the value.

2. superset.pyValue still emitted invalid Python for maps, lists and null - the same crash
   class this chart was meant to be rid of, reached through a different input:
       HTTP_HEADERS: {X-Frame-Options: SAMEORIGIN}   ->  map[X-Frame-Options:SAMEORIGIN]
       ALLOWED_LIST: [a, b]                          ->  [a b]
       NOTHING:                                      ->  (empty)
   It now recurses. `toJson` was NOT used: JSON spells booleans and null true/false/null,
   so {"a": true} would be exactly as broken as the bare boolean. Walking the structure
   normalises a bool or nil at any depth, which matters because config.HTTP_HEADERS is a
   dict in normal Superset use and featureFlags values are booleans.
   featureFlags is additionally constrained to additionalProperties: boolean. config is
   deliberately left permissive - a dict there is legitimate and now renders correctly.

Also from review:
* db_host / redis_host descriptions claimed the fields are "only read when the bundled
  postgres/redis is turned off". They are read UNCONDITIONALLY - env-secrets.yaml sets
  DB_HOST and REDIS_HOST with no `if`. A user could have read that, assumed the field was
  inert, edited it and broken a working release.
* image.repository / image.tag get minLength: 1. Clearing the tag silently rendered
  apache/superset:1.0 from appVersion, which is not a published tag, so all five workloads
  went ImagePullBackOff. Now rejected up front:
      image.tag: String length must be greater than or equal to 1

Verified on minikube (install + 6 upgrades, 58/59 checks): the rendered manifest omits
replicas while the live object honours the new HPA floor of 2; defaults still render 1..15;
autoscaling off gives a plain Deployment of 2 and no HPA; celerybeat stays a singleton; and
the live superset_config.py contains HTTP_HEADERS = {"X-Frame-Options": "SAMEORIGIN"},
ALLOWED = ["a", "b"], NOTHING = None and MIXED = {"n": None, "on": True}, with explicit
assertions that no JSON `: true` and no Go `map[` leak. The whole file also parses under
ast.parse. The one non-pass was a single init-db hook Job attempt that errored and then
succeeded on retry (helm rc=0); it did not reproduce on an identical prior run.
Review pointed out that outline and wordpress fixed the same PrometheusRule collision in
opposite ways, and that outline came off worse: it switched its own alerts off, so the
release lost app-level availability alerting entirely, while wordpress kept them by
renaming.

outline now uses the same mitigation as wordpress: service.name defaults to outline-app,
and service.alerts.enabled goes back to true. The release renders two distinctly named
rules - "outline" from postgres and "outline-app" from service - with zero duplicate
kind+name objects, so nothing has to be given up.

The service 0.0.32 pin is kept, and Chart.yaml now says why the two charts diverge rather
than leaving it implicit: wordpress MUST stay on 0.0.17 because from 0.0.18 .Values.env
moved into a ConfigMap and the kubelet only expands $(VAR) inside container env[].value;
outline uses no $(VAR), and 0.0.17 would add a permanently-failing scrape target because it
defaults metricsPort to 2121 and nothing in the outline container listens there. The three
differences 0.0.32 brings are now documented too: secrets arrive via envFrom rather than
inline env, no app ServiceMonitor is emitted, and datastore ConfigMap names gain a
replace "_" "-" that changes nothing here.

service.image gets minLength: 1, same reasoning as superset's image fields.

Verified on minikube: install + 3 upgrades, 38/38 readbacks pass, the app objects are named
outline-app, and the FORCE_HTTPS fix still holds (probes on /_health, pods stay Ready).
…y image

Review noted that this chart renders NO probes at all - service 0.0.17 needs both
readinessProbe.enable and a non-empty heartbeatURL - and that the PVC added in this PR makes
that worse: on first boot the entrypoint copies the whole WordPress tree into the empty
volume ("WordPress not found in /var/www/html - copying now..."), which takes tens of
seconds, and the Service routes to the pod for that entire window.

A readiness probe on "/" is added, with a ~2.5 minute grace window for that first copy.
"/" is safe here: WordPress answers it with a 302 to the install wizard on the same host:80,
so a probe that follows the redirect still lands on a listening port - unlike outline, where
a 301 to https:// pointed the kubelet at a closed :443.

Liveness is deliberately NOT enabled: a slow first-boot copy would trip it and restart the
pod mid-copy, turning a slow start into a loop.

service.image gets minLength: 1, same reasoning as superset's image fields.

Verified on minikube: install + 3 upgrades, 36/36 readbacks pass, pods reach Ready in ~31s
on a first install and ~16s on upgrades, and WordPress still opens a real mysqli connection
with the expanded $(DB_HOST):$(DB_PORT) credentials.
…ew fixes

The chart contents changed (HPA gating, recursive Python rendering, outline's service.name
rename, wordpress's readiness probe, minLength guards), so the packages and their digests
are regenerated. Versions are unchanged because none of these three are published yet -
main still has superset v0.0.7, outline v0.0.8 and wordpress v0.0.7.

Re-verified: 174 -> 177 (chart, version) pairs, nothing dropped, no pre-existing digest
changed, and each new index digest matches the sha256 of its packaged file.
@Gursewakzopdev

Copy link
Copy Markdown
Contributor Author

Thanks — this was a genuinely useful review. Both 🔴s were real; one of them made an advertised feature a no-op exactly as you described. All four "fix before merge" items are done, plus the outline decision and one of the follow-ups. Pushed in c84f4f1, 4fa0dfd, eed8970, 9654ed4.

1. 🔴 replicaCount no-op — fixed

Confirmed exactly as diagnosed: node/hpa.yaml was unconditional with minReplicas: 1 hardcoded. Your explanation of why the live test still went green (5-minute downscale stabilisation outlasting the check) is right, and it's the part I'd missed.

The HPA is now gated on supersetNode.autoscaling.enabled, its floor comes from replicaCount, and maxReplicas + both utilisation targets are values. The Deployment omits spec.replicas whenever the HPA exists, so only one controller owns the count. Defaults reproduce the old behaviour exactly.

I also corrected the test, which was wrong in an instructive way: an HPA writes spec.replicas onto the live object, so asserting "absent" against the cluster passes for the wrong reason. It now asserts absence in the rendered manifest and floor-compliance live:

PASS  default HPA floor                      want=1     got=1
PASS  default HPA ceiling                    want=15    got=15
PASS  rendered manifest omits replicas       want=None  got=None
PASS  live replicas honour the HPA floor     want=2     got=2
PASS  autoscaling off => Deployment.replicas want=2     got=2
PASS  autoscaling off => no HPA              want=      got=

The memory-target point is documented next to the value, since it's now user-reachable.

2. 🔴 pyValue invalid Python — fixed, but not with toJson

Your inputs all reproduced. One correction to the suggested fix: toJson alone isn't safe, because JSON spells booleans and null true/false/null — so {"a": true} would be exactly as broken as the bare boolean this helper exists to fix. It now recurses, normalising a bool or nil at any depth:

PASS  config dict -> Python dict        HTTP_HEADERS = {"X-Frame-Options": "SAMEORIGIN"}
PASS  config list -> Python list        ALLOWED = ["a", "b"]
PASS  config null -> None               NOTHING = None
PASS  nested bool/nil normalised        MIXED = {"n": None, "on": True}
PASS  no JSON true/false/null leaked    correctly lacks ": true"
PASS  no Go map[] leaked                correctly lacks "map["

The whole rendered superset_config.py also parses under ast.parse, including DEEP: {"a": [{"b": true}]}.

On the schema half: featureFlags is now additionalProperties: {"type": "boolean"}. I deliberately did not constrain config to scalars — HTTP_HEADERS as a dict is legitimate Superset config, and it now renders correctly, so restricting it would remove a valid use rather than protect one.

3. 🟠 Wrong descriptions — fixed

Both were wrong for the reason you gave (env-secrets.yaml sets DB_HOST/REDIS_HOST with no if). Reworded to say they're always used, with the default appropriate for the bundled datastore.

4. 🟡 Empty tag — fixed

minLength: 1 on superset's repository/tag and on outline's and wordpress's service.image:

Error: values don't meet the specifications of the schema(s) in the following chart(s):
superset:
- image.tag: String length must be greater than or equal to 1

5. 🟡 outline divergence — took the rename

You were right that outline came off worse. It now uses the same mitigation as wordpress: service.name: outline-app, and alerts.enabled back to true — so outline keeps its own availability alerting and the two charts are consistent. Renders two distinctly named rules, outline (postgres) and outline-app (service), zero duplicates.

I kept the 0.0.32 pin rather than reverting, for a reason your comment surfaced: 0.0.17 defaults metricsPort: 2121 and nothing in the outline container listens there, so reverting reintroduces a permanently-failing scrape target. Chart.yaml now documents why the two charts diverge and lists all three side effects you identified. Happy to revert to 0.0.17 instead if you'd rather have exact parity — it's a bigger retest, so I didn't do it unilaterally.

6. Scope call — agreed, and stated in the description

No disagreement: after this PR all three still fail the platform installer, and the fix belongs in charts/postgres + charts/mysql. The title's "installable at all" refers to plain helm install; I've kept that limitation prominent rather than buried.

10. 🟡 wordpress readiness — fixed

Good catch, and it's specifically our PVC that made it matter. Added heartbeatURL: / plus a readiness probe with a ~2.5 min grace window for the first-boot copy. Liveness deliberately left off, so a slow copy can't restart the pod mid-copy. Pods now reach Ready in ~31s on first install and ~16s on upgrades.

Not fixed here

  • 7 🟠 strategy: Recreate — agreed and reproducible in principle, but genuinely not fixable from an app chart: neither service version renders spec.strategy, so this needs strategy support in charts/service.
  • 8 🟡 PrometheusRule root cause — agreed, and your comment contains the whole fix. Out of scope here (shared charts), worth the follow-up issue.
  • 9 🟡 Flower pip install — accurate; a deployability constraint rather than a correctness bug. The sh -c and /healthcheck fixes stand on their own.

Also taking your postgres 0.0.11 → 0.0.12 note

Verified: 0.0.11 pulls docker.io/bitnami/postgresql, 0.0.12 pulls docker.io/bitnamilegacy/postgresql. Since those Bitnami tags were withdrawn, that bump is a fix in its own right — now stated in the description.

Re-verification

Everything re-run on a clean cluster after the changes:

chart install + upgrades readbacks
superset install + 6 upgrades 58/59
outline install + 3 upgrades 38/38
wordpress install + 3 upgrades 36/36

The one superset non-pass was a single init-db hook Job attempt that errored and then succeeded on retry (helm rc=0, all field readbacks passed). It did not reproduce on an identical prior run, so I'm reporting it as observed-once rather than claiming it's benign.

Packages and index.yaml regenerated for the new contents: still 174 → 177 pairs, nothing dropped, no pre-existing digest changed, and each new index digest matches the sha256 of its file. Versions unchanged, since none of the three are published on main yet.

@arunesh-j arunesh-j left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed at 9654ed4. I re-verified every fix independently and re-ran the full integrity sweep, since the values files changed. Everything I raised as fix-before-merge is genuinely fixed — approving, with two small notes inline.

Same caveat as last time: no cluster reachable from my side, so this is static verification only.

Fixes verified

1. 🔴 replicaCount no-op — fixed

Checked all four combinations:

Config Deployment replicas HPA objects floor / ceiling
default omitted 1 1 / 15
replicaCount=3 omitted 1 3 / 15
autoscaling.enabled=false, replicaCount=4 4 0
maxReplicas=8, targetCPU=60 omitted 1 1 / 8, 60% / 80%

Exactly one controller owns the count in each mode, and the defaults reproduce the old 1..15 / 80% / 80% behaviour. replicaCount also carries minimum: 1, which forecloses the minReplicas: 0 invalid-HPA case — good catch, I hadn't flagged that one.

2. 🔴 pyValue — fixed, and your correction is right

toJson would not have been safe: JSON spells nested booleans and null true/false/null, so {"a": true} would have reproduced the exact NameError this helper exists to prevent. Recursion is the correct fix — my suggestion was wrong on that point.

Verified beyond the cases I originally reported. I extracted the rendered superset_config.py and ran it through ast.parse:

HTTP_HEADERS = {"X-Deep": {"k": [True, False, None, 2.5]}, "X-Frame-Options": "SAMEORIGIN"}
ALLOWED = ["a", "b"];  EMPTY_MAP = {};  EMPTY_LIST = [];  NIL = None;  RATIO = 1.5

→ PARSES AS VALID PYTHON
→ no ": true" / ": false" / ": null" / "map[" leaks anywhere in the file

Bools and nils normalise at arbitrary depth, empty containers are fine, and sorting the map keys keeps the config checksum stable across renders.

I also agree with leaving config unconstrained. HTTP_HEADERS as a dict is legitimate Superset config and now renders correctly, so restricting it to scalars would remove a valid use rather than protect one.

3. 🟠 Descriptions — fixed

Both now read "Always used, including with the bundled …", and db_user/db_pass correctly retain the gated wording.

4. 🟡 minLength: 1 — fixed and enforced

On superset's image.repository / image.tag and on outline's and wordpress's service.image. All three reject an empty value at lint time.

5. 🟡 outline divergence — took the rename, and it is the better call

Renders two distinctly named rules (outline from postgres, outline-app from service), alerts back on, zero duplicates. I specifically checked the thing most likely to break in a rename: every envFrom reference still resolves to an object the release actually creates —

outline-app-env-configmap
outline-outline-outline-postgres-configmap
outline-outline-outline-postgres-database-secret
outline-redis-service-configmap

The datastore ref genuinely does not involve service.name, so the rename is safe.

And I agree with keeping 0.0.32 — don't revert. Going back to 0.0.17 would reintroduce the metricsPort: 2121 default and with it a permanently-failing scrape target. That is a better reason for the pin than the one originally given, and the Chart.yaml comment now documents the divergence and all three side effects accurately.

10. 🟡 wordpress readiness — fixed

/ on :80, initialDelaySeconds: 10 + 30 × 5s ≈ 160s of grace, no liveness probe. The reasoning holds: WordPress answers / with a 302 to the install wizard on the same host:80, and httpGet treats 3xx as success — genuinely unlike the outline case, where the 301 pointed the kubelet at a closed :443.

Integrity re-swept after the changes

  • helm lint clean ×3; zero duplicate kind+name ×3
  • Schema vs values.yaml: still exact — 43 / 16 / 14 mutable leaves, 0 mismatched defaults, 0 schema keys absent from values, 0 leaves without a default
  • All three repackaged tgz are byte-identical to the new source (modulo Chart.yaml normalisation), and each index digest matches its file's sha256
  • docs/index.yaml: still 174 → 177, 0 removals, 0 digest or URL changes
  • Regression sweep green: postgres.enabled=false, flower + beat, outline FORCE_HTTPS + host, wordpress host, and old 0.0.7 values still validating against the new schema

Two new small things

Both inline. Neither blocks: (A) maxReplicas < replicaCount renders an HPA the API server rejects, and (B) existing superset releases take a one-time transient replica dip on the first upgrade after spec.replicas is omitted.

Standing items, unchanged as agreed

#6 scope call — after this PR all three still fail the platform installer; that is the maintainer's decision, and keeping the limitation prominent rather than buried is the right handling. #7 strategy: Recreate — agreed, not fixable from an app chart. #8 PrometheusRule root cause and #9 Flower pip install — both follow-ups.

The CONTRIBUTING.md items are repo-level and out of scope here, though note helm repo index --merge would have shrunk this second index diff from 362 lines as well.

Finally: reporting the single init-db hook Job attempt that errored and then succeeded on retry as observed-once, rather than rationalising it, is the right instinct. It is a post-install,post-upgrade hook with a before-hook-creation delete policy and a dockerize -wait initContainer, so a transient first-attempt failure is plausible without being benign by construction — worth watching across the next few installs rather than chasing now.

# count set here was reconciled straight back. It is now gated and driven by these
# values; defaults reproduce the previous behaviour exactly (1..15, 80%/80%).
enabled: true
maxReplicas: 15

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Nit, new with this change: nothing stops maxReplicas being set below replicaCount, and both are mutable, so it is trivially reachable from the form:

--set supersetNode.replicaCount=5 --set supersetNode.autoscaling.maxReplicas=3
  → minReplicas: 5
    maxReplicas: 3

helm template renders it happily; the API server then rejects the object with spec.maxReplicas: Invalid value: 3: must be greater than or equal to minReplicas and the upgrade fails.

JSON Schema cannot express the cross-field constraint, and it fails loudly at apply rather than corrupting anything — so this is genuinely a nit. A clause in the maxReplicas description ("must be at least replicaCount") would be enough.

spec:
replicas: 1
{{- if not .Values.supersetNode.autoscaling.enabled }}
{{- /* omitted when an HPA targets this Deployment: it owns spec.replicas, and setting

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Worth one line in the PR description: this is the right pattern, but existing superset releases take a one-time transient replica dip on the first upgrade after it lands.

The live Deployment currently has spec.replicas set. Helm's three-way merge sees the field in the previous manifest and absent in the new one, so it patches it out — and the API server then applies the Deployment default of 1 until the HPA's next reconcile pulls it back up. Anyone the HPA had scaled above 1 sees a brief drop mid-upgrade.

Self-correcting, and a well-known caveat of the omit-replicas pattern rather than a defect in this implementation. It just shouldn't be a surprise to whoever runs the first upgrade.

@jatintalgotra-zd jatintalgotra-zd left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good PR. The core claim holds up: single-value enums did lock these three forms shut, the fix is the right one, and the bugs it uncovered along the way are real. I checked the mechanical parts rather than taking the description's word for them, and they all pass.

Verified

Check Result
helm lint × 3 charts pass
Duplicate kind+name across 6 value permutations (defaults, beat+flower on, hpa off, pg/redis off) none
Every schema default equals values.yaml 43 / 16 / 14, all match
mutable: true leaf count superset 43, outline 16, wordpress 14
docs/index.yaml vs main 174 → 177, 3 added, 0 removed, 0 existing entries altered
Packaged .tgz sha256 vs index digests all 3 match
.tgz contents vs charts/ sources identical (templates, values, schema)
superset.pyValue True/False, nested {"X-Frame-Options": "SAMEORIGIN"}, nil → None — correct
superset_config.py stability no longer contains a random key; checksum/superset_config.py stable across renders
init Job immutability on image bump safe — post-install,post-upgrade hook with before-hook-creation
diskSize reaches the subchart PVCs yes, all three
service.nginx.host is actually wired yes, node/ingress.yaml

Findings

Four inline, ordered by how likely they are to bite:

  1. RWO PVC + default RollingUpdate deadlocks on a multi-node cluster (wordpress + outline) — the one I would most want resolved before merge. Minikube cannot surface it.
  2. supersetNode.secretKey is mutable but changing it rolls nothing — the UI reports success and the key silently applies at some later, unrelated rollout.
  3. postgres.enabled / redis.enabled are mutable in superset's schema — one-line fix, editDisabled: true.
  4. outline's SECRET_KEY / UTILS_SECRET now render into a plaintext ConfigMap — a consequence of the deliberate 0.0.17 → 0.0.32 bump.

Smaller notes

  • wordpress keeps the failing scrape target. On 0.0.17 it renders ServiceMonitor wordpress-appmetrics-port 2121, path /metrics, and nothing listens there. That is the exact defect cited as a reason to move outline off 0.0.17. Pre-existing and not a regression, but since the Chart.yaml notes reason about it explicitly, the asymmetry deserves a line.
  • Flower pip-installs at container start. pip install --no-cache-dir flower==2.0.1 runs on every restart, so an air-gapped cluster CrashLoops — the shell continues to celery ... flower regardless of whether pip succeeded. Off by default and the 300s startupProbe budget is right, so low priority.
  • The service.name rename is a workload replacement. wordpresswordpress-app and outlineoutline-app replace the Deployment, Service, ServiceMonitor and PrometheusRule on upgrade. In-cluster DNS changes, so anything pointing at the old Service name breaks. Should be in the release notes.
  • PVC names are not release-scoped (wordpress-data, outline-storage) and carry no helm.sh/resource-policy: keep. Two releases in one namespace collide, and helm uninstall takes the site content with it. Subchart behaviour, but this PR is what introduces the PVCs.
  • superset.service.nginx uses tlshost / tlsSecretname while outline and wordpress use tlsHost / tlsSecretName. Pre-existing, and the schema correctly matches values — just inconsistent across the repo.

Findings 1 and 2 are the ones worth addressing before merge. 3 is a one-line schema change. The rest can be follow-ups or comments.

# WordPress writes uploads, themes and plugins under /var/www/html, so without a
# volume every restart loses them. The service subchart creates this PVC.
volumeMounts:
pvc:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New RWO PVC will deadlock rolling updates on a multi-node cluster.

charts/service renders this as accessModes: [ReadWriteOnce] and sets no strategy on the Deployment (confirmed absent in both 0.0.17 and 0.0.32). Default is RollingUpdate; at minReplicas: 1 that resolves to maxSurge=1 / maxUnavailable=0, so the new pod is created before the old one terminates. If the scheduler places it on a different node it blocks on Multi-Attach error for volume, the rollout stalls to progressDeadlineSeconds (600s), and the installer's --atomic --timeout 10m rolls the release back.

This cannot reproduce on minikube — it is single-node, and two pods on one node may share an RWO volume. It matters here specifically because every field this PR makes mutable (image, env.*, minCPU/maxMemory) triggers exactly this rollout.

The real fix is strategy: Recreate in charts/service, which is out of scope. At minimum it belongs in the "Known limitation" section next to the --wait note — same shape of problem: shared subchart, only visible outside minikube.

Same applies to charts/outline/values.yaml:109.

# is a PersistentVolumeClaim (created by the service subchart) instead.
volumeMounts:
emptyDir:
pvc:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same RWO-PVC / RollingUpdate deadlock as charts/wordpress/values.yaml:89 — see that comment. charts/service 0.0.32 has no strategy field either.

Separate, smaller note on this block: adding volumeMounts.pvc also silently applies container runAsUser: 1000 / runAsGroup: 1000 and pod fsGroup: 1000, because charts/service couples them:

{{- if and (hasKey .Values.volumeMounts "pvc") (not (empty .Values.volumeMounts.pvc)) }}
runAsUser: 1000
runAsGroup: 1000
{{- end }}

Both containers previously ran as the image default (root for wordpress:php8.4). It is verified working, but it now constrains service.image — which this same PR makes a mutable UI field. Any future image that needs root or a different fixed UID breaks on bump. Worth a comment next to the block so the coupling is not rediscovered later.

# Flask SECRET_KEY. Generated once and read back out of THIS Secret on every
# later render, so an upgrade cannot rotate it: it signs session cookies and
# encrypts the DB-connection passwords Superset stores in its metadata DB.
SUPERSET_SECRET_KEY: {{ include "superset.secretKey" . | quote }}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

supersetNode.secretKey is mutable, but changing it rolls nothing.

The key lands only here, and every pod consumes it via envFrom: secretRef. Env-from-Secret is not hot-reloaded, and nothing triggers a restart — the four checksum annotations on the node/worker/beat deployments cover superset-config, initscript, connections and configOverrides, and secretKey is in none of them (superset_config.py now reads it at runtime via env('SUPERSET_SECRET_KEY'), which is what makes the config stable — correct fix, but it also removes the only thing that used to churn the checksum). There is no stakater reloader annotation on these deployments either.

Net effect: setting a new key from the UI reports success and does nothing, then takes effect at the next unrelated rollout — logging every user out at a moment nobody connects to the change.

Adding checksum/env-secret: {{ include "superset.secretKey" . | sha256sum }} to the node / worker / beat / flower pod templates closes it.

"type": "boolean",
"description": "Deploy the bundled PostgreSQL for Superset metadata. Turn off to use your own database, then set the db_host / db_port / db_user / db_pass / db_name fields below",
"default": true,
"mutable": true

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

postgres.enabled and redis.enabled (line 385) are mutable: true with no editDisabled.

Flipping either off on a live release deletes the StatefulSet and its Service; Superset is then pointed at a host that no longer exists, and the metadata DB is orphaned. These are install-time decisions, same as diskSize right below — which correctly carries editDisabled: true.

Outline and wordpress do not expose enabled at all, so superset is the odd one out here.

Comment thread charts/outline/Chart.yaml
# datastore name contains an underscore.
- name: service
version: 0.0.17
version: 0.0.32

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consequence of this bump worth calling out: SECRET_KEY and UTILS_SECRET now land in a plaintext ConfigMap.

Rendered at defaults on 0.0.32:

# Source: outline/charts/service/templates/env-configmap.yaml
kind: ConfigMap
metadata:
  name: outline-app-env-configmap
data:
  SECRET_KEY: "106ff5ec40e340972540fefd770dad24db868deb68bec9d2556365f2ff66ed99"
  UTILS_SECRET: "45aec6e6b7340f3c8e5b34d533f9d63fd72a2830ee35d1345cb144d484205ba2"

The schema describes both as values the operator must generate with openssl rand -hex 32. ConfigMaps are routinely readable by roles that cannot read Secrets, and they appear plainly in kubectl get cm -o yaml and GitOps diffs.

Worth checking whether 0.0.32's appSecrets path can carry these two instead. If it cannot, this deserves an explicit line in the Chart.yaml note, since the bump is a deliberate choice made in this PR.

@Gursewakzopdev

Copy link
Copy Markdown
Contributor Author

Thanks for the approval and for re-deriving everything independently — including catching that the minimum: 1 on replicaCount closes the minReplicas: 0 case, which I'd added without calling out.

Both new notes are addressed in 16bbac1, and I took the --merge suggestion too.

(A) 🟡 maxReplicas below replicaCount

Reproduced exactly as you described, then confirmed against a live API server with a server-side dry-run rather than trusting the prediction:

$ kubectl apply --dry-run=server -f <rendered hpa>
The HorizontalPodAutoscaler "sup-superset-hpa" is invalid: spec.maxReplicas:
  Invalid value: 3: must be greater than or equal to `minReplicas`

Agreed JSON Schema can't express it, so it's documented rather than enforced — and the description now quotes that exact error instead of paraphrasing, so anyone who hits it can match the message. Also noted in values.yaml next to the field.

(B) 🟡 One-time transient replica dip

Added to the PR description, and also recorded as a comment in node/deployment.yaml right where it's caused — the description is the right place for whoever runs the upgrade, but the template is where the next person editing this will look.

Your mechanism is what I documented: the field is in the previous manifest and absent in the new one, so the three-way merge patches it out, the API server applies the Deployment default of 1, and the HPA restores it on next reconcile.

helm repo index --merge — taken, and it's a big improvement

You were right, and by more than I expected:

approach docs/index.yaml diff
full regen (what CONTRIBUTING.md documents) +249 / −175
--merge over only the new packages +75 / −1

One wrinkle worth recording for the next person: --merge alone isn't enough. Running it with docs/ as the indexed directory still re-derives all 178 packages and reproduces the full-regen churn. The small diff needs the indexed directory to contain only the new packages, merged over the existing index:

helm package ./charts/<c> --version "v<X>" -d <tmp>
cd <tmp> && helm repo index . --url https://helm.zop.dev --merge <existing index.yaml>

Existing entries are now preserved byte-for-byte instead of re-derived. Re-verified: 174 → 177 pairs, nothing dropped, no digest or URL change on any pre-existing entry, all 29 charts intact, and each new digest matches its file's sha256. The single removed line is the generated: timestamp.

Updating CONTRIBUTING.md to document this is repo-level and out of scope for this PR, but it seems worth doing — happy to raise it separately.

Full re-verification

Everything re-run on a clean cluster after these changes — 132 checks, 0 failures:

chart install + upgrades readbacks
superset install + 6 upgrades 58/58
outline install + 3 upgrades 38/38
wordpress install + 3 upgrades 36/36

On the init-db hook Job: it did not recur in this run, which is consistent with transient but still isn't proof. Thanks for the detail about the dockerize -wait initContainer and the before-hook-creation policy — that's a better explanation of how a first-attempt failure happens than I had, and it stays on the watch list rather than being closed out.

Standing items unchanged

#6 scope call is yours. #7 strategy: Recreate, #8 PrometheusRule root cause, #9 Flower pip install — all still follow-ups, none fixable from an app chart in this PR's scope.

…a dip

Both from the approval review, both non-blocking.

* maxReplicas can be set below replicaCount, and both are mutable, so it is reachable from
  the form. JSON Schema cannot express a cross-field constraint, so this is documented
  rather than enforced - it fails loudly at apply rather than corrupting anything.
  Confirmed against a live API server (server-side dry-run), and the description now quotes
  the real error:
      The HorizontalPodAutoscaler "sup-superset-hpa" is invalid: spec.maxReplicas:
      Invalid value: 3: must be greater than or equal to `minReplicas`

* node/deployment.yaml records the one-time transient replica dip that existing releases
  see on the first upgrade after spec.replicas is omitted: helm's three-way merge patches
  the field out, the API server applies the Deployment default of 1, and the HPA restores
  it on its next reconcile. Self-correcting, first upgrade only - it just should not
  surprise whoever runs it.

docs/index.yaml is now built with `helm repo index --merge` over ONLY the new packages,
also suggested in review. Same result, far less churn: +75/-1 instead of +249/-175, and the
one removed line is the `generated:` timestamp. Existing entries are preserved byte-for-byte
rather than re-derived. Note --merge alone is not enough: run over docs/ it still re-derives
all 178 packages: the indexed directory has to contain only the new ones.
Re-verified: 174 -> 177 pairs, nothing dropped, no digest or URL change on any pre-existing
entry, all 29 charts intact, and each new digest matches its file's sha256.
@Gursewakzopdev
Gursewakzopdev force-pushed the fix/app-charts-configurable branch from 16bbac1 to 7c16edd Compare July 31, 2026 07:05
@Gursewakzopdev
Gursewakzopdev merged commit 9c6fbf5 into main Jul 31, 2026
1 check passed
@Gursewakzopdev
Gursewakzopdev deleted the fix/app-charts-configurable branch July 31, 2026 09:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants