diff --git a/.github/workflows/docs-values-check.yml b/.github/workflows/docs-values-check.yml new file mode 100644 index 0000000..435d7a8 --- /dev/null +++ b/.github/workflows/docs-values-check.yml @@ -0,0 +1,33 @@ +name: Check values.md is up to date + +on: + pull_request: + paths: + - 'values.yaml' + - 'docs/docs/deployment/values.md' + - 'docs/helm-docs.gotmpl' + +jobs: + check-values-docs: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install helm-docs + run: | + wget -q -O helm-docs.tar.gz \ + https://github.com/norwoodj/helm-docs/releases/download/v1.14.2/helm-docs_1.14.2_Linux_x86_64.tar.gz + tar -xzf helm-docs.tar.gz helm-docs + sudo mv helm-docs /usr/local/bin/helm-docs + + - name: Regenerate values.md + run: | + helm-docs \ + --chart-search-root=. \ + --template-files=docs/values.md.gotmpl \ + --output-file=docs/docs/deployment/values.md + + - name: Fail if values.md is out of sync + run: | + git diff --exit-code docs/docs/deployment/values.md || \ + (echo "❌ values.md is out of sync. Run 'make generate-docs' and commit the result." && exit 1) diff --git a/.helmignore b/.helmignore new file mode 100644 index 0000000..4e43fd7 --- /dev/null +++ b/.helmignore @@ -0,0 +1,26 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ + +# Docs site (not part of the Helm chart) +docs/ diff --git a/Makefile b/Makefile index 7e01692..1bc3f24 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ # Minimal Makefile for local development # Only two commands needed - Tilt handles everything else via its UI -.PHONY: up down +.PHONY: up down generate-docs # Start local development environment # - ctlptl apply is idempotent (creates cluster only if not exists) @@ -15,3 +15,7 @@ down: -tilt down -pkill -f "tilt up" 2>/dev/null || true ctlptl delete -f ctlptl-config.yaml + +# Generate docs/docs/deployment/values.md from annotated values.yaml +generate-docs: + yarn --cwd docs generate diff --git a/docs/docs/deployment/deploy.md b/docs/docs/deployment/deploy.md new file mode 100644 index 0000000..35626bd --- /dev/null +++ b/docs/docs/deployment/deploy.md @@ -0,0 +1,253 @@ +--- +title: Deploy the pack +description: Step-by-step instructions for deploying the Nebari Nebi Pack. +sidebar_position: 1 +--- + +# Deploy the pack + +This guide is for **operators** installing the pack on a Kubernetes +cluster. End users connecting to an already-deployed cluster should read +[Use the pack](../user-guide/use) instead. + +:::note[Defaults reflect chart v0.1.0-alpha.4] + +Image tags and other defaults cited below match +[`Chart.yaml`](https://github.com/nebari-dev/nebari-nebi-pack/blob/main/Chart.yaml) +and [`values.yaml`](https://github.com/nebari-dev/nebari-nebi-pack/blob/main/values.yaml) +on `main`. Check the repo for the latest pinned versions before copying +examples verbatim. + +::: + +The pack deploys: + +- **Nebi server** — a single-pod Deployment running `--mode=both` (API + background worker combined). Image: `quay.io/nebari/nebi`. Listens on port 8460. +- **PostgreSQL 16** — an embedded StatefulSet for application state and workspace metadata. 10 Gi persistent storage. +- **ClusterIP Service** — exposes the Nebi server on port 80 within the cluster. +- **Environments PVC** — 20 Gi RWO volume mounted at `/app/data/environments` for pixi workspace files. +- **Secret** — JWT signing key and PostgreSQL password, created once by a PreSync Job and never overwritten on upgrade. +- **NebariApp** *(Nebari clusters only)* — configures Envoy Gateway routing and provisions a Keycloak OIDC client automatically. +- **ServiceAccount** — bound to the Nebi Deployment. + +## Prerequisites + +| Requirement | Details | +|---|---| +| Kubernetes ≥ 1.27 | Any CNCF-conformant cluster; [k3d](https://k3d.io/) works for local dev | +| Helm ≥ 3.12 | | +| `nebari-operator` | Required when `nebariapp.enabled: true` (Nebari clusters only) | +| Persistent storage | Default StorageClass with RWO support; ~30 Gi total | +| ArgoCD | Required for the GitOps path only | + +## Standalone install (no Nebari) + +Use this path for local dev or clusters without `nebari-operator`. Skips +the NebariApp routing layer entirely. You are responsible for exposing +the service and handling authentication. + +### From source + +```bash +git clone https://github.com/nebari-dev/nebari-nebi-pack.git +cd nebari-nebi-pack + +helm install nebi-pack . \ + --namespace nebi \ + --create-namespace \ + --set nebariapp.enabled=false +``` + +Expose the service with an Ingress: + +```yaml +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: nebi + namespace: nebi +spec: + rules: + - host: nebi.your-cluster.example.com + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: nebi-pack-nebari-nebi-pack + port: + number: 80 +``` + +### Local dev loop (Tilt + k3d) + +The [repo](https://github.com/nebari-dev/nebari-nebi-pack) ships a Tiltfile and `ctlptl-config.yaml` for an end-to-end +local dev loop. Prerequisites: +[Docker](https://docs.docker.com/get-docker/), +[ctlptl](https://github.com/tilt-dev/ctlptl), +[Tilt](https://docs.tilt.dev/install.html). + +```bash +make up # creates the k3d cluster and starts Tilt +# Tilt UI: http://localhost:10350 +# Nebi UI: http://localhost:8460 (Tilt auto-forwards this port) +make down # tear down cluster and Tilt +``` + +:::note +In the local dev loop, Tilt uses `nebi` as the Helm release name, so +resources are named `nebi-nebari-nebi-pack` (not `nebi-pack-nebari-nebi-pack`). +The `kubectl` commands in [Verifying the deployment](#verifying-the-deployment) +use the `nebi-pack` release name from the Helm/ArgoCD paths — adjust +accordingly if you are inspecting a Tilt-managed deployment. +::: + + + +## Nebari install (ArgoCD + GitOps) + +The recommended production deployment. The chart creates a `NebariApp` +resource that `nebari-operator` picks up to provision routing, TLS, and +Keycloak OIDC. + +Save the following to `apps/nebi-pack.yaml` in your GitOps repo: + +```yaml +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: nebi-pack + namespace: argocd + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + project: default + source: + repoURL: https://github.com/nebari-dev/nebari-nebi-pack.git + path: . + targetRevision: main + helm: + releaseName: nebi-pack + values: | + nebariapp: + hostname: nebi.your-cluster.example.com + destination: + server: https://kubernetes.default.svc + namespace: nebi + syncPolicy: + automated: + prune: true + selfHeal: true + managedNamespaceMetadata: + labels: + nebari.dev/managed: "true" + syncOptions: + - CreateNamespace=true + - ServerSideApply=true + - SkipDryRunOnMissingResource=true + retry: + limit: 5 + backoff: + duration: 5s + factor: 2 + maxDuration: 3m +``` + +:::warning[`nebari.dev/managed: "true"` is required] + +The `managedNamespaceMetadata` block applies the `nebari.dev/managed` +label to the namespace. **Without this label the nebari-operator will +silently ignore your NebariApp resource** — the hostname will return 404 +and `kubectl describe nebariapp` will show no progress on conditions. + +::: + +**What happens on first sync:** + +1. The PreSync Job runs before any other resources are applied. It creates a Kubernetes Secret containing a random JWT signing key and PostgreSQL password — only if the secret does not already exist. +2. The PostgreSQL StatefulSet starts and waits to become healthy. +3. The Nebi Deployment starts, connects to PostgreSQL, and begins serving on port 8460. +4. `nebari-operator` reconciles the `NebariApp` CRD, provisions a Keycloak OIDC client, and configures Envoy Gateway to enforce authentication. + +The chart's PreSync hook handles secret creation idempotently — no manual +bootstrapping required. + +## Configuration + +Configuring the chart beyond the defaults — auth groups, OIDC, storage +size, resource limits, the landing page card — is covered in the +[values.yaml reference](./values.md). + +## Verifying the deployment + +Check that all pods are running: + +```bash +kubectl get pods -n nebi +``` + +Expected output: + +``` +NAME READY STATUS RESTARTS AGE +nebi-pack-nebari-nebi-pack- 1/1 Running 0 2m +nebi-pack-nebari-nebi-pack-postgres-0 1/1 Running 0 2m +``` + +Check the health endpoint: + +```bash +kubectl run nebi-probe --rm -it --image=curlimages/curl --restart=Never -- \ + curl -sf http://nebi-pack-nebari-nebi-pack.nebi.svc.cluster.local/api/v1/health +``` + +Expected: `{"status":"healthy"}` with HTTP 200. + +If `nebariapp.enabled`, check the NebariApp conditions: + +```bash +kubectl get nebariapp -n nebi +kubectl describe nebariapp -n nebi +``` + +You want `RoutingReady`, `TLSReady`, and `AuthReady` all `True`. + +## Upgrade + +```bash +helm upgrade nebi-pack . \ + --namespace nebi \ + --reuse-values \ + --set image.tag=sha- +``` + +The PreSync Job is idempotent — it skips secret creation if the secret +already exists. + +## Uninstall + +```bash +helm uninstall nebi-pack --namespace nebi +``` + +:::warning PVC retention +`helm uninstall` does **not** delete PersistentVolumeClaims. Environment +and database data is preserved. To permanently delete all data: + +```bash +kubectl delete pvc -n nebi -l app.kubernetes.io/instance=nebi-pack +``` +::: + +## Operator troubleshooting + +Recovery steps for common failures — pods not starting, NebariApp stuck +on conditions, secret bootstrap errors — live on the +[Troubleshoot](./troubleshoot.md) page. + +## Next steps + +- **End users** → [Use the pack](../user-guide/use.md) — log in and manage environments. +- **Full chart reference** → [values.yaml reference](./values.md) — every option with type, default, and description. +- **Upstream docs** → [Nebi](https://github.com/nebari-dev/nebi), [Pixi](https://pixi.sh). diff --git a/docs/docs/deployment/index.md b/docs/docs/deployment/index.md new file mode 100644 index 0000000..0ce3b03 --- /dev/null +++ b/docs/docs/deployment/index.md @@ -0,0 +1,23 @@ +--- +title: Deployment +description: Install and configure the Nebari Nebi Pack on a Kubernetes or Nebari cluster. +sidebar_position: 1 +--- + +import DocCardList from '@theme/DocCardList'; + +# Deployment + +The Nebi Pack is a Helm chart that deploys [Nebi](https://github.com/nebari-dev/nebi) — a team environment management server built on [Pixi](https://pixi.sh) — on a Nebari or plain Kubernetes cluster. This section covers everything an operator needs to install, configure, and maintain the pack. + +## Prerequisites + +| Requirement | Notes | +|---|---| +| Kubernetes ≥ 1.27 | Any CNCF-conformant cluster | +| Helm ≥ 3.12 | | +| `nebari-operator` | Required when `nebariapp.enabled: true` (Nebari clusters only) | +| A dedicated namespace | Recommended: `nebi` | +| Persistent storage | Default StorageClass with RWO support; ~30 Gi total (20 Gi environments + 10 Gi PostgreSQL) | + + diff --git a/docs/docs/deployment/troubleshoot.md b/docs/docs/deployment/troubleshoot.md new file mode 100644 index 0000000..28d18b6 --- /dev/null +++ b/docs/docs/deployment/troubleshoot.md @@ -0,0 +1,357 @@ +--- +title: Troubleshooting +description: Solutions to common issues with the Nebari Nebi Pack. +sidebar_position: 5 +--- + +# Troubleshooting + +This page covers the Nebari/ArgoCD production deployment path. + +## Diagnostic flow + +Run these in order to locate the failure. + +**1. Check pod status:** + +```bash +kubectl get pods -n nebi +``` + +All pods should be `Running` with `READY 1/1`. Common problem states: + +| State | Where to look next | +|---|---| +| `Pending` | [Pods stuck in Pending](#pods-stuck-in-pending) | +| `CrashLoopBackOff` | [Nebi pod CrashLoopBackOff](#nebi-pod-crashloopbackoff) | +| No pods at all | [PreSync Job failed](#presync-job-failed) | + +**2. Check events on a problem pod:** + +```bash +kubectl describe pod -n nebi +``` + +Scroll to the `Events` section at the bottom for the immediate cause. + +**3. Check logs:** + +```bash +# Nebi server +kubectl logs -n nebi deployment/nebi-pack-nebari-nebi-pack --tail=100 + +# PostgreSQL +kubectl logs -n nebi nebi-pack-nebari-nebi-pack-postgres-0 --tail=100 +``` + +**4. Check NebariApp conditions:** + +```bash +kubectl describe nebariapp -n nebi +``` + +Look for `RoutingReady`, `TLSReady`, and `AuthReady`. Any `False` condition will have a message explaining why. + +**5. Check the ArgoCD sync status:** + +In the ArgoCD UI, check the `nebi-pack` Application for sync errors. If the PreSync hook failed, the Job will still be present in the namespace (it self-deletes only on success): + +```bash +kubectl get jobs -n nebi +``` + +If the Job is still present after a sync, see [PreSync Job failed](#presync-job-failed). + +--- + +## PreSync Job failed + +**Symptom:** No pods exist in the `nebi` namespace after an ArgoCD sync. `kubectl get jobs -n nebi` shows `nebi-pack-nebari-nebi-pack-secret-init` still present. + +**Cause:** The PreSync Job creates the JWT signing key and PostgreSQL password Secret before any other resources are applied. If it fails, the Secret is never created and all pods fail to start. + +**Fix:** + +Check the Job logs: + +```bash +kubectl logs -n nebi job/nebi-pack-nebari-nebi-pack-secret-init +``` + +Common causes: + +- **Image pull failure** — the Job uses `bitnami/kubectl:latest`. If the node can't reach the registry, check image pull events on the Job pod with `kubectl describe pod -n nebi -l job-name=nebi-pack-nebari-nebi-pack-secret-init`. +- **RBAC error** — the Job's ServiceAccount needs `get` and `create` on Secrets in the `nebi` namespace. Verify the Role and RoleBinding were applied: + ```bash + kubectl get role,rolebinding -n nebi | grep secret-init + ``` +- **Namespace not created** — if the `nebi` namespace didn't exist before the hook ran, ensure `CreateNamespace=true` is present in syncOptions (already set in the recommended Application manifest in [Deploy the pack](./deploy.md)). + +After fixing the root cause, re-sync the ArgoCD Application. The Job will re-run and create the Secret if it does not already exist. + +--- + +## Pods stuck in Pending + +**Symptom:** `kubectl get pods -n nebi` shows one or both pods in `Pending` indefinitely. + +**Cause:** A PersistentVolumeClaim cannot bind. The Nebi pod requires a 20 Gi RWO environments PVC; PostgreSQL requires a 10 Gi RWO data PVC. Both need a StorageClass that supports `ReadWriteOnce`. + +**Fix:** + +Check PVC status: + +```bash +kubectl get pvc -n nebi +kubectl describe pvc -n nebi +``` + +The `Events` section will say why binding failed (e.g. `no persistent volumes available`, `storageclass not found`). + +List available StorageClasses to find the right name: + +```bash +kubectl get storageclasses +``` + +Then set the StorageClass explicitly in your ArgoCD Application values and re-sync: + +```yaml +helm: + values: | + persistence: + storageClassName: your-storage-class + postgres: + storage: + storageClassName: your-storage-class +``` + +--- + +## Nebi pod CrashLoopBackOff + +**Symptom:** `nebi-pack-nebari-nebi-pack-` cycles through `CrashLoopBackOff`. The PostgreSQL pod is `Running`. + +**Cause:** Most commonly the Secret doesn't exist (PreSync Job failed on a previous sync), or the pod started before PostgreSQL finished initializing. + +**Fix:** + +Read the logs from the previous (crashed) container: + +```bash +kubectl logs -n nebi deployment/nebi-pack-nebari-nebi-pack --previous +``` + +- **Secret missing** — logs will show a `secret not found` or missing key error. Verify the Secret exists: + ```bash + kubectl get secret nebi-pack-nebari-nebi-pack -n nebi + ``` + If it doesn't, see [PreSync Job failed](#presync-job-failed). + +- **Database connection refused** — logs will show a `connection refused` or `dial tcp` error pointing at the PostgreSQL address. PostgreSQL may still be initializing on first install. Once the postgres pod is `Ready`, restart the Nebi Deployment: + ```bash + kubectl rollout restart deployment/nebi-pack-nebari-nebi-pack -n nebi + ``` + +--- + +## NebariApp not progressing + +**Symptom:** `kubectl describe nebariapp -n nebi` shows `RoutingReady`, `TLSReady`, or `AuthReady` stuck at `False` or `Unknown`. The hostname returns 404. + +**Cause A — Missing namespace label:** `nebari-operator` silently ignores NebariApp resources in namespaces that don't have the `nebari.dev/managed: "true"` label. + +Check the label: + +```bash +kubectl get ns nebi --show-labels +``` + +If `nebari.dev/managed=true` is absent, the `managedNamespaceMetadata` block is missing from your ArgoCD Application manifest. Add it and re-sync: + +```yaml +syncPolicy: + managedNamespaceMetadata: + labels: + nebari.dev/managed: "true" +``` + +ArgoCD will apply the label to the namespace and `nebari-operator` will begin reconciling. + +**Cause B — `nebari-operator` not installed:** + +```bash +kubectl get pods -n nebari-operator-system +``` + +If the namespace doesn't exist or the operator pod is not running, install `nebari-operator` before syncing the nebi-pack Application. + +--- + +## ArgoCD sync failing + +**Symptom:** ArgoCD reports a sync error containing `no matches for kind "NebariApp"` or a dry-run validation failure referencing the NebariApp resource. + +**Cause:** `SkipDryRunOnMissingResource=true` is missing from syncOptions. ArgoCD performs a server-side dry-run before applying resources. If the `NebariApp` CRD isn't installed yet (e.g. `nebari-operator` hasn't synced first), the dry-run rejects the manifest and the sync fails. + +**Fix:** + +Add `SkipDryRunOnMissingResource=true` to the Application's `syncOptions` (already included in the recommended manifest in [Deploy the pack](./deploy.md)): + +```yaml +syncOptions: + - CreateNamespace=true + - ServerSideApply=true + - SkipDryRunOnMissingResource=true +``` + +Re-sync the Application after updating. + +--- + +## Users have no admin access + +**Symptom:** A user can authenticate but cannot perform admin actions in Nebi. + +**Cause:** The user's Keycloak group is not listed in `auth.proxyAdminGroups`. The default value is `admin,nebi-admin`. + +**Fix:** + +Check the user's group membership in Keycloak, then add the group to `auth.proxyAdminGroups` in your ArgoCD Application values: + +```yaml +helm: + values: | + auth: + proxyAdminGroups: "admin,nebi-admin,your-group" +``` + +Commit the change and ArgoCD will roll out an updated Deployment. + +--- + +## JupyterLab / data-science-pack integration + +These issues apply when the nebi-pack and data-science-pack are deployed together. See [Deploy with the data-science-pack](./with-data-science-pack.md) for the expected configuration. + +A quick end-to-end check — spawn a JupyterLab server and open a terminal: + +```bash +nebi whoami # should return your username without prompting for login +nebi workspace list # should list workspaces from the nebi server +``` + +If either command fails, work through the sections below. + +### `nebi whoami` fails with an auth error + +**Symptom:** `nebi whoami` returns an authentication error, or `NEBI_AUTH_TOKEN` is empty inside the pod. + +**Cause:** Token exchange between JupyterHub and Nebi failed. On pod spawn, JupyterHub uses RFC 8693 token exchange to convert its Keycloak access token into a Nebi JWT. This requires `tokenExchange.enabled: true` on the Nebi NebariApp and matching OIDC client IDs. + +**Fix:** + +Check hub logs for token-exchange errors: + +```bash +kubectl logs -n data-science -l component=hub --tail=200 | grep -i "nebi\|token-exchange" +``` + +Verify `tokenExchange` is enabled on the NebariApp: + +```bash +kubectl get nebariapp -n nebi -o yaml | grep -A3 tokenExchange +``` + +If not set, add it to the nebi-pack ArgoCD Application values and re-sync: + +```yaml +nebariapp: + auth: + tokenExchange: + enabled: true +``` + +Verify the client IDs in the data-science-pack values match the auto-provisioned Keycloak clients. The naming formula is `--`: + +```yaml +jupyterhub: + custom: + nebi-client-id: "nebi-nebi-pack-nebari-nebi-pack" + jupyterhub-client-id: "data-science-data-science-pack-nebari-data-science-pack" +``` + +If you used non-default release names or namespaces, recalculate these using the formula in [Client IDs and auto-provisioned secrets](./with-data-science-pack.md#client-ids-and-auto-provisioned-secrets). + +Verify the OIDC client secrets were provisioned by `nebari-operator`: + +```bash +kubectl get secret -n nebi nebi-pack-nebari-nebi-pack-oidc-client +kubectl get secret -n data-science data-science-pack-nebari-data-science-pack-oidc-client +``` + +If either secret is missing, `nebari-operator` has not yet reconciled the NebariApp — see [NebariApp not progressing](#nebariapp-not-progressing). + +### `nebi workspace list` fails / Nebi unreachable from JupyterLab + +**Symptom:** `nebi workspace list` returns a connection error. `nebi whoami` succeeds. + +**Cause A — Wrong `internalURL`:** The data-science-pack uses `nebi.internalURL` for in-cluster API calls. If this value doesn't match the actual Service name and namespace, all CLI calls from inside pods will fail. + +Verify the Service name: + +```bash +kubectl get svc -n nebi +``` + +The Service name follows the pattern `-nebari-nebi-pack`. With the default release name `nebi-pack`, the correct `internalURL` is: + +``` +http://nebi-pack-nebari-nebi-pack.nebi.svc.cluster.local +``` + +Test reachability directly from inside the cluster: + +```bash +kubectl run nebi-probe --rm -it --image=curlimages/curl --restart=Never -- \ + curl -sf http://nebi-pack-nebari-nebi-pack.nebi.svc.cluster.local/api/v1/health +``` + +**Cause B — `/api/` route not public:** The nebi-pack NebariApp must declare `/api/` and `/docs/` as public routes so that in-cluster service calls and CLI tools can reach the Nebi API without a browser session. Without this, the Envoy Gateway enforces OIDC on all paths. + +Ensure `publicRoutes` is set in the nebi-pack ArgoCD Application values: + +```yaml +nebariapp: + routing: + publicRoutes: + - pathPrefix: /api/ + pathType: PathPrefix + - pathPrefix: /docs/ + pathType: PathPrefix +``` + +**Cause C — NetworkPolicy blocking egress:** On clusters where kube-proxy DNAT's the LoadBalancer VIP before NetworkPolicy evaluation (common on Hetzner), singleuser pods need an explicit egress rule to reach the Nebi gateway hostname. See [Network egress for singleuser pods](./with-data-science-pack.md#network-egress-for-singleuser-pods) for the required NetworkPolicy configuration. + +### `nebi` command not found in JupyterLab + +**Symptom:** Running `nebi` in a JupyterLab terminal returns `command not found`. + +**Cause:** The init container that copies the nebi binary into the singleuser pod failed or did not run. + +**Fix:** + +Find the singleuser pod name and check its init container logs: + +```bash +kubectl get pods -n data-science +kubectl logs -n data-science -c nebi-init +``` + +If the init container image couldn't be pulled or exited with an error, check events: + +```bash +kubectl describe pod -n data-science +``` + +A failed init container prevents the main container from starting — the pod will be stuck in `Init:Error` or `Init:CrashLoopBackOff`. After fixing the underlying cause, the user should stop and restart their JupyterLab server from the JupyterHub control panel. diff --git a/docs/docs/deployment/values.md b/docs/docs/deployment/values.md new file mode 100644 index 0000000..2883104 --- /dev/null +++ b/docs/docs/deployment/values.md @@ -0,0 +1,85 @@ +--- +title: values.yaml reference +description: All chart values for the Nebari Nebi Pack with their defaults and descriptions. +sidebar_position: 3 +--- + +# values.yaml reference + +:::info Auto-generated +This page is generated from [`values.yaml`](https://github.com/nebari-dev/nebari-nebi-pack/blob/main/values.yaml). +::: + +## Values + +| Key | Type | Description | Default | +|-----|------|-------------|---------| +| auth | object | Auth configuration (env vars on the Deployment) | `{"oidc":{"clientID":"","clientSecretName":"","discoveryURL":"","enabled":true,"issuerURL":""},"proxyAdminGroups":"admin,nebi-admin"}` | +| auth.oidc | object | OIDC integration. Enabled by default so a stock Nebari install authenticates against Keycloak with no extra values: issuerURL, clientID and clientSecretName are all derived to match the client the nebari-operator provisions for this NebariApp. Override any field to opt out of the convention. nebi still runs NEBI_AUTH_TYPE=basic and validates the gateway-forwarded OIDC token via the verifier wired in here. | `{"clientID":"","clientSecretName":"","discoveryURL":"","enabled":true,"issuerURL":""}` | +| auth.oidc.clientID | string | OIDC client ID. | `""` | +| auth.oidc.clientSecretName | string | Name of the Kubernetes Secret containing the OIDC client secret under key `client-secret`. | `""` | +| auth.oidc.enabled | bool | Enable direct OIDC authentication. | `true` | +| auth.oidc.issuerURL | string | OIDC issuer URL (e.g. `https://keycloak.example.com/realms/nebari`). | `""` | +| auth.proxyAdminGroups | string | Keycloak/OIDC groups that grant Nebi admin role | `"admin,nebi-admin"` | +| fullnameOverride | string | Override the full name of Kubernetes resources. | `""` | +| image | object | Container image for the Nebi server. | `{"pullPolicy":"IfNotPresent","repository":"quay.io/nebari/nebi","tag":"sha-f4b23ca"}` | +| image.pullPolicy | string | Image pull policy. | `"IfNotPresent"` | +| image.repository | string | Image repository. | `"quay.io/nebari/nebi"` | +| image.tag | string | Image tag. Pin to a specific `sha-` tag for reproducible deployments. | `"sha-f4b23ca"` | +| keycloak.hostname | string | | `""` | +| log | object | Logging | `{"format":"json","level":"info"}` | +| log.format | string | Log output format. `json` for structured logs; `text` for human-readable. | `"json"` | +| log.level | string | Log verbosity level. One of `debug`, `info`, `warn`, `error`. | `"info"` | +| nameOverride | string | Override the name portion of Kubernetes resource names. | `""` | +| nebariapp | object | Creates a NebariApp CRD that configures routing and auth via nebari-operator. Set to `false` when deploying outside of Nebari. | `{"auth":{"enabled":true,"provider":"keycloak","provisionClient":true,"redirectURI":"/oauth2/callback","scopes":["openid","profile","email","groups"],"tokenExchange":{"enabled":true}},"enabled":true,"hostname":"","landingPage":{"category":"Platform","description":"Manage and share Conda/Pip environments for your Nebari cluster","displayName":"Nebi","enabled":true,"healthCheck":{"enabled":true,"intervalSeconds":30,"path":"/api/v1/health","timeoutSeconds":5},"icon":"https://raw.githubusercontent.com/nebari-dev/nebi/main/docs/static/img/nebi-icon.png","priority":10},"routing":{"publicRoutes":[{"pathPrefix":"/api/","pathType":"PathPrefix"},{"pathPrefix":"/docs/","pathType":"PathPrefix"}],"routes":[{"pathPrefix":"/"}]},"service":{"name":"","port":80}}` | +| nebariapp.auth.enabled | bool | Enforce Keycloak authentication at the Envoy Gateway for this app. | `true` | +| nebariapp.auth.provider | string | OIDC provider for authentication. | `"keycloak"` | +| nebariapp.auth.provisionClient | bool | Automatically provision a Keycloak OIDC client for this NebariApp. | `true` | +| nebariapp.auth.redirectURI | string | redirectURI must match the Envoy Gateway SecurityPolicy callback path. Nebi itself does NOT run an SSO proxy – auth is handled at the gateway (enforceAtGateway: true, the operator default), so oauth2-proxy's /oauth2/callback endpoint is what the IdP redirects back to. | `"/oauth2/callback"` | +| nebariapp.auth.scopes | list | OIDC scopes to request. | `["openid","profile","email","groups"]` | +| nebariapp.enabled | bool | Create the NebariApp resource. | `true` | +| nebariapp.hostname | string | Cluster hostname for the NebariApp route (e.g. `nebi.your-cluster.example.com`). **Required** when `nebariapp.enabled` is `true`. | | +| nebariapp.landingPage | object | landingPage controls whether and how this service appears on the Nebari landing page portal (served by nebari-landing / nebari-webapi). | `{"category":"Platform","description":"Manage and share Conda/Pip environments for your Nebari cluster","displayName":"Nebi","enabled":true,"healthCheck":{"enabled":true,"intervalSeconds":30,"path":"/api/v1/health","timeoutSeconds":5},"icon":"https://raw.githubusercontent.com/nebari-dev/nebi/main/docs/static/img/nebi-icon.png","priority":10}` | +| nebariapp.landingPage.category | string | Category label for grouping on the landing page. | `"Platform"` | +| nebariapp.landingPage.description | string | Short description shown on the landing page card. | `"Manage and share Conda/Pip environments for your Nebari cluster"` | +| nebariapp.landingPage.displayName | string | Display name shown on the landing page card. | `"Nebi"` | +| nebariapp.landingPage.enabled | bool | Show this service on the Nebari landing page portal. | `true` | +| nebariapp.landingPage.healthCheck.enabled | bool | Enable health-check polling for the landing page card status indicator. | `true` | +| nebariapp.landingPage.healthCheck.intervalSeconds | int | Seconds between health-check polls. | `30` | +| nebariapp.landingPage.healthCheck.path | string | HTTP path polled for the health check. | `"/api/v1/health"` | +| nebariapp.landingPage.healthCheck.timeoutSeconds | int | Health-check timeout in seconds. | `5` | +| nebariapp.landingPage.icon | string | Icon URL for the landing page card. | `"https://raw.githubusercontent.com/nebari-dev/nebi/main/docs/static/img/nebi-icon.png"` | +| nebariapp.landingPage.priority | int | Sort priority on the landing page (lower numbers appear first). | `10` | +| nebariapp.service.name | string | Name of the Kubernetes Service the NebariApp routes to. | | +| nebariapp.service.port | int | Port on the Service to route traffic to. | `80` | +| persistence | object | Storage for workspace environments | `{"accessMode":"ReadWriteOnce","enabled":true,"mountPath":"/app/data/environments","size":"20Gi","storageClassName":""}` | +| persistence.accessMode | string | Access mode. `ReadWriteOnce` is sufficient for a single-replica deployment. | `"ReadWriteOnce"` | +| persistence.enabled | bool | Enable the environments PersistentVolumeClaim. | `true` | +| persistence.mountPath | string | Mount path inside the Nebi container where workspace environments are stored. | `"/app/data/environments"` | +| persistence.size | string | Size of the environments PVC. | `"20Gi"` | +| persistence.storageClassName | string | StorageClass for the environments PVC. Leave empty to use the cluster default. | | +| postgres | object | Embedded PostgreSQL StatefulSet. Disable to use an external database and provide the DSN via `NEBI_DATABASE_DSN`. | `{"enabled":true,"image":{"pullPolicy":"IfNotPresent","repository":"postgres","tag":"16"},"resources":{},"storage":{"size":"10Gi","storageClassName":""}}` | +| postgres.enabled | bool | Deploy the embedded PostgreSQL StatefulSet. | `true` | +| postgres.image | object | PostgreSQL container image. | `{"pullPolicy":"IfNotPresent","repository":"postgres","tag":"16"}` | +| postgres.image.pullPolicy | string | Image pull policy. | `"IfNotPresent"` | +| postgres.image.repository | string | Image repository. | `"postgres"` | +| postgres.image.tag | string | PostgreSQL version. | `"16"` | +| postgres.resources | object | Resource requests and limits for the PostgreSQL container. | | +| postgres.storage.size | string | Size of the PostgreSQL data PVC. | `"10Gi"` | +| postgres.storage.storageClassName | string | StorageClass for the PostgreSQL PVC. Leave empty to use the cluster default. | | +| queue | object | Queue configuration (single pod = in-memory) | `{"type":"memory"}` | +| queue.type | string | Queue backend. `memory` is suitable for single-replica deployments (jobs are lost on pod restart). Use `valkey` with the upstream chart for multi-replica setups. | `"memory"` | +| replicaCount | int | Number of Nebi pod replicas. A single replica is sufficient for most deployments because `--mode=both` runs API and worker in the same pod. | `1` | +| resources | object | Resource requests and limits for the Nebi container. | | +| server | object | Server configuration | `{"mode":"production","port":8460}` | +| server.mode | string | Nebi server run mode. | `"production"` | +| server.port | int | Port the Nebi container listens on. | `8460` | +| service | object | Kubernetes Service configuration. | `{"port":80,"targetPort":8460,"type":"ClusterIP"}` | +| service.port | int | Port the Service exposes. | `80` | +| service.targetPort | int | Target port on the Nebi container. | `8460` | +| service.type | string | Service type. | `"ClusterIP"` | +| serviceAccount | object | ServiceAccount configuration. | `{"annotations":{},"create":true,"name":""}` | +| serviceAccount.annotations | object | Annotations to add to the ServiceAccount (e.g. for IAM role binding). | `{}` | +| serviceAccount.create | bool | Create a Kubernetes ServiceAccount for the Nebi Deployment. | `true` | +| serviceAccount.name | string | Name of the ServiceAccount. Defaults to the Helm release fullname. | | +| strategy | object | Deployment strategy – Recreate avoids Multi-Attach errors on RWO volumes | `{"type":"Recreate"}` | \ No newline at end of file diff --git a/docs/docs/deployment/with-data-science-pack.md b/docs/docs/deployment/with-data-science-pack.md new file mode 100644 index 0000000..76c3c17 --- /dev/null +++ b/docs/docs/deployment/with-data-science-pack.md @@ -0,0 +1,411 @@ +--- +title: Deploy with the data-science-pack +description: How to deploy the Nebari Nebi Pack alongside the nebari-data-science-pack (JupyterHub) on a Nebari cluster. +sidebar_position: 2 +--- + +# Deploy with the data-science-pack + +This guide covers deploying the nebi-pack and the +[nebari-data-science-pack](https://github.com/nebari-dev/nebari-data-science-pack) together on the same +Nebari cluster, so JupyterHub users can select Nebi-managed pixi environments when spawning servers. + +## How the integration works + +``` +Browser ──OIDC──► Envoy Gateway ──► JupyterHub (data-science-pack) + │ token exchange (RFC 8693) + ▼ + Nebi API (nebi-pack) + │ pixi workspace + ▼ + Environments PVC +``` + +On every pod spawn the data-science-pack: +1. Copies the `nebi` binary into the singleuser pod via an init container. +2. Exchanges the user's Keycloak token for a Nebi JWT and injects it as `NEBI_AUTH_TOKEN`, so `nebi` CLI commands work without re-authenticating. +3. Sets `NEBI_REMOTE_URL` so the nebi CLI knows which server to talk to. + +For the **main JupyterLab session** there is no automatic pull — `nebi pull` is expected to be run +manually in a terminal. The nebi binary is pre-installed and pre-authenticated, so running it +requires no extra login step. + +| Scenario | Behavior | +|---|---| +| Open main JupyterLab | nebi binary available + pre-authenticated; run `nebi pull ` yourself | +| Launch app via jhub-apps with workspace selected | init container auto-pulls and runs `pixi install` before the app starts | + +Two values wire the packs together: + +| Value (data-science-pack) | What it points to | +|---|---| +| `nebi.remoteURL` | External URL that **browsers** reach Nebi at (gateway hostname) | +| `nebi.internalURL` | In-cluster Service URL — pods call this directly, bypassing the gateway | + +JupyterHub acquires environment metadata from `internalURL` (low-latency, no TLS). The user's browser +is redirected to `remoteURL` for the full Nebi UI. + +**Automatic pull only happens for jhub-apps named servers.** When a user launches an app through +jhub-apps and selects a workspace in the environment selector, an additional init container runs +`nebi pull && pixi install` before the app pod starts. + +## Prerequisites + +Everything in the [Deploy the pack prerequisites](./deploy.md#prerequisites), plus: + +- An ArgoCD GitOps repository (the app-of-apps pattern shown below) +- `nebari-operator` running on the cluster +- The **nebi-pack** deployed and healthy before the data-science-pack syncs + +## 1. Deploy the nebi-pack + +### ArgoCD Application + +Save this to `apps/nebi-pack.yaml` in your GitOps repo. The values below highlight the options that +differ from a standalone nebi-pack deployment: + +```yaml +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: nebi-pack + namespace: argocd + labels: + app.kubernetes.io/part-of: nebari-packs + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + project: default + source: + repoURL: https://github.com/nebari-dev/nebari-nebi-pack.git + path: . + targetRevision: main # pin to a release tag in production + helm: + releaseName: nebi-pack + values: | + nebariapp: + hostname: nebi.your-cluster.example.com + routing: + routes: + - pathPrefix: / + # Nebi enforces its own JWT auth on the API — bypass gateway OIDC + # for API and docs routes so CLI tools and the JupyterHub integration + # can reach Nebi directly without a browser session. + publicRoutes: + - pathPrefix: /api/ + pathType: PathPrefix + - pathPrefix: /docs/ + pathType: PathPrefix + auth: + enabled: true + provider: keycloak + provisionClient: true + redirectURI: /oauth2/callback + # Required: lets JupyterHub exchange its access token for a Nebi token + # via RFC 8693 token exchange. + tokenExchange: + enabled: true + # Required: enables OAuth 2.0 device authorization flow so notebook + # cells and CLI tools can authenticate without a browser redirect. + deviceFlowClient: + enabled: true + landingPage: + enabled: true + healthCheck: + enabled: true + path: /api/v1/health + # Enable direct OIDC so Nebi can validate tokens from JupyterHub. + # The client ID and secret are auto-provisioned by nebari-operator; + # the deterministic name is described in the Client IDs section below. + auth: + oidc: + enabled: true + issuerURL: "https://keycloak.your-cluster.example.com/realms/nebari" + clientID: nebi-nebi-pack-nebari-nebi-pack + clientSecretName: nebi-pack-nebari-nebi-pack-oidc-client + # Set if your cluster's default StorageClass does not support RWO, + # or to pin to a specific class (e.g. hcloud-volumes on Hetzner). + # persistence: + # storageClassName: hcloud-volumes + # postgres: + # storage: + # storageClassName: hcloud-volumes + destination: + server: https://kubernetes.default.svc + namespace: nebi + syncPolicy: + automated: + prune: true + selfHeal: true + managedNamespaceMetadata: + labels: + nebari.dev/managed: "true" # required — operator ignores apps without this + syncOptions: + - CreateNamespace=true + - ServerSideApply=true + - SkipDryRunOnMissingResource=true + retry: + limit: 5 + backoff: + duration: 5s + factor: 2 + maxDuration: 3m +``` + +:::warning[`nebari.dev/managed: "true"` is required] + +The `managedNamespaceMetadata` block applies `nebari.dev/managed: "true"` to the namespace. +**Without this label `nebari-operator` silently ignores the `NebariApp` resource** — the hostname +returns 404 and `kubectl describe nebariapp` shows no progress on conditions. + +::: + +## 2. Deploy the data-science-pack + +### ArgoCD Application + +Save this to `apps/data-science-pack.yaml` in your GitOps repo: + +```yaml +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: data-science-pack + namespace: argocd + labels: + app.kubernetes.io/part-of: nebari-packs + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + project: default + source: + repoURL: https://github.com/nebari-dev/nebari-data-science-pack.git + targetRevision: main # pin to a release tag in production + path: . + helm: + releaseName: data-science-pack + values: | + nebariapp: + hostname: jupyter.your-cluster.example.com + routing: + routes: + - pathPrefix: / + auth: + enabled: true + provider: keycloak + provisionClient: true + # JupyterHub runs its own OAuth2 callback — do not enforce gateway OIDC. + enforceAtGateway: false + forwardAccessToken: false + redirectURI: /hub/oauth_callback + landingPage: + enabled: true + + # Point the data-science-pack at the running nebi-pack. + # remoteURL: what users' browsers reach (external gateway hostname) + # internalURL: what pods call inside the cluster (bypasses gateway, faster) + nebi: + remoteURL: "https://nebi.your-cluster.example.com" + internalURL: "http://nebi-pack-nebari-nebi-pack.nebi.svc.cluster.local" + namespace: nebi + + # JupyterHub custom config for Nebi integration. + # Client IDs here must match the auto-provisioned Keycloak clients — + # see the Client IDs section below for the naming formula. + jupyterhub: + custom: + nebi-remote-url: "https://nebi.your-cluster.example.com" + nebi-internal-url: "http://nebi-pack-nebari-nebi-pack.nebi.svc.cluster.local" + nebi-client-id: "nebi-nebi-pack-nebari-nebi-pack" + jupyterhub-client-id: "data-science-data-science-pack-nebari-data-science-pack" + nebi-environment-selector: true + hub: + config: + JupyterHub: + authenticator_class: generic-oauth + extraEnv: + # The OIDC client secret is auto-provisioned into this secret by + # nebari-operator. The secret name follows the same deterministic + # formula as the client ID (see Client IDs below). + JUPYTERHUB_OIDC_CLIENT_SECRET: + valueFrom: + secretKeyRef: + name: data-science-pack-nebari-data-science-pack-oidc-client + key: client-secret + + # Shared storage for per-group /shared/ directories. + # Requires a ReadWriteMany StorageClass (e.g. longhorn, or NFS server below). + sharedStorage: + enabled: true + size: 10Gi + # Enable the in-cluster NFS server when no RWX StorageClass is available. + # nfsServer: + # enabled: true + # storageClass: your-rwo-storage-class + + rbac: + bootstrap: + hubClientId: "data-science-data-science-pack-nebari-data-science-pack" + + destination: + server: https://kubernetes.default.svc + namespace: data-science + syncPolicy: + automated: + prune: true + selfHeal: true + managedNamespaceMetadata: + labels: + nebari.dev/managed: "true" + syncOptions: + - CreateNamespace=true + - ServerSideApply=true + - SkipDryRunOnMissingResource=true + - RespectIgnoreDifferences=true + retry: + limit: 5 + backoff: + duration: 5s + factor: 2 + maxDuration: 3m +``` + +## Client IDs and auto-provisioned secrets + +`nebari-operator` auto-provisions a Keycloak client for every `NebariApp` and stores the client +secret in a deterministic Kubernetes Secret. + +The naming formulas are: + +``` +Keycloak client ID: -- +Secret name: --oidc-client +``` + +With the defaults used in this guide: + +| Resource | Namespace | Helm release | Chart name | → | +|---|---|---|---|---| +| nebi-pack | `nebi` | `nebi-pack` | `nebari-nebi-pack` | client: `nebi-nebi-pack-nebari-nebi-pack` | +| data-science-pack | `data-science` | `data-science-pack` | `nebari-data-science-pack` | client: `data-science-data-science-pack-nebari-data-science-pack` | + +:::note[Changing release names or namespaces] + +If you use different Helm release names or deploy into different namespaces, recalculate the client +IDs using the formulas above and update the `nebi-client-id`, `jupyterhub-client-id`, +`auth.oidc.clientID`, and `rbac.bootstrap.hubClientId` values accordingly. + +::: + +Verify the secrets were created after the first sync: + +```bash +kubectl get secret -n nebi nebi-pack-nebari-nebi-pack-oidc-client +kubectl get secret -n data-science data-science-pack-nebari-data-science-pack-oidc-client +``` + +## Shared storage + +The data-science-pack mounts per-group directories at `/shared/` inside singleuser pods. +This requires a **ReadWriteMany (RWX)** StorageClass. + +| Cluster type | Recommended setup | +|---|---| +| NIC-managed Hetzner | Longhorn (`storageClass: longhorn`) provides RWX | +| Cluster without RWX class | Enable `sharedStorage.nfsServer.enabled: true` with any RWO class | +| Shared storage not needed | `sharedStorage.enabled: false` | + +```yaml +sharedStorage: + enabled: true + size: 10Gi + storageClass: longhorn # RWX StorageClass +``` + +Or with the bundled NFS server: + +```yaml +sharedStorage: + enabled: true + size: 10Gi + nfsServer: + enabled: true + storageClass: hcloud-volumes # any RWO class to back the NFS server + installClient: true + mountOptions: + - nfsvers=3 + - proto=tcp +``` + +## Network egress for singleuser pods + +On clusters where kube-proxy DNAT's the LoadBalancer VIP before NetworkPolicy evaluation (common +on Hetzner), singleuser pods need an explicit egress rule to reach Nebi via its external hostname. +Without it, the egress request is dropped at the NetworkPolicy layer before it reaches the gateway. + +```yaml +jupyterhub: + singleuser: + networkPolicy: + egress: + - to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: envoy-gateway-system + podSelector: + matchLabels: + gateway.envoyproxy.io/owning-gateway-name: nebari-gateway + ports: + - port: 10443 + protocol: TCP +``` + +This is not needed on clusters where pods can reach the external hostname directly (e.g. via a +cloud LoadBalancer that routes back into the cluster without kube-proxy DNAT). + +## Verifying the integration + +After both packs are synced and healthy: + +**1. Check NebariApp conditions for both packs:** + +```bash +kubectl get nebariapp -n nebi +kubectl get nebariapp -n data-science +kubectl describe nebariapp -n nebi +kubectl describe nebariapp -n data-science +``` + +`RoutingReady`, `TLSReady`, and `AuthReady` should all be `True`. + +**2. Confirm Nebi is reachable from inside the cluster:** + +```bash +kubectl run nebi-probe --rm -it --image=curlimages/curl --restart=Never -- \ + curl -sf http://nebi-pack-nebari-nebi-pack.nebi.svc.cluster.local/api/v1/health +``` + +Expected: `{"status":"healthy"}`. + +**3. Verify the nebi binary and token injection in JupyterLab:** + +Log in to JupyterHub, spawn a JupyterLab server, and open a terminal. The nebi binary should be +present and pre-authenticated: + +```bash +nebi whoami # should return your username without prompting for login +nebi workspace list # should list workspaces from the nebi server +``` + +If `nebi whoami` fails or returns an auth error, check hub logs for token-exchange failures: + +```bash +kubectl logs -n data-science -l component=hub --tail=100 | grep -i "nebi\|token-exchange" +``` + +## Next steps + +- **Full nebi-pack chart reference** → [values.yaml reference](./values.md) +- **Troubleshooting** → [Troubleshoot](./troubleshoot.md) +- **nebari-data-science-pack upstream docs** → [GitHub](https://github.com/nebari-dev/nebari-data-science-pack) diff --git a/docs/docs/introduction.mdx b/docs/docs/introduction.mdx index ca10a27..752b1eb 100644 --- a/docs/docs/introduction.mdx +++ b/docs/docs/introduction.mdx @@ -1,14 +1,85 @@ --- title: Introduction -description: Documentation for the Nebari Nebi Pack, an environment management solution for Nebari. +description: An overview of the Nebari Nebi Pack — deploy the Nebi environment management platform on Nebari. slug: / sidebar_position: 1 -last_update: - date: 2026-06-12 --- +import DocCardList from '@theme/DocCardList'; + # Nebari Nebi Pack -:::note[Documentation in progress] -This documentation site is being built out. Content is coming soon. -::: +## What is the Nebari Nebi Pack? + +The Nebari Nebi Pack is a Helm chart that deploys [Nebi](https://github.com/nebari-dev/nebi) on a Nebari or Kubernetes cluster. Nebi is a team environment management server built on top of [Pixi](https://pixi.sh) — it adds version control, team sharing, and access control for conda and PyPI environments, making it easy for data science teams to reproduce and share complex software stacks. + +Think of it as **git for environments**: workspaces can be snapshotted with semantic tags, diffed across versions, rolled back, and published to OCI registries — all from the command line or Nebi's web UI. + +## Why use this pack? + +Data science work depends on reproducible environments. `pip install` and `conda env create` get you started, but they don't answer "how do I give every teammate the exact same CUDA + GDAL + PyTorch stack I have?" Pixi solves the installation problem with a unified lockfile across conda-forge and PyPI. Nebi solves the collaboration problem on top of that: + +- **One command to get your teammate's environment.** `nebi pull geo-ml:v1.2` installs the exact locked environment, regardless of OS. +- **No silent environment drift.** Environments are version-controlled. `nebi diff` shows exactly what changed between releases. +- **Central governance.** Workspace access is controlled per-team with RBAC. Audit logs track who changed what. +- **Native Nebari integration.** On Nebari, authentication is handled by Keycloak at the Envoy Gateway — no separate login for users. + +## Key features + +| Feature | Description | +|---|---| +| **Version control** | Push, pull, diff, and roll back workspace environments with semantic tags | +| **Team sharing** | Central server stores workspace specs; teammates pull exact environments by name | +| **OCI publishing** | Publish workspace bundles to any OCI registry (GHCR, Quay.io, Docker Hub) | +| **RBAC** | Per-workspace read / write / admin permissions with OIDC group sync | +| **Web UI** | Browser-based interface for browsing workspaces, versions, and members | +| **Nebari SSO** | On Nebari, Keycloak handles authentication at the gateway — no separate Nebi login | +| **Embedded PostgreSQL** | Zero-dependency deploy; no external database required out of the box | + +## Who is this for? + +**Platform operators** deploying on Nebari who want to give their data science users a central place to manage and share Pixi environments. The pack handles the Kubernetes complexity — networking, auth, storage, secrets — so operators can focus on configuration. + +**Data science teams** who need reproducible, shareable environments with system-level dependencies (CUDA, GDAL, MPI) that pip alone cannot install. If your team has ever said "it works on my machine," Nebi is worth a look. + +## What this pack is not + +- **Not a package manager.** Nebi orchestrates [Pixi](https://pixi.sh) workspaces — it does not resolve packages itself. Pixi (via conda-forge and PyPI) handles the actual installation. +- **Not a replacement for Nebari profiles.** Nebari JupyterHub profiles control the compute resources and base image for each session. Nebi manages the software environments within those sessions. +- **Not a multi-replica production deployment.** The pack runs Nebi in `--mode=both` (API + worker in one pod) with an in-memory queue and RWO storage. It is sized for typical Nebari cluster workloads. For higher availability, use the [upstream Nebi chart](https://github.com/nebari-dev/nebi) directly. +- **Not a managed service.** You own and operate the deployment. + +## At a glance + +| Property | Value | +|---|---| +| **Chart name** | `nebari-nebi-pack` | +| **App version** | `latest` (pin via `image.tag`) | +| **Recommended namespace** | `nebi` | +| **Nebi image** | `quay.io/nebari/nebi` | +| **Database** | Embedded PostgreSQL 16 | +| **Storage required** | ~30 Gi RWO (20 Gi environments + 10 Gi PostgreSQL) | +| **Auth (on Nebari)** | Keycloak via NebariApp CRD — enforced at Envoy Gateway | +| **Auth (standalone)** | Basic auth or direct OIDC (configurable) | +| **Maturity** | Experimental | + + diff --git a/docs/docs/reference/index.md b/docs/docs/reference/index.md new file mode 100644 index 0000000..1161609 --- /dev/null +++ b/docs/docs/reference/index.md @@ -0,0 +1,9 @@ +--- +title: Reference +description: Reference material for the Nebari Nebi Pack. +sidebar_position: 3 +--- + +# Reference + +Content coming soon. diff --git a/docs/docs/reference/release-notes.md b/docs/docs/reference/release-notes.md new file mode 100644 index 0000000..8e413e4 --- /dev/null +++ b/docs/docs/reference/release-notes.md @@ -0,0 +1,9 @@ +--- +title: Release Notes +description: Release notes for the Nebari Nebi Pack. +sidebar_position: 1 +--- + +# Release Notes + +Content coming soon. diff --git a/docs/docs/user-guide/index.md b/docs/docs/user-guide/index.md new file mode 100644 index 0000000..a129db3 --- /dev/null +++ b/docs/docs/user-guide/index.md @@ -0,0 +1,9 @@ +--- +title: User Guide +description: Guides for end users of the Nebari Nebi Pack. +sidebar_position: 2 +--- + +# User Guide + +Content coming soon. diff --git a/docs/docs/user-guide/use.md b/docs/docs/user-guide/use.md new file mode 100644 index 0000000..7d3bb0c --- /dev/null +++ b/docs/docs/user-guide/use.md @@ -0,0 +1,9 @@ +--- +title: Using the Nebi Pack +description: How to use the Nebi Pack once it is installed on your cluster. +sidebar_position: 1 +--- + +# Using the Nebi Pack + +Content coming soon. diff --git a/docs/sidebars.ts b/docs/sidebars.ts index 9b02a03..e9cd650 100644 --- a/docs/sidebars.ts +++ b/docs/sidebars.ts @@ -3,6 +3,33 @@ import type {SidebarsConfig} from '@docusaurus/plugin-content-docs'; const sidebars: SidebarsConfig = { docsSidebar: [ 'introduction', + { + type: 'category', + label: 'Deployment', + link: { type: 'doc', id: 'deployment/index' }, + items: [ + 'deployment/deploy', + 'deployment/with-data-science-pack', + 'deployment/values', + 'deployment/troubleshoot', + ], + }, + { + type: 'category', + label: 'User Guide', + link: { type: 'doc', id: 'user-guide/index' }, + items: [ + 'user-guide/use', + ], + }, + { + type: 'category', + label: 'Reference', + link: { type: 'doc', id: 'reference/index' }, + items: [ + 'reference/release-notes', + ], + }, ], }; diff --git a/docs/values.md.gotmpl b/docs/values.md.gotmpl new file mode 100644 index 0000000..0cc22e9 --- /dev/null +++ b/docs/values.md.gotmpl @@ -0,0 +1,21 @@ +--- +title: values.yaml reference +description: All chart values for the Nebari Nebi Pack with their defaults and descriptions. +sidebar_position: 3 +--- + +# values.yaml reference + +:::info Auto-generated +This page is generated from [`values.yaml`](https://github.com/nebari-dev/nebari-nebi-pack/blob/main/values.yaml). +::: + +{{ template "chart.valuesSection" . }} + +{{- define "chart.valuesTable" -}} +| Key | Type | Description | Default | +|-----|------|-------------|---------| + {{- range .Values }} +| {{ .Key }} | {{ .Type }} | {{ if .Description }}{{ .Description }}{{ else }}{{ .AutoDescription }}{{ end }} | {{ if .Default }}{{ .Default }}{{ end }} | + {{- end }} +{{- end -}} diff --git a/values.yaml b/values.yaml index b3fa854..ac4d9cb 100644 --- a/values.yaml +++ b/values.yaml @@ -1,16 +1,23 @@ -# Default values for nebari-nebi-pack -# This is a YAML-formatted file. +# Default values for nebari-nebi-pack. # ============================================================================= # Nebari Integration # ============================================================================= -# Creates a NebariApp CRD that configures routing and auth via nebari-operator. -# Set to false when deploying outside of Nebari. + +# -- Creates a NebariApp CRD that configures routing and auth via nebari-operator. +# Set to `false` when deploying outside of Nebari. nebariapp: + # -- Create the NebariApp resource. enabled: true - # hostname: nebi.example.com # Required when enabled + # -- Cluster hostname for the NebariApp route (e.g. `nebi.your-cluster.example.com`). + # **Required** when `nebariapp.enabled` is `true`. + # @default -- `""` (must be set explicitly) + hostname: "" service: - name: "" # Defaults to {{ fullname }} + # -- Name of the Kubernetes Service the NebariApp routes to. + # @default -- `""` (defaults to Helm release fullname) + name: "" + # -- Port on the Service to route traffic to. port: 80 routing: routes: @@ -24,14 +31,18 @@ nebariapp: - pathPrefix: /docs/ pathType: PathPrefix auth: + # -- Enforce Keycloak authentication at the Envoy Gateway for this app. enabled: true + # -- OIDC provider for authentication. provider: keycloak + # -- Automatically provision a Keycloak OIDC client for this NebariApp. provisionClient: true - # redirectURI must match the Envoy Gateway SecurityPolicy callback path. + # -- redirectURI must match the Envoy Gateway SecurityPolicy callback path. # Nebi itself does NOT run an SSO proxy – auth is handled at the gateway # (enforceAtGateway: true, the operator default), so oauth2-proxy's # /oauth2/callback endpoint is what the IdP redirects back to. redirectURI: /oauth2/callback + # -- OIDC scopes to request. scopes: - openid - profile @@ -42,50 +53,69 @@ nebariapp: # token for a Nebi-audience token in Keycloak. Required by the jhub-apps # environment selector and Nebi auto-auth. enabled: true - # landingPage controls whether and how this service appears on the Nebari landing page + # -- landingPage controls whether and how this service appears on the Nebari landing page # portal (served by nebari-landing / nebari-webapi). landingPage: + # -- Show this service on the Nebari landing page portal. enabled: true + # -- Display name shown on the landing page card. displayName: "Nebi" + # -- Short description shown on the landing page card. description: "Manage and share Conda/Pip environments for your Nebari cluster" + # -- Icon URL for the landing page card. icon: "https://raw.githubusercontent.com/nebari-dev/nebi/main/docs/static/img/nebi-icon.png" + # -- Category label for grouping on the landing page. category: "Platform" + # -- Sort priority on the landing page (lower numbers appear first). priority: 10 # externalUrl: "" # Override the URL shown on the card (defaults to hostname) healthCheck: + # -- Enable health-check polling for the landing page card status indicator. enabled: true + # -- HTTP path polled for the health check. path: /api/v1/health + # -- Seconds between health-check polls. intervalSeconds: 30 + # -- Health-check timeout in seconds. timeoutSeconds: 5 # ============================================================================= # Nebi Application # ============================================================================= + +# -- Container image for the Nebi server. image: + # -- Image repository. repository: quay.io/nebari/nebi + # -- Image tag. Pin to a specific `sha-` tag for reproducible deployments. tag: sha-f4b23ca + # -- Image pull policy. pullPolicy: IfNotPresent +# -- Number of Nebi pod replicas. A single replica is sufficient for most deployments +# because `--mode=both` runs API and worker in the same pod. replicaCount: 1 -# Deployment strategy – Recreate avoids Multi-Attach errors on RWO volumes +# -- Deployment strategy – Recreate avoids Multi-Attach errors on RWO volumes strategy: type: Recreate -# Auth configuration (env vars on the Deployment) +# -- Auth configuration (env vars on the Deployment) auth: - # Keycloak/OIDC groups that grant Nebi admin role + # -- Keycloak/OIDC groups that grant Nebi admin role proxyAdminGroups: "admin,nebi-admin" - # OIDC integration. Enabled by default so a stock Nebari install authenticates + # -- OIDC integration. Enabled by default so a stock Nebari install authenticates # against Keycloak with no extra values: issuerURL, clientID and # clientSecretName are all derived to match the client the nebari-operator # provisions for this NebariApp. Override any field to opt out of the # convention. nebi still runs NEBI_AUTH_TYPE=basic and validates the # gateway-forwarded OIDC token via the verifier wired in here. oidc: + # -- Enable direct OIDC authentication. enabled: true # Derived from keycloak.hostname (which defaults to keycloak.) as https:///realms/nebari. + # -- OIDC issuer URL (e.g. `https://keycloak.example.com/realms/nebari`). issuerURL: "" # Optional: separate discovery URL for split-horizon deployments where the # public issuerURL is not reachable from in-cluster pods. Nebi fetches @@ -96,7 +126,9 @@ auth: # Derived to match the operator-provisioned client when empty: # clientID = - # clientSecretName = -oidc-client + # -- OIDC client ID. clientID: "" + # -- Name of the Kubernetes Secret containing the OIDC client secret under key `client-secret`. clientSecretName: "" # Keycloak hostname, used only to derive the OIDC issuer URL when @@ -105,53 +137,86 @@ auth: keycloak: hostname: "" -# Server configuration +# -- Server configuration server: + # -- Port the Nebi container listens on. port: 8460 + # -- Nebi server run mode. mode: production -# Logging +# -- Logging log: + # -- Log output format. `json` for structured logs; `text` for human-readable. format: json + # -- Log verbosity level. One of `debug`, `info`, `warn`, `error`. level: info -# Queue configuration (single pod = in-memory) +# -- Queue configuration (single pod = in-memory) queue: + # -- Queue backend. `memory` is suitable for single-replica deployments (jobs are + # lost on pod restart). Use `valkey` with the upstream chart for multi-replica setups. type: memory -# Storage for workspace environments +# -- Storage for workspace environments persistence: + # -- Enable the environments PersistentVolumeClaim. enabled: true + # -- Size of the environments PVC. size: 20Gi + # -- Access mode. `ReadWriteOnce` is sufficient for a single-replica deployment. accessMode: ReadWriteOnce - storageClassName: "" # Leave empty to use cluster default + # -- StorageClass for the environments PVC. Leave empty to use the cluster default. + # @default -- `""` (cluster default StorageClass) + storageClassName: "" + # -- Mount path inside the Nebi container where workspace environments are stored. mountPath: /app/data/environments # ============================================================================= # Embedded PostgreSQL # ============================================================================= + +# -- Embedded PostgreSQL StatefulSet. Disable to use an external database and +# provide the DSN via `NEBI_DATABASE_DSN`. postgres: + # -- Deploy the embedded PostgreSQL StatefulSet. enabled: true + # -- PostgreSQL container image. image: + # -- Image repository. repository: postgres + # -- PostgreSQL version. tag: "16" + # -- Image pull policy. pullPolicy: IfNotPresent storage: + # -- Size of the PostgreSQL data PVC. size: 10Gi - storageClassName: "" # Leave empty to use cluster default + # -- StorageClass for the PostgreSQL PVC. Leave empty to use the cluster default. + # @default -- `""` (cluster default StorageClass) + storageClassName: "" + # -- Resource requests and limits for the PostgreSQL container. + # @default -- `{}` (no limits) resources: {} # ============================================================================= # Service # ============================================================================= + +# -- Kubernetes Service configuration. service: + # -- Service type. type: ClusterIP + # -- Port the Service exposes. port: 80 + # -- Target port on the Nebi container. targetPort: 8460 # ============================================================================= # Resources # ============================================================================= + +# -- Resource requests and limits for the Nebi container. +# @default -- `{}` (no limits set; recommended to set in production) resources: {} # limits: # cpu: 1000m @@ -163,13 +228,22 @@ resources: {} # ============================================================================= # Service Account # ============================================================================= + +# -- ServiceAccount configuration. serviceAccount: + # -- Create a Kubernetes ServiceAccount for the Nebi Deployment. create: true + # -- Name of the ServiceAccount. Defaults to the Helm release fullname. + # @default -- `""` (defaults to release fullname) name: "" + # -- Annotations to add to the ServiceAccount (e.g. for IAM role binding). annotations: {} # ============================================================================= # Overrides # ============================================================================= + +# -- Override the name portion of Kubernetes resource names. nameOverride: "" +# -- Override the full name of Kubernetes resources. fullnameOverride: ""