Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
3 changes: 3 additions & 0 deletions .github/workflows/test-chart.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
29 changes: 27 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand All @@ -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 cases.static.$$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)
Expand Down
6 changes: 6 additions & 0 deletions charts/nebari-app/Chart.yaml
Original file line number Diff line number Diff line change
@@ -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"
107 changes: 107 additions & 0 deletions charts/nebari-app/README.md
Original file line number Diff line number Diff line change
@@ -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.
13 changes: 13 additions & 0 deletions charts/nebari-app/templates/_nebari-app.tpl
Original file line number Diff line number Diff line change
@@ -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 -}}
10 changes: 10 additions & 0 deletions test/fixture/Chart.yaml
Original file line number Diff line number Diff line change
@@ -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
24 changes: 24 additions & 0 deletions test/fixture/templates/_helpers.tpl
Original file line number Diff line number Diff line change
@@ -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 -}}
13 changes: 13 additions & 0 deletions test/fixture/templates/computed.yaml
Original file line number Diff line number Diff line change
@@ -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 }}
15 changes: 15 additions & 0 deletions test/fixture/templates/multi.yaml
Original file line number Diff line number Diff line change
@@ -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 }}
6 changes: 6 additions & 0 deletions test/fixture/templates/static.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{{- if .Values.cases.static.enabled }}
{{ include "nebari-app.nebariApp" (dict
"metadata" .Values.cases.static.metadata
"spec" .Values.cases.static.spec
) }}
{{- end }}
82 changes: 82 additions & 0 deletions test/fixture/values.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
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
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