Skip to content

Proposal: ship a Helm library chart exporting NebariApp templates for use across packs #130

Description

@viniciusdc

Context

The org has ~14 active software packs (nebari-dev/repositories?q=pack), of which 8–10 build their charts around the NebariApp CRD: data-science, llm-serving, data-engineering, superset, rayserve, chat, mlflow, lgtm, pi-coding-agent, and probably a few more I'm miscounting. Each of them carries its own copy of templates/nebariapp.yaml that hand-renders the NebariApp object — wrapping CRD field names in {{- with ... }} blocks, building up spec.routing, spec.auth, spec.landingPage, etc.

Today, when this operator adds a field to the NebariApp CRD (recent examples: routing.tls.secretName via #114, auth.enforceAtGateway, auth.tokenExchange), each pack's template needs an independent hand-edit to surface that field. If a pack doesn't update, the field is silently dropped at templating time — set it in values.yaml, see nothing in the rendered manifest. We hit this in nebari-data-science-pack#57: a real deployment needed enforceAtGateway: true and the only path through was kubectl patch against the live resource, with the chart's values.yaml then drifting from the cluster.

The immediate per-pack fix is to switch the hand-rendered blocks to toYaml | nindent N passthrough (just landed in nebari-data-science-pack#94). That fixes the drop-silently problem at the field level, but it doesn't fix the structural problem: every pack still has its own 60-line templates/nebariapp.yaml, and every CRD addition still requires N copies to converge.

Proposal

Ship a Helm library chart from this repo (or from a sibling repo — see Where to host) that exports a nebari.app.spec template. Each pack's templates/nebariapp.yaml collapses to:

{{- if .Values.nebariapp.enabled }}
{{ include "nebari.app.spec" . }}
{{- end }}

The library template itself is pure passthrough plus the object shell:

{{- define "nebari.app.spec" -}}
apiVersion: reconcilers.nebari.dev/v1
kind: NebariApp
metadata:
  name: {{ include "nebari.app.fullname" . }}
  namespace: {{ .Release.Namespace }}
  labels:
    {{- include "nebari.app.labels" . | nindent 4 }}
spec:
  hostname: {{ required "nebariapp.hostname is required" .Values.nebariapp.hostname }}
  {{- with .Values.nebariapp.service }}
  service:
    {{- toYaml . | nindent 4 }}
  {{- end }}
  {{- with .Values.nebariapp.routing }}
  routing:
    {{- toYaml . | nindent 4 }}
  {{- end }}
  {{- with .Values.nebariapp.auth }}
  auth:
    {{- toYaml . | nindent 4 }}
  {{- end }}
  {{- with .Values.nebariapp.landingPage }}
  landingPage:
    {{- toYaml . | nindent 4 }}
  {{- end }}
{{- end -}}

Adding a new spec.X field to the CRD becomes:

  1. Update the CRD in this repo.
  2. Add a one-line {{- with .Values.nebariapp.X }} X: {{- toYaml . | nindent 4 }} {{- end }} to the library template.
  3. Bump the library chart minor version.
  4. Each pack picks it up via helm dependency update on its next release.

That last step is still N-shaped, but it's mechanical (a Chart.yaml version bump) rather than a template edit, and reviewable in seconds rather than minutes. The library template is also the natural place to enforce object-level invariants once we have them (label conventions, naming, required metadata).

What the library chart owns vs what stays per-pack

Library owns:

  • The full NebariApp object shell (apiVersion, kind, metadata structure with parameterized name).
  • Every spec.<section> block as passthrough.
  • Standard label/annotation helpers (nebari.dev/pack-name, version, helm.sh/chart, etc.) via named templates.
  • Future composite helpers as they emerge — e.g., shared validation templates for value combinations the CRD can't fully express on its own.

Per-pack stays per-pack:

  • values.yaml defaults — every pack has its own JupyterHub-specific or Superset-specific or MLflow-specific OAuth callback path, scopes, healthCheck path.
  • values.schema.json — the curated supported subset differs per pack.
  • Subchart wiring (jupyterhub, postgresql, etc.) and resources that aren't NebariApps.

Where to host

Three reasonable options, with trade-offs:

  1. In nebari-operator itself (this repo). Pro: CRD and library chart move together — make prepare-release could publish both. Versions stay coupled, no chance of "CRD has new field, library doesn't expose it" drift. Con: this repo currently distributes via flat kubectl apply -f deploy/install.yaml, no Helm artifact pipeline at all. Adding one (OCI publish to ghcr.io/nebari-dev/charts, or a gh-pages-style index) is real but not large.

  2. New nebari-helm-library repo. Pro: clean separation, library evolves at its own cadence, doesn't entangle with operator release ritual. Con: another repo to maintain; CRD-vs-library version coupling stops being mechanical and becomes process — easy to forget to bump the library when the CRD changes.

  3. In nebari-software-pack-template. Tempting because it's the canonical scaffold-from point, but conceptually wrong — the template is meant to be copied and diverged from, not depended on. Mentioned for completeness; I'd reject this.

My lean: #1 (here, in nebari-operator). The version-coupling argument is strong, and the Helm-publish pipeline is a one-time cost that the org will benefit from regardless. If shipping Helm artifacts from this repo turns out to be heavier than expected, falling back to #2 is straightforward.

Distribution

Whichever repo hosts it, the library chart needs to be addressable from consuming packs' Chart.yaml. Two viable mechanisms:

  • OCI registry (oci://ghcr.io/nebari-dev/charts/nebari-app-lib). Helm 3.8+ supports this natively, no helm-repo-index machinery. Probably the right default given the org already uses ghcr for image distribution.
  • Classic Helm repo via GitHub Pages. Works, well-trodden, but adds a Pages site to maintain.

OCI looks like the easier path.

Migration

No coordinated rewrite needed:

  1. First adoption: nebari-software-pack-template. Once the template uses the library chart, every new pack scaffolded from it inherits the right pattern automatically.
  2. Existing packs: each one migrates opportunistically on its own release cycle. The passthrough shape that several packs are already converging on (per nebari-data-science-pack#94) is one line away from the library include — delete the file body, add the include call, add the Chart.yaml dependency. Reviewable in five minutes.
  3. Long tail: packs that don't migrate aren't broken — they just don't get the automatic alignment benefit. No urgency, no flag day.

Open questions

  • Whether to also include the controlling {{- if .Values.nebariapp.enabled }} guard inside the library define, or leave it to the consumer. Both work; leaving it to the consumer keeps the library template strictly about the NebariApp object shape and lets consumers gate it on whatever condition they want.
  • Whether metadata.name should be derived from a library helper (nebari.app.fullname) or expected to be pre-computed by the consumer. The former enforces consistency; the latter respects packs that already have their own naming conventions.
  • Whether to ship CRD validation (e.g., a values.schema.json fragment for the supported nebariapp.* shape) alongside the library — would catch typos at helm lint time. Could be a phase 2.
  • Versioning policy. The library version should track CRD compatibility (e.g., library 0.X is compatible with CRD reconcilers.nebari.dev/v1 fields up to operator version Y), but the exact contract is worth being explicit about.

Out of scope (for this proposal, can be follow-ups)

  • A full operator-deploy Helm chart (we still ship via flat manifests; not changing that here).
  • Migrating any specific pack — this proposal is about creating the library, not the per-pack adoption work.
  • Replacing CRD validation. The library is layout, not semantics; the CRD remains the authority on what values are accepted.

Refs: nebari-data-science-pack#57, nebari-data-science-pack#94, #114 (routing.tls.secretName addition that motivated the passthrough pattern for routing).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    Priority

    None yet

    Start date

    None yet

    Target date

    None yet

    Size

    None yet

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions