From 15942dbea99e2c805b35ff6a72cebf97fef428f9 Mon Sep 17 00:00:00 2001 From: Philip Meier Date: Thu, 2 Jul 2026 12:08:44 +0000 Subject: [PATCH 1/2] Add nebari-app library Helm chart for NebariApp CR --- .github/workflows/release.yml | 7 ++ .github/workflows/test-chart.yml | 3 + .gitignore | 4 + Makefile | 29 +++++- charts/nebari-app/Chart.yaml | 6 ++ charts/nebari-app/README.md | 107 ++++++++++++++++++++ charts/nebari-app/templates/_nebari-app.tpl | 13 +++ test/fixture/Chart.yaml | 10 ++ test/fixture/templates/_helpers.tpl | 24 +++++ test/fixture/templates/computed.yaml | 13 +++ test/fixture/templates/multi.yaml | 15 +++ test/fixture/templates/static.yaml | 6 ++ test/fixture/values.yaml | 86 ++++++++++++++++ 13 files changed, 321 insertions(+), 2 deletions(-) create mode 100644 charts/nebari-app/Chart.yaml create mode 100644 charts/nebari-app/README.md create mode 100644 charts/nebari-app/templates/_nebari-app.tpl create mode 100644 test/fixture/Chart.yaml create mode 100644 test/fixture/templates/_helpers.tpl create mode 100644 test/fixture/templates/computed.yaml create mode 100644 test/fixture/templates/multi.yaml create mode 100644 test/fixture/templates/static.yaml create mode 100644 test/fixture/values.yaml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5c1c5e3..e8c522e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -200,3 +200,10 @@ jobs: chart-path: dist/chart chart-name: nebari-operator token: ${{ secrets.NEBARI_HELM_REPO_TOKEN }} + + - name: Sync library chart to helm-repository + uses: nebari-dev/helm-repository/.github/actions/sync-chart@main + with: + chart-path: charts/nebari-app + chart-name: nebari-app + token: ${{ secrets.NEBARI_HELM_REPO_TOKEN }} diff --git a/.github/workflows/test-chart.yml b/.github/workflows/test-chart.yml index 29417db..0b32dea 100644 --- a/.github/workflows/test-chart.yml +++ b/.github/workflows/test-chart.yml @@ -55,6 +55,9 @@ jobs: run: | helm lint ./dist/chart + - name: Test nebari-app library chart + run: make helm-test + # TODO: Uncomment if cert-manager is enabled # - name: Install cert-manager via Helm (wait for readiness) # run: | diff --git a/.gitignore b/.gitignore index 2df5417..72512af 100644 --- a/.gitignore +++ b/.gitignore @@ -68,3 +68,7 @@ landingpage /webapi *-coverage.out *-coverage.html + +# Helm dependency vendoring (repopulated by `helm dependency build`) +test/fixture/charts/ +test/fixture/Chart.lock diff --git a/Makefile b/Makefile index 2056fbc..1f1bcb3 100644 --- a/Makefile +++ b/Makefile @@ -186,7 +186,8 @@ helm-package: ## Package the Helm chart (run helm-chart first). @command -v helm >/dev/null 2>&1 || { echo >&2 "helm is required but not installed. See https://helm.sh/docs/intro/install/"; exit 1; } @if [ ! -d "dist/chart" ]; then echo "Error: dist/chart/ not found. Run 'make helm-chart' first."; exit 1; fi helm package dist/chart --destination dist/ - @echo "✅ Helm chart packaged in dist/" + helm package charts/nebari-app --destination dist/ + @echo "✅ Helm charts packaged in dist/" .PHONY: helm-chart-version helm-chart-version: ## Update Helm chart version and appVersion (requires VERSION and APP_VERSION vars). @@ -199,7 +200,31 @@ helm-chart-version: ## Update Helm chart version and appVersion (requires VERSIO sed -i.bak "s/^version:.*/version: $(VERSION)/" dist/chart/Chart.yaml sed -i.bak "s/^appVersion:.*/appVersion: \"$(APP_VERSION)\"/" dist/chart/Chart.yaml rm -f dist/chart/Chart.yaml.bak - @echo "✅ Updated chart version to $(VERSION) and appVersion to $(APP_VERSION)" + sed -i.bak "s/^version:.*/version: $(VERSION)/" charts/nebari-app/Chart.yaml + sed -i.bak "s/^appVersion:.*/appVersion: \"$(APP_VERSION)\"/" charts/nebari-app/Chart.yaml + rm -f charts/nebari-app/Chart.yaml.bak + @echo "✅ Updated chart versions to $(VERSION) and appVersion to $(APP_VERSION)" + +.PHONY: helm-lint +helm-lint: ## Lint the nebari-app library chart. + @command -v helm >/dev/null 2>&1 || { echo >&2 "helm is required but not installed. See https://helm.sh/docs/intro/install/"; exit 1; } + helm lint charts/nebari-app + +.PHONY: helm-test +helm-test: helm-lint ## Render the nebari-app fixture chart for all cases and verify required-field guards. + @command -v helm >/dev/null 2>&1 || { echo >&2 "helm is required but not installed. See https://helm.sh/docs/intro/install/"; exit 1; } + helm dependency build test/fixture >/dev/null + @for c in static computed multi; do \ + helm template t test/fixture --set cases.$$c.enabled=true >/dev/null \ + || { echo >&2 "case $$c failed to render"; exit 1; }; \ + echo " case $$c: ok"; \ + done + @for f in spec.hostname metadata.name metadata.namespace spec.service.name spec.service.port; do \ + helm template t test/fixture --set cases.static.enabled=true --set $$f= >/dev/null 2>&1 \ + && { echo >&2 "expected required failure for $$f"; exit 1; }; \ + echo " required guard $$f: ok"; \ + done + @echo "✅ nebari-app chart tests passed" .PHONY: generate-dev generate-dev: ## Generate for development (CRDs + deepcopy code only) diff --git a/charts/nebari-app/Chart.yaml b/charts/nebari-app/Chart.yaml new file mode 100644 index 0000000..205e017 --- /dev/null +++ b/charts/nebari-app/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v2 +name: nebari-app +description: Library chart providing a NebariApp custom resource template. +type: library +version: 0.1.0 +appVersion: "0.1.0" diff --git a/charts/nebari-app/README.md b/charts/nebari-app/README.md new file mode 100644 index 0000000..a98dec5 --- /dev/null +++ b/charts/nebari-app/README.md @@ -0,0 +1,107 @@ +# nebari-app + +A [library](https://helm.sh/docs/chart_template_guide/getting_started/#the-chart-yaml-file) Helm chart providing a reusable named template for rendering `NebariApp` custom resource instances. + +The chart exposes one named template, `nebari-app.nebariApp`, that acts as a pure function: callers pass `metadata` and `spec` dicts and the template renders a `NebariApp` resource. It is not installable on its own — it is consumed as a [chart dependency](https://helm.sh/docs/helm/helm_dependency/). + +## Install as a dependency + +Add the chart to your consumer chart's `Chart.yaml`: + +```yaml +dependencies: + - name: nebari-app + version: 0.1.0 + repository: https://nebari.dev/charts +``` + +or via a local file path during development: + +```yaml +dependencies: + - name: nebari-app + version: 0.1.0 + repository: file://../nebari-app +``` + +then run `helm dependency build`. + +## Usage + +The template takes a dict with `metadata` and `spec` keys: + +```yaml +{{ include "nebari-app.nebariApp" (dict + "metadata" (dict + "name" .Release.Name + "namespace" .Release.Namespace + "labels" (dict "app.kubernetes.io/name" .Chart.Name) + ) + "spec" .Values.nebariApp +) }} +``` + +### Required fields + +The template enforces these required fields via Helm's `required` function (render aborts if any is missing or empty): + +- `metadata.name` +- `metadata.namespace` +- `spec.hostname` +- `spec.service.name` +- `spec.service.port` + +All other validation (the rest of the `NebariAppSpec` schema) happens API-server-side at apply time. Pipe `helm template` output through `kubectl --dry-run=client -f -` in CI to catch schema errors before deployment. + +### Dynamic service values + +When `spec.service.name` is derived (e.g. from a component name) rather than static, use `mergeOverwrite` to layer computed values under the consumer's static values: + +```yaml +{{ include "nebari-app.nebariApp" (dict + "metadata" (dict + "name" "frontend" + "namespace" .Release.Namespace + ) + "spec" (mergeOverwrite (dict + "service" (dict + "name" "frontend" + "port" .Values.frontend.service.port + ) + ) .Values.frontend.nebariApp) +) }} +``` + +`mergeOverwrite` gives precedence to its **second argument**, so consumer values override the computed defaults on overlapping keys; the computed `service.name`/`service.port` fill in the gaps. + +### Multiple NebariApps from one chart + +To avoid repeating `metadata` construction at every call site, define a thin consumer-owned wrapper template that builds `metadata` from `top` and `component` and forwards `spec`: + +```yaml +{{ define "mychart.nebariApp" -}} +{{- $top := .top -}} +{{- $component := .component -}} +{{- include "nebari-app.nebariApp" (dict + "metadata" (dict + "name" $component + "namespace" $top.Release.Namespace + ) + "spec" .spec +) -}} +{{- end }} +``` + +Then each call site passes only `top`, `component`, and `spec`: + +```yaml +{{ include "mychart.nebariApp" (dict "top" . "component" "frontend" "spec" .Values.frontend.nebariApp) }} +``` + +## Namespace requirement + +The Nebari operator only reconciles `NebariApp` resources whose target namespace carries the label `nebari.dev/managed=true`. This chart does not template that label — ensure the namespace is opted in separately (e.g. via a `Namespace` resource in your consumer chart). + +## Field reference + +The canonical reference for all `spec` fields is [docs/api-reference.md](../../docs/api-reference.md), auto-generated from the Go types. See also [docs/configuration-reference.md](../../docs/configuration-reference.md) for examples and usage guidance. diff --git a/charts/nebari-app/templates/_nebari-app.tpl b/charts/nebari-app/templates/_nebari-app.tpl new file mode 100644 index 0000000..847eb81 --- /dev/null +++ b/charts/nebari-app/templates/_nebari-app.tpl @@ -0,0 +1,13 @@ +{{- define "nebari-app.nebariApp" -}} +{{- $_ := required "metadata.name is required" .metadata.name -}} +{{- $_ := required "metadata.namespace is required" .metadata.namespace -}} +{{- $_ := required "spec.hostname is required" .spec.hostname -}} +{{- $_ := required "spec.service.name is required" .spec.service.name -}} +{{- $_ := required "spec.service.port is required" .spec.service.port -}} +apiVersion: reconcilers.nebari.dev/v1 +kind: NebariApp +metadata: + {{- toYaml .metadata | nindent 2 }} +spec: + {{- toYaml .spec | nindent 2 }} +{{- end -}} diff --git a/test/fixture/Chart.yaml b/test/fixture/Chart.yaml new file mode 100644 index 0000000..797f4c4 --- /dev/null +++ b/test/fixture/Chart.yaml @@ -0,0 +1,10 @@ +apiVersion: v2 +name: fixture +description: Test fixture consuming the nebari-app library chart. +type: application +version: 0.1.0 +appVersion: "0.1.0" +dependencies: + - name: nebari-app + version: 0.1.0 + repository: file://../../charts/nebari-app diff --git a/test/fixture/templates/_helpers.tpl b/test/fixture/templates/_helpers.tpl new file mode 100644 index 0000000..fcf1d46 --- /dev/null +++ b/test/fixture/templates/_helpers.tpl @@ -0,0 +1,24 @@ +{{/* fixture.component-name — deterministic name for a component. */}} +{{- define "fixture.component-name" -}} +{{- .component -}} +{{- end -}} + +{{/* fixture.labels — minimal label set for a component. */}} +{{- define "fixture.labels" -}} +app.kubernetes.io/name: {{ .component }} +app.kubernetes.io/instance: {{ .top.Release.Name }} +{{- end -}} + +{{/* fixture.nebariApp — wrapper building metadata and forwarding spec. */}} +{{- define "fixture.nebariApp" -}} +{{- $top := .top -}} +{{- $component := .component -}} +{{- include "nebari-app.nebariApp" (dict + "metadata" (dict + "name" (include "fixture.component-name" (dict "top" $top "component" $component)) + "namespace" $top.Release.Namespace + "labels" (include "fixture.labels" (dict "top" $top "component" $component) | fromYaml) + ) + "spec" .spec +) -}} +{{- end -}} diff --git a/test/fixture/templates/computed.yaml b/test/fixture/templates/computed.yaml new file mode 100644 index 0000000..fb011d2 --- /dev/null +++ b/test/fixture/templates/computed.yaml @@ -0,0 +1,13 @@ +{{- if .Values.cases.computed.enabled }} +{{- $component := .Values.cases.computed.component }} +{{- include "fixture.nebariApp" (dict + "top" . + "component" $component + "spec" (mergeOverwrite (dict + "service" (dict + "name" (include "fixture.component-name" (dict "top" . "component" $component)) + "port" .Values.cases.computed.servicePort + ) + ) .Values.cases.computed.nebariApp) +) }} +{{- end }} diff --git a/test/fixture/templates/multi.yaml b/test/fixture/templates/multi.yaml new file mode 100644 index 0000000..4cfbec5 --- /dev/null +++ b/test/fixture/templates/multi.yaml @@ -0,0 +1,15 @@ +{{- if .Values.cases.multi.enabled }} +{{- range $app := .Values.cases.multi.apps }} +{{- include "fixture.nebariApp" (dict + "top" $ + "component" $app.component + "spec" (mergeOverwrite (dict + "service" (dict + "name" (include "fixture.component-name" (dict "top" $ "component" $app.component)) + "port" $app.servicePort + ) + ) $app.nebariApp) +) }} +--- +{{- end }} +{{- end }} diff --git a/test/fixture/templates/static.yaml b/test/fixture/templates/static.yaml new file mode 100644 index 0000000..f2f980e --- /dev/null +++ b/test/fixture/templates/static.yaml @@ -0,0 +1,6 @@ +{{- if .Values.cases.static.enabled }} +{{ include "nebari-app.nebariApp" (dict + "metadata" .Values.metadata + "spec" .Values.spec +) }} +{{- end }} diff --git a/test/fixture/values.yaml b/test/fixture/values.yaml new file mode 100644 index 0000000..1b0cd4c --- /dev/null +++ b/test/fixture/values.yaml @@ -0,0 +1,86 @@ +cases: + static: + enabled: false + computed: + enabled: false + component: frontend + servicePort: 8080 + nebariApp: + hostname: computed.nebari.local + auth: + enabled: false + multi: + enabled: false + apps: + - component: frontend + servicePort: 80 + nebariApp: + hostname: chat.nebari.local + auth: + enabled: true + provider: keycloak + provisionClient: true + enforceAtGateway: false + redirectURI: /* + scopes: + - openid + - profile + - email + routing: + routes: + - pathPrefix: / + pathType: PathPrefix + landingPage: + enabled: true + displayName: Chat + description: Chat with AI models + category: AI + priority: 1 + healthCheck: + enabled: true + path: /health + - component: api + servicePort: 8000 + nebariApp: + hostname: api.nebari.local + routing: + routes: + - pathPrefix: / + pathType: PathPrefix + +# Static-case data (rendered when cases.static.enabled=true). +# Kept at top level so the required-field validation tests can omit a +# single field via `--set spec.hostname=` etc. +metadata: + name: static-app + namespace: default + labels: + app.kubernetes.io/name: static-app +spec: + hostname: static.nebari.local + service: + name: static-svc + port: 80 + auth: + enabled: true + provider: keycloak + provisionClient: true + enforceAtGateway: false + redirectURI: /* + scopes: + - openid + - profile + - email + routing: + routes: + - pathPrefix: / + pathType: PathPrefix + landingPage: + enabled: true + displayName: Static + description: Static fixture app + category: misc + priority: 1 + healthCheck: + enabled: true + path: /health From 79a0595c080721a35c42d244e36c12ed4e9fe03c Mon Sep 17 00:00:00 2001 From: Philip Meier Date: Thu, 2 Jul 2026 12:23:35 +0000 Subject: [PATCH 2/2] Move static-case values under cases.static, fix validation --set paths --- Makefile | 2 +- test/fixture/templates/static.yaml | 4 +- test/fixture/values.yaml | 70 ++++++++++++++---------------- 3 files changed, 36 insertions(+), 40 deletions(-) diff --git a/Makefile b/Makefile index 1f1bcb3..1da0994 100644 --- a/Makefile +++ b/Makefile @@ -220,7 +220,7 @@ helm-test: helm-lint ## Render the nebari-app fixture chart for all cases and ve echo " case $$c: ok"; \ done @for f in spec.hostname metadata.name metadata.namespace spec.service.name spec.service.port; do \ - helm template t test/fixture --set cases.static.enabled=true --set $$f= >/dev/null 2>&1 \ + helm template t test/fixture --set cases.static.enabled=true --set cases.static.$$f= >/dev/null 2>&1 \ && { echo >&2 "expected required failure for $$f"; exit 1; }; \ echo " required guard $$f: ok"; \ done diff --git a/test/fixture/templates/static.yaml b/test/fixture/templates/static.yaml index f2f980e..30c2e56 100644 --- a/test/fixture/templates/static.yaml +++ b/test/fixture/templates/static.yaml @@ -1,6 +1,6 @@ {{- if .Values.cases.static.enabled }} {{ include "nebari-app.nebariApp" (dict - "metadata" .Values.metadata - "spec" .Values.spec + "metadata" .Values.cases.static.metadata + "spec" .Values.cases.static.spec ) }} {{- end }} diff --git a/test/fixture/values.yaml b/test/fixture/values.yaml index 1b0cd4c..3baf016 100644 --- a/test/fixture/values.yaml +++ b/test/fixture/values.yaml @@ -1,6 +1,39 @@ cases: static: enabled: false + metadata: + name: static-app + namespace: default + labels: + app.kubernetes.io/name: static-app + spec: + hostname: static.nebari.local + service: + name: static-svc + port: 80 + auth: + enabled: true + provider: keycloak + provisionClient: true + enforceAtGateway: false + redirectURI: /* + scopes: + - openid + - profile + - email + routing: + routes: + - pathPrefix: / + pathType: PathPrefix + landingPage: + enabled: true + displayName: Static + description: Static fixture app + category: misc + priority: 1 + healthCheck: + enabled: true + path: /health computed: enabled: false component: frontend @@ -47,40 +80,3 @@ cases: routes: - pathPrefix: / pathType: PathPrefix - -# Static-case data (rendered when cases.static.enabled=true). -# Kept at top level so the required-field validation tests can omit a -# single field via `--set spec.hostname=` etc. -metadata: - name: static-app - namespace: default - labels: - app.kubernetes.io/name: static-app -spec: - hostname: static.nebari.local - service: - name: static-svc - port: 80 - auth: - enabled: true - provider: keycloak - provisionClient: true - enforceAtGateway: false - redirectURI: /* - scopes: - - openid - - profile - - email - routing: - routes: - - pathPrefix: / - pathType: PathPrefix - landingPage: - enabled: true - displayName: Static - description: Static fixture app - category: misc - priority: 1 - healthCheck: - enabled: true - path: /health