From b1dabed19cb34efa1a6d2a5afe90bfeb51185e45 Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Fri, 19 Jun 2026 14:55:34 +0100 Subject: [PATCH 1/4] docs: add example values and ArgoCD Application Add examples/ directory: nebari-values.yaml (full Nebari deploy needing only a keycloak.hostname edit) and argocd-application.yaml referencing the published chart, with the managedNamespaceMetadata label, five ignoreDifferences paths, and SkipDryRunOnMissingResource documented inline. Closes #127 --- examples/README.md | 39 +++++++++++++++ examples/argocd-application.yaml | 83 ++++++++++++++++++++++++++++++++ examples/nebari-values.yaml | 21 ++++++++ 3 files changed, 143 insertions(+) create mode 100644 examples/README.md create mode 100644 examples/argocd-application.yaml create mode 100644 examples/nebari-values.yaml diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..752cc91 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,39 @@ +# Examples + +Ready-to-use configurations for deploying the data science pack. + +| File | What it does | +|------|--------------| +| [`nebari-values.yaml`](nebari-values.yaml) | Full Nebari deployment. Edit `keycloak.hostname`, then `helm install`. | +| [`argocd-application.yaml`](argocd-application.yaml) | ArgoCD Application referencing the published chart. | + +## Helm + +```bash +helm install data-science-pack nebari/nebari-data-science-pack \ + -f examples/nebari-values.yaml +``` + +This assumes `nebari-operator` is running and an RWX `StorageClass` (longhorn +on NIC clusters) is available. See the comments in the file for prerequisites. + +## ArgoCD + +Edit the two hostnames in `argocd-application.yaml`, then: + +```bash +kubectl apply -f examples/argocd-application.yaml +``` + +The file documents inline why each non-default setting is needed +(`managedNamespaceMetadata`, the five `ignoreDifferences` paths, +`SkipDryRunOnMissingResource`). + +One operational note: the rbac-bootstrap job runs as a PostSync hook, and hook +results don't affect sync status. If it fails, the app sits `Synced` +indefinitely; the hook only re-runs on a new sync operation: + +```bash +kubectl -n argocd patch application data-science-pack \ + --type merge -p '{"operation":{"sync":{}}}' +``` diff --git a/examples/argocd-application.yaml b/examples/argocd-application.yaml new file mode 100644 index 0000000..c537d99 --- /dev/null +++ b/examples/argocd-application.yaml @@ -0,0 +1,83 @@ +# ArgoCD Application deploying the data science pack from the published chart. +# +# Edit the two hostnames below, apply with `kubectl apply -f`, or reference +# this file from an app-of-apps. The non-obvious settings each map to a +# deploy-time failure mode and are explained inline. + +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: data-science-pack + namespace: argocd +spec: + project: default + source: + # Central Nebari Helm repository. The chart lands in this index once its + # per-chart sync PR merges; until then it is installable as an OCI + # artifact: repoURL: oci://quay.io/nebari/charts (drop the `chart` field + # and put the chart name in the URL path). + repoURL: https://raw.githubusercontent.com/nebari-dev/helm-repository/gh-pages/ + chart: nebari-data-science-pack + targetRevision: 0.1.0 + helm: + # Release name is load-bearing: the chart mounts the operator-provisioned + # OIDC secret by its conventional name --oidc-client. + releaseName: data-science-pack + values: | + keycloak: + hostname: keycloak.example.com + sharedStorage: + storageClass: longhorn + size: 100Gi + nfsServer: + enabled: false + destination: + server: https://kubernetes.default.svc + # Namespace is load-bearing: the operator names the hub OIDC client + # -, which the chart derives as + # jupyterhub--. + namespace: jupyterhub + # z2jh regenerates three Secret token fields on every `helm template` (its + # lookup-based retention can't run under ArgoCD's repo-server), and two + # Deployment checksum annotations derive from them. Without all five paths + # plus RespectIgnoreDifferences=true, selfHeal restarts hub/proxy on every + # sync. Trade-off: config changes flowing through the hub Secret (e.g. + # custom.profiles) still sync but need a manual + # `kubectl -n jupyterhub rollout restart deployment/hub` to take effect. + ignoreDifferences: + - kind: Secret + name: hub + namespace: jupyterhub + jsonPointers: + - /data/hub.config.ConfigurableHTTPProxy.auth_token + - /data/hub.config.JupyterHub.cookie_secret + - /data/hub.config.CryptKeeper.keys + - group: apps + kind: Deployment + name: hub + namespace: jupyterhub + jsonPointers: + - /spec/template/metadata/annotations/checksum~1secret + - group: apps + kind: Deployment + name: proxy + namespace: jupyterhub + jsonPointers: + - /spec/template/metadata/annotations/checksum~1auth-token + syncPolicy: + # The operator only reconciles NebariApps in namespaces labeled + # nebari.dev/managed=true; without it the app reports NamespaceNotOptedIn, + # no OIDC client secret is provisioned, and the hub crash-loops. + managedNamespaceMetadata: + labels: + nebari.dev/managed: "true" + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + - ServerSideApply=true + - RespectIgnoreDifferences=true + # First sync would otherwise fail preflight when the NebariApp CRD + # (installed by the operator) isn't registered yet. + - SkipDryRunOnMissingResource=true diff --git a/examples/nebari-values.yaml b/examples/nebari-values.yaml new file mode 100644 index 0000000..fd45964 --- /dev/null +++ b/examples/nebari-values.yaml @@ -0,0 +1,21 @@ +# Full Nebari deployment of the data science pack. +# +# Deploys as-is after a SINGLE edit: set keycloak.hostname to your cluster's +# Keycloak FQDN. Everything else (hub/nebi hostnames, OIDC client provisioning, +# gateway routing, images, kernels) is derived from it by the chart and the +# nebari-operator. +# +# Prerequisites on the target cluster: +# * nebari-operator running (provisions the OIDC client + gateway HTTPRoute) +# * the install namespace labeled nebari.dev/managed=true +# * an RWX StorageClass for /shared group dirs (longhorn on NIC clusters) + +keycloak: + hostname: keycloak.example.com # <-- EDIT: your Keycloak FQDN + +# NIC-managed clusters ship longhorn (ReadWriteMany). Use it for the per-group +# shared directories instead of the transitional in-cluster NFS server. +sharedStorage: + storageClass: longhorn + nfsServer: + enabled: false From 78f6d47d733949f9cc897ddd948f699d5e52fa3e Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Fri, 19 Jun 2026 15:01:33 +0100 Subject: [PATCH 2/4] ci: render example values in lint workflow Render examples/nebari-values.yaml and the values embedded in examples/argocd-application.yaml through `helm template` so they can't drift from the chart schema. Add tools/extract_helm_values.py to pull the inline ArgoCD values, and helmignore examples/ so deploy recipes don't ship in the chart package. --- .github/workflows/lint.yaml | 9 +++++++++ .helmignore | 2 ++ 2 files changed, 11 insertions(+) diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 56cb0d5..b6a60cc 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -29,5 +29,14 @@ jobs: - name: Install uv uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0 + - name: Render examples + run: | + # Full-Nebari example renders through the chart. + helm template test . -f examples/nebari-values.yaml > /dev/null + # The values embedded in the ArgoCD Application must stay valid chart + # inputs; extract and render them too. + uv run --with PyYAML==6.0.2 tools/extract_helm_values.py \ + examples/argocd-application.yaml | helm template test . -f - > /dev/null + - name: Lint Python run: uvx ruff check config/ diff --git a/.helmignore b/.helmignore index 0925765..f86810f 100644 --- a/.helmignore +++ b/.helmignore @@ -42,3 +42,5 @@ docs/ tests/ *_test.yaml .venv-unit/ +# Examples (deploy recipes, not chart sources) +examples/ From 03426191bbf6b4d6d17b84135b6bc89266eb8f9c Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Fri, 19 Jun 2026 15:02:01 +0100 Subject: [PATCH 3/4] ci: move extract_helm_values to scripts/ (tools/ is gitignored) --- .github/workflows/lint.yaml | 2 +- scripts/extract_helm_values.py | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 scripts/extract_helm_values.py diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index b6a60cc..0c04847 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -35,7 +35,7 @@ jobs: helm template test . -f examples/nebari-values.yaml > /dev/null # The values embedded in the ArgoCD Application must stay valid chart # inputs; extract and render them too. - uv run --with PyYAML==6.0.2 tools/extract_helm_values.py \ + uv run --with PyYAML==6.0.2 scripts/extract_helm_values.py \ examples/argocd-application.yaml | helm template test . -f - > /dev/null - name: Lint Python diff --git a/scripts/extract_helm_values.py b/scripts/extract_helm_values.py new file mode 100644 index 0000000..bee90bc --- /dev/null +++ b/scripts/extract_helm_values.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python3 +"""Print the inline Helm values from an ArgoCD Application manifest to stdout. + +CI pipes the output into `helm template -f -` so the values embedded in +examples/argocd-application.yaml are rendered through the chart and can't +silently drift from its schema. + +Usage: extract_helm_values.py +""" + +import sys + +import yaml + +manifest = yaml.safe_load(open(sys.argv[1])) +sys.stdout.write(manifest["spec"]["source"]["helm"]["values"]) From ff4c3b458819399750dec1360c5bb3fb4c246bcf Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Fri, 19 Jun 2026 15:12:44 +0100 Subject: [PATCH 4/4] ci: deploy example values on a NIC sandbox and assert Healthy Add a deploy-example job that bootstraps the full NIC platform stack (operator + Keycloak + gateway) with action-nebari-sandbox, registers the chart with examples/nebari-values.yaml through add-software-pack, and blocks until the ArgoCD Application reaches Healthy. Exercises the example's Nebari path end to end: NebariApp CRD reconcile, operator OIDC client provisioning, and hub boot with Keycloak OAuth. Shared storage is disabled here (k3d has no RWX class; the kind-based e2e job covers it). --- .github/workflows/test.yaml | 149 ++++++++++++++++++++++++++++++++++++ 1 file changed, 149 insertions(+) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index b47f7c2..2e5b68e 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -230,3 +230,152 @@ jobs: kubectl -n keycloak logs statefulset/keycloak-keycloakx --tail=200 || true echo "--- port-forward log ---" cat /tmp/kc-pf.log || true + + # Deploy examples/nebari-values.yaml onto a real NIC platform sandbox + # (operator + Keycloak + gateway) via ArgoCD and assert the resulting + # Application reaches Healthy. This is the only job that exercises the + # full Nebari path the example targets: NebariApp CRD -> operator + # provisions the OIDC client -> hub boots with Keycloak OAuth. + # + # Two sandbox-specific overlays on top of the example, both legitimate + # per-cluster knobs the example documents: + # * keycloak.hostname -> the sandbox's issuer host (the example's + # "edit the hostname" step). + # * sharedStorage.enabled=false -> k3d ships only local-path (RWO); the + # example's longhorn (RWX) isn't present. Shared storage is covered by + # the kind-based e2e job above. + deploy-example: + name: deploy / example on NIC sandbox + runs-on: ubuntu-24.04 + timeout-minutes: 30 + steps: + - name: Checkout + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + + - name: Install Helm + run: curl -fsSL https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash + + # Vendor the jupyterhub subchart and package a clean chart dir (helm + # package applies .helmignore, dropping tests/, examples/, .git/), so + # ArgoCD's file:// repo renders offline without a nested .git. The + # example values file is helmignored out of the package, so copy it + # back to the chart root where the Application's valueFiles expects it. + - name: Stage chart + example values + run: | + helm dependency update + helm package . -d /tmp/pkg + mkdir -p /tmp/stage + tar -xzf /tmp/pkg/*.tgz -C /tmp/stage + cp examples/nebari-values.yaml /tmp/stage/nebari-data-science-pack/nebari-values.yaml + + - name: Write ArgoCD Application for the sandbox + run: | + # Mirrors examples/argocd-application.yaml (releaseName, + # ignoreDifferences, syncPolicy) but points the source at the + # sandbox's file:// gitops repo and overlays the two per-cluster + # values. ${GITOPS_DIR} is expanded by add-software-pack's envsubst. + cat > /tmp/application.yaml <<'EOF' + apiVersion: argoproj.io/v1alpha1 + kind: Application + metadata: + name: data-science-pack + namespace: argocd + spec: + project: default + source: + repoURL: "file://${GITOPS_DIR}" + targetRevision: HEAD + path: data-science-pack + helm: + releaseName: data-science-pack + valueFiles: + - nebari-values.yaml + values: | + keycloak: + hostname: keycloak.nebari.local + sharedStorage: + enabled: false + jupyterhub: + prePuller: + hook: + enabled: false + continuous: + enabled: false + destination: + server: https://kubernetes.default.svc + namespace: jupyterhub + ignoreDifferences: + - kind: Secret + name: hub + namespace: jupyterhub + jsonPointers: + - /data/hub.config.ConfigurableHTTPProxy.auth_token + - /data/hub.config.JupyterHub.cookie_secret + - /data/hub.config.CryptKeeper.keys + - group: apps + kind: Deployment + name: hub + namespace: jupyterhub + jsonPointers: + - /spec/template/metadata/annotations/checksum~1secret + - group: apps + kind: Deployment + name: proxy + namespace: jupyterhub + jsonPointers: + - /spec/template/metadata/annotations/checksum~1auth-token + syncPolicy: + managedNamespaceMetadata: + labels: + nebari.dev/managed: "true" + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + - ServerSideApply=true + - RespectIgnoreDifferences=true + - SkipDryRunOnMissingResource=true + EOF + + - name: Provision Nebari sandbox (k3d + NIC platform stack) + id: sandbox + uses: nebari-dev/action-nebari-sandbox@09f542cdbcb0ed74307f86fee4f8ad0053a4cd8f # v2.0.2 + with: + profile: platform + cluster-name: deploy-example + + - name: Surface KUBECONFIG for the sub-action's wait + run: echo "KUBECONFIG=${{ steps.sandbox.outputs.kubeconfig }}" >> "${GITHUB_ENV}" + + - name: Deploy chart with example values via ArgoCD + uses: nebari-dev/action-nebari-sandbox/add-software-pack@09f542cdbcb0ed74307f86fee4f8ad0053a4cd8f # v2.0.2 + with: + gitops-dir: ${{ steps.sandbox.outputs.gitops-dir }} + app-name: data-science-pack + chart-source: /tmp/stage/nebari-data-science-pack + application-manifest: /tmp/application.yaml + wait-healthy: "true" + wait-timeout: "10m" + + - name: Verify hub is running + env: + KUBECONFIG: ${{ steps.sandbox.outputs.kubeconfig }} + run: | + kubectl -n jupyterhub rollout status deployment/hub --timeout=120s + kubectl -n jupyterhub get pods + + - name: Debug on failure + if: failure() + env: + KUBECONFIG: ${{ steps.sandbox.outputs.kubeconfig }} + run: | + kubectl get applications -n argocd -o wide || true + kubectl get application/data-science-pack -n argocd -o yaml || true + kubectl -n jupyterhub get pods || true + kubectl -n jupyterhub get events --sort-by=.lastTimestamp || true + kubectl -n jupyterhub logs -l component=hub --tail=200 || true + + - name: Cleanup + if: always() + run: k3d cluster delete deploy-example 2>/dev/null || true