Skip to content
Draft
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
9 changes: 9 additions & 0 deletions .github/workflows/lint.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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 scripts/extract_helm_values.py \
examples/argocd-application.yaml | helm template test . -f - > /dev/null

- name: Lint Python
run: uvx ruff check config/
149 changes: 149 additions & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 2 additions & 0 deletions .helmignore
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,5 @@ docs/
tests/
*_test.yaml
.venv-unit/
# Examples (deploy recipes, not chart sources)
examples/
39 changes: 39 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
@@ -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":{}}}'
```
83 changes: 83 additions & 0 deletions examples/argocd-application.yaml
Original file line number Diff line number Diff line change
@@ -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 <release>-<chart>-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
# <namespace>-<NebariApp name>, which the chart derives as
# jupyterhub-<release>-<chart>.
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
21 changes: 21 additions & 0 deletions examples/nebari-values.yaml
Original file line number Diff line number Diff line change
@@ -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
16 changes: 16 additions & 0 deletions scripts/extract_helm_values.py
Original file line number Diff line number Diff line change
@@ -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 <application.yaml>
"""

import sys

import yaml

manifest = yaml.safe_load(open(sys.argv[1]))
sys.stdout.write(manifest["spec"]["source"]["helm"]["values"])
Loading