diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..b9cae1a --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,67 @@ +name: Docs +on: + push: + branches: [main] + paths: ['docs/site/**', '.github/workflows/docs.yml'] + pull_request: + paths: ['docs/site/**', '.github/workflows/docs.yml'] + workflow_dispatch: +permissions: + contents: read + pull-requests: write +concurrency: + group: docs-${{ github.ref }} + cancel-in-progress: true +env: + PACK_SLUG: building-a-software-pack + # CF project name differs from the route slug for the template guide; the preview + # hostname and the deploy target must use the project, not the slug. + CF_PROJECT: nebari-software-pack-template +jobs: + docs: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: { fetch-depth: 0 } + - uses: actions/setup-go@v5 + with: { go-version-file: docs/site/go.mod, cache: false } + - uses: peaceiris/actions-hugo@v3 + with: { hugo-version: "0.159.0", extended: true } + + - name: Compute baseURL and deploy branch + id: base + env: + GH_REF: ${{ github.ref }} + GH_HEAD_REF: ${{ github.head_ref || github.ref_name }} + run: | + if [ "$GH_REF" = "refs/heads/main" ]; then + echo "url=https://packs.nebari.dev/${PACK_SLUG}/" >> "$GITHUB_OUTPUT" + echo "branch=main" >> "$GITHUB_OUTPUT" + else + HEAD="$GH_HEAD_REF" + ALIAS=$(printf '%s' "$HEAD" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g') + echo "url=https://${ALIAS}.${CF_PROJECT}.pages.dev/" >> "$GITHUB_OUTPUT" + echo "branch=${HEAD}" >> "$GITHUB_OUTPUT" + fi + + - name: Build + run: cd docs/site && hugo --gc --minify --baseURL "${{ steps.base.outputs.url }}" + + # Fork PRs cannot read secrets; skip deploy there (build above still gates). + - name: Deploy to Cloudflare Pages + id: deploy + if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }} + uses: cloudflare/wrangler-action@v3 + with: + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + command: pages deploy docs/site/public --project-name=${{ env.CF_PROJECT }} --branch=${{ steps.base.outputs.branch }} + + - name: Comment preview URL + if: ${{ github.event_name == 'pull_request' && steps.deploy.outcome == 'success' }} + uses: thollander/actions-comment-pull-request@v3 + with: + comment-tag: docs-preview + message: | + 📄 **Docs preview** for `${{ github.event.pull_request.head.ref }}`: + ${{ steps.deploy.outputs.pages-deployment-alias-url }} diff --git a/.gitignore b/.gitignore index 4c8bfa9..dbf7df9 100644 --- a/.gitignore +++ b/.gitignore @@ -25,5 +25,9 @@ venv/ .DS_Store Thumbs.db +# Hugo build output +docs/site/public/ +docs/site/.hugo_build.lock + # Claude CLAUDE.md diff --git a/README.md b/README.md index 1e94c2b..5f8a9b6 100644 --- a/README.md +++ b/README.md @@ -848,6 +848,54 @@ helm version helm dependency update examples/wrap-existing-chart/chart/ ``` +## Documentation Portal + +Pack docs are served at `packs.nebari.dev//`. The portal routes each +pack's docs site transparently via a Cloudflare edge Worker so users see a single +unified domain, while each pack deploys and previews independently. + +### Opting in + +1. **Add `docs_site: true` to `pack-metadata.yaml`:** + + ```yaml + docs_site: true + links: + docs: https://packs.nebari.dev// + ``` + +2. **Copy `.github/workflows/docs.yml` from this repo** into your pack repo and + update two values: + + | Variable | Set to | + |----------|--------| + | `PACK_SLUG` (env) | your repo's short name (the segment after `nebari-dev/`) | + | `--project-name=...` in the `wrangler` command | the matching Cloudflare Pages project name | + + For most packs, `PACK_SLUG` and the project name are the same (e.g., `llm-serving-pack`). + The template repo is a special case: `PACK_SLUG: building-a-software-pack` routes to + `packs.nebari.dev/building-a-software-pack/` but deploys to the `nebari-software-pack-template` + CF Pages project. + +3. **Add your pack to `tracked-packs.yaml`** in + [software-pack-dashboard](https://github.com/nebari-dev/software-pack-dashboard) if it is + not already there. The dashboard schema is at + `nebari-dev/software-pack-dashboard/schema/pack-metadata.schema.json`. + +### How it works + +- **Production:** push to `main` builds Hugo with `baseURL https://packs.nebari.dev//` + and deploys to the pack's Cloudflare Pages project. +- **PR previews:** every pull request builds with `baseURL https://..pages.dev/` + and deploys to a preview deployment; a bot comments the preview URL on the PR. +- **Fork PRs:** the build and link-check run, but the deploy step is skipped (fork PRs + cannot read org secrets). + +The edge Worker at `packs.nebari.dev` proxies `//*` to `.pages.dev/*` +transparently. For packs in `tracked-packs.yaml` with `docs_site: true`, the route is +generated automatically. The `building-a-software-pack` route for this template repo is +wired in the dashboard's static extra-routes map. + ## License Apache 2.0 - see [LICENSE](LICENSE). diff --git a/docs/site/content/_index.md b/docs/site/content/_index.md new file mode 100644 index 0000000..866090e --- /dev/null +++ b/docs/site/content/_index.md @@ -0,0 +1,22 @@ ++++ +title = "Building a Software Pack" ++++ + +This guide covers Nebari Software Packs: what they are, how to build one, and how to maintain it. + +A software pack is a Kubernetes application bundled with a `NebariApp` custom resource. When you deploy one, the Nebari platform automatically wires it into the shared routing, TLS, and OIDC authentication systems - no manual gateway or certificate configuration required. + +## In this guide + +- **[What is a software pack](/what-is-a-software-pack/)** - definition, why packs exist, pack contents, the NebariApp resource, pack lifecycle, and official vs community packs +- **[Build your own](/build-your-own/)** - start from the template, choose an example, deploy via ArgoCD, private and organizational packs + +## Reference pages + +- **[NebariApp CRD reference](/nebariapp-crd-reference/)** - complete field-by-field reference for the `NebariApp` custom resource +- **[Authentication flow](/auth-flow/)** - how OIDC works end-to-end; reading the IdToken in your app +- **[Release readiness](/release-readiness/)** - maturity levels and the promotion checklist for first-party packs + +## Pack template + +Use the [nebari-software-pack-template](https://github.com/nebari-dev/nebari-software-pack-template) as your starting point. It includes five examples spanning plain YAML, Kustomize, basic Helm, auth-aware apps, and wrapping an existing upstream chart. diff --git a/docs/site/content/auth-flow.md b/docs/site/content/auth-flow.md new file mode 100644 index 0000000..a4524a7 --- /dev/null +++ b/docs/site/content/auth-flow.md @@ -0,0 +1,396 @@ ++++ +title = "Authentication Flow" ++++ + +Detailed explanation of how OIDC authentication works for Nebari Software Packs. + +## Overview + +When a NebariApp has `auth.enabled: true`, the nebari-operator sets up a complete +OIDC authentication flow using Keycloak and Envoy Gateway. Users are required to +log in before accessing the application. + +## Components + +| Component | Role | +|-----------|------| +| **Envoy Gateway** | Reverse proxy that enforces the SecurityPolicy (OIDC filter) | +| **Keycloak** | OIDC identity provider that handles login and issues tokens | +| **nebari-operator** | Creates and manages all the glue resources (HTTPRoute, SecurityPolicy, Certificate, Keycloak client) | +| **cert-manager** | Provisions TLS certificates for the application hostname | + +## The Flow + +``` + Nebari Cluster + User Envoy Gateway Keycloak Your App + | | | | + |--1. GET /---->| | | + | |--2. No session---->| | + |<--3. 302 -----| cookie? | | + | redirect | | | + | | | | + |--4. Login ----|---------------+--->| | + | page | | | | + | | | | | + |--5. Submit----|---------------+--->| | + | credentials | | | | + | | | | | + |<--6. 302 -----|<--auth code--------| | + | redirect | | | + | | | | + |--7. GET / --->| | | + | (with code) |--8. Exchange------>| | + | | code for tokens | | + | |<--9. ID + Access---| | + | | tokens | | + | | | | + |<--10. Set ----| | | + | cookies + | | | + | redirect | | | + | | | | + |--11. GET / -->| | | + | (with |--12. Forward-------|---------------->| + | cookies) | request | | + | | | | + |<--13. Response from your app------|<-----------------| +``` + +### Step by step + +1. **User visits the app** at `https://my-pack.nebari.example.com` + +2. **Envoy Gateway checks for session cookies.** The OIDC filter (configured by the + SecurityPolicy) looks for valid `IdToken-*` and `AccessToken-*` cookies. + +3. **No valid session - redirect to Keycloak.** Envoy Gateway sends a 302 redirect + to the Keycloak authorization endpoint with the client ID, redirect URI, and + requested scopes. + +4. **Keycloak presents the login page.** The user sees the Keycloak login form + (or SSO if already authenticated with Keycloak). + +5. **User submits credentials.** Keycloak validates the username/password (or + delegates to an external IdP if configured). + +6. **Keycloak redirects back with an authorization code.** The redirect goes to the + `redirectURI` configured in the NebariApp (default: `/oauth2/callback`), which + is handled by Envoy Gateway's OIDC filter. + +7. **Browser follows the redirect** back to Envoy Gateway with the authorization code. + +8. **Envoy Gateway exchanges the code for tokens.** A server-to-server call from + Envoy Gateway to Keycloak's token endpoint. + +9. **Keycloak returns ID token, access token, and refresh token.** + +10. **Envoy Gateway sets session cookies.** The tokens are stored in cookies: + - `IdToken-` (JWT containing user claims) + - `AccessToken-` + - `OauthHMAC-`, `OauthExpires-`, `RefreshToken-` + + The `` is an 8-character hex string derived from the SecurityPolicy's + Kubernetes UID (e.g., `IdToken-a1b2c3d4`). + +11. **Browser retries the original request** with the session cookies attached. + +12. **Envoy Gateway validates the cookies** and forwards the request to your service + via the HTTPRoute. + +13. **Your app receives the request.** The IdToken cookies are available for your app + to read if it needs user identity information. + +## Cookie Format + +Envoy Gateway's OIDC filter sets cookies with the following naming convention: + +``` +IdToken- +AccessToken- +OauthHMAC- +OauthExpires- +RefreshToken- +OauthNonce- +``` + +The `` is an 8-character hexadecimal string generated by FNV-32a hashing the +SecurityPolicy resource's Kubernetes UID. This ensures unique cookie names when +multiple SecurityPolicies exist on the same domain. + +For example: `IdToken-a1b2c3d4`, `AccessToken-a1b2c3d4`. + +Cookie names can be customized via the `cookieNames` field in the SecurityPolicy's +OIDC configuration. + +### Reading the IdToken in your app + +Find the cookie starting with `IdToken-`: + +```python +for name, value in request.cookies.items(): + if name.startswith("IdToken-"): + full_token = value + break +``` + +### Decoding the JWT payload + +The IdToken is a standard JWT with three base64url-encoded sections separated by dots: +`header.payload.signature` + +Since Envoy Gateway already verified the signature, you can safely decode just the +payload to extract claims: + +```python +import base64, json + +parts = full_token.split(".") +payload = parts[1] +# Add base64 padding +payload += "=" * (4 - len(payload) % 4) +claims = json.loads(base64.urlsafe_b64decode(payload)) +``` + +### Common JWT claims + +| Claim | Description | +|-------|-------------| +| `preferred_username` | Keycloak username | +| `email` | User's email address | +| `name` | Display name | +| `given_name` | First name | +| `family_name` | Last name | +| `groups` | Keycloak group memberships (if `groups` scope requested) | +| `realm_access.roles` | Keycloak realm roles | +| `sub` | Unique subject identifier | +| `iss` | Token issuer URL (Keycloak realm) | +| `exp` | Token expiration timestamp | + +## What the Operator Creates + +When `auth.enabled: true`, the nebari-operator creates these resources: + +### 1. Keycloak Client (when `provisionClient: true`) + +The operator calls the Keycloak Admin API to create an OIDC client: + +- **Client ID:** `-` (namespace-scoped to prevent collisions) +- **Client protocol:** `openid-connect` +- **Access type:** `confidential` (not public) +- **Standard Flow:** enabled (OAuth2 Authorization Code flow) +- **Redirect URIs:** Both HTTP and HTTPS variants of the hostname +- **Web Origins:** `*` (allows CORS) +- **Scopes:** As configured in `spec.auth.scopes` + +### 2. Kubernetes Secret + +Client credentials are stored in a Secret named `-oidc-client`: + +```yaml +apiVersion: v1 +kind: Secret +metadata: + name: -oidc-client + labels: + app.kubernetes.io/name: nebariapp + app.kubernetes.io/instance: + app.kubernetes.io/managed-by: nebari-operator +data: + client-id: # Always present. Value: - + client-secret: # Always present. Cryptographically generated. + issuer-url: # Present when external consumers are configured. + # Value: Keycloak issuer URL (e.g., https://keycloak.example.com/realms/nebari) + spa-client-id: # Present when spaClient is enabled. + device-client-id: # Present when deviceFlowClient is enabled. +``` + +The operator also creates RBAC resources granting your app's ServiceAccount read +access to the secret: + +- **Role:** `-oidc-secret-reader` +- **RoleBinding:** `-oidc-secret-reader` + +This means your app's pods can reference the secret in `env.valueFrom.secretKeyRef` +without additional RBAC configuration. + +### 3. Envoy Gateway SecurityPolicy (when `enforceAtGateway: true`) + +```yaml +apiVersion: gateway.envoyproxy.io/v1alpha1 +kind: SecurityPolicy +metadata: + name: -oidc +spec: + targetRefs: + - group: gateway.networking.k8s.io + kind: HTTPRoute + name: + oidc: + provider: + issuer: https:///realms/ + clientID: + clientSecret: + name: -oidc-client + redirectURL: https:// + scopes: [openid, profile, email] +``` + +### 4. HTTPRoute + +```yaml +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: +spec: + parentRefs: + - name: + namespace: + hostnames: + - + rules: + - backendRefs: + - name: + port: +``` + +### 5. cert-manager Certificate (when `routing.tls.enabled: true`) + +```yaml +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: -tls +spec: + secretName: -tls + dnsNames: + - + issuerRef: + name: + kind: ClusterIssuer +``` + +## App-Native OAuth + +Some applications handle OAuth natively (e.g., Grafana, Superset, Gitea). For these +apps, the operator provisions the Keycloak client and stores credentials, but the app +handles the OAuth flow itself. This is useful when the app needs deeper integration +with the OAuth flow, such as mapping Keycloak groups/roles to app-internal roles. + +### Gateway-only auth (app reads JWT from cookies) + +If your app just needs user identity (not role mapping), use `enforceAtGateway: true` +(the default) and read the IdToken cookie as described above. + +### App-native auth only (no gateway enforcement) + +Set `enforceAtGateway: false` to skip gateway auth. The operator will: +- Provision a Keycloak client +- Store credentials in a Secret +- **NOT** create a SecurityPolicy + +```yaml +auth: + enabled: true + provider: keycloak + provisionClient: true + enforceAtGateway: false +``` + +### Dual-layer auth (recommended for RBAC apps) + +Use both gateway enforcement AND app-native OAuth. The gateway ensures users are +authenticated, while the app reads roles/groups for authorization: + +```yaml +auth: + enabled: true + provider: keycloak + provisionClient: true + # enforceAtGateway defaults to true +``` + +The app also authenticates against the same Keycloak client to get roles/groups. + +### Wiring credentials to your app + +Reference the operator-created OIDC secret to inject credentials as environment +variables. Use `valueFrom.secretKeyRef` to map specific keys: + +```yaml +# In your Helm values (e.g., for an upstream chart's extraEnv/extraEnvRaw) +extraEnvRaw: + - name: OAUTH_CLIENT_ID + valueFrom: + secretKeyRef: + name: -oidc-client + key: client-id + - name: OAUTH_CLIENT_SECRET + valueFrom: + secretKeyRef: + name: -oidc-client + key: client-secret + - name: OAUTH_ISSUER_URL + valueFrom: + secretKeyRef: + name: -oidc-client + key: issuer-url + optional: true # May not be present in all configurations +``` + +The OIDC discovery URL can be constructed as: +`/.well-known/openid-configuration` + +Your app then configures its OAuth provider using these environment variables. + +> **Note on `extraEnv` vs `extraEnvRaw`:** Many upstream Helm charts support +> multiple env var formats. Use `extraEnvRaw` (or the equivalent) when you need +> `valueFrom.secretKeyRef` syntax. Check your upstream chart's documentation for +> the correct field name. + +## NebariApp CRD vs Envoy Gateway SecurityPolicy + +The fields documented in the [NebariApp CRD Reference](/nebariapp-crd-reference/) are +the fields the **operator** understands - they go on `spec.auth` of the NebariApp +resource. At runtime, the operator generates an Envoy Gateway `SecurityPolicy` from +the NebariApp, and that SecurityPolicy has its own (much larger) set of OIDC tuning +knobs. + +For the OIDC filter fields specifically, mentally place each one in one of these +buckets: + +1. **Surfaced on NebariApp.** The operator exposes the field as a NebariApp + `spec.auth.*` field and copies it into the SecurityPolicy at reconcile time. + Currently this includes `forwardAccessToken` (`auth.forwardAccessToken`) and + redirect-deny rules (`auth.denyRedirect`). Set these on the NebariApp. + +2. **Not surfaced on NebariApp.** Most fine-grained OIDC filter fields - including + `cookieNames`, `disableIdToken`, `disableAccessToken`, `passThroughAuthHeader`, + custom logout URLs, and similar - are not exposed on NebariApp today. To use + them, either: + - File an issue / PR on + [nebari-operator](https://github.com/nebari-dev/nebari-operator) asking for the + field to be plumbed through. + - Set `auth.enforceAtGateway: false` and manage your own `SecurityPolicy` + resource alongside the NebariApp. The operator will still provision the + Keycloak client and Secret, but won't generate a SecurityPolicy to conflict + with yours. + +Refer to the [Envoy Gateway SecurityPolicy reference](https://gateway.envoyproxy.io/docs/api/extension_types/#securitypolicy) +for the full set of OIDC fields. + +## Limitations + +- **Local development:** The OIDC flow requires Keycloak and Envoy Gateway. When + developing locally with kind, set `nebariapp.enabled=false` and test without auth. + The FastAPI example shows "Not Authenticated" when no IdToken cookie is present. + +- **Token expiration:** Envoy Gateway handles token refresh automatically via refresh + tokens stored in cookies. Your app does not need to handle token refresh. + +- **Cookie size:** Very large JWTs (many groups/roles) may exceed browser cookie + size limits (typically 4KB). If this is an issue, reduce token size by limiting + scopes/claims at the Keycloak level. Token-cookie suppression + (`disableIdToken`/`disableAccessToken`) is a SecurityPolicy field the operator + does not currently expose - see the boundary section above for how to use it + if you need to. diff --git a/docs/site/content/build-your-own.md b/docs/site/content/build-your-own.md new file mode 100644 index 0000000..bcafa0e --- /dev/null +++ b/docs/site/content/build-your-own.md @@ -0,0 +1,54 @@ ++++ +title = "Build your own pack" +weight = 20 ++++ + +If a workload you'd like to run on Nebari isn't already in the catalog, you can package it yourself. + +## What's in a pack? + +A pack is a Kubernetes application bundled with a `NebariApp` custom resource. The Nebari Operator reads the `NebariApp` to wire up routing, TLS, and authentication for your app. + +If your app already runs on Kubernetes - via Helm, Kustomize, or plain YAML - adding a `NebariApp` resource is all it takes to make it a pack. + +## Start from the template + +The easiest way to create a pack is the [Software Pack template](https://github.com/nebari-dev/nebari-software-pack-template). + +1. Click **Use this template** on the template repository. +2. Clone your new repo. +3. Pick the example closest to your application. +4. Follow the instructions in the README to deploy your pack to a Nebari cluster. + +The template ships five examples of increasing complexity: + +| Example | Best for | +|---------|----------| +| `examples/vanilla-yaml/` | Plain `kubectl apply`, no tooling | +| `examples/kustomize-nginx/` | Per-environment overlays | +| `examples/basic-nginx/` | Simplest Helm chart | +| `examples/auth-fastapi/` | Custom app that reads auth tokens | +| `examples/wrap-existing-chart/` | Wrapping an existing upstream Helm chart | + +## Deploy via ArgoCD + +To deploy a pack, commit an ArgoCD Application to your gitops repo. This is a small YAML file that tells ArgoCD which pack to deploy and how to configure it. + +ArgoCD then: + +1. Reads the Application. +2. Pulls in the pack from its repository. +3. Applies the Application's configuration values to the pack. +4. Applies the resulting resources to the cluster. + +This GitOps approach means the pack configuration is version-controlled alongside everything else in your infrastructure. + +## Private and organizational packs + +Packs do not need to be public. Put yours in a private GitHub repo, an internal Git host, or any other location ArgoCD can reach. It works the same as a published pack - it just won't appear in the public catalog. + +## Going deeper + +- [NebariApp CRD reference](/nebariapp-crd-reference/) - every field explained +- [Authentication flow](/auth-flow/) - how OIDC works end-to-end, including reading the IdToken in your app +- [Release readiness](/release-readiness/) - maturity levels and the promotion checklist for official packs diff --git a/docs/site/content/concepts.md b/docs/site/content/concepts.md new file mode 100644 index 0000000..a4b6e03 --- /dev/null +++ b/docs/site/content/concepts.md @@ -0,0 +1,178 @@ ++++ +title = "Concepts" ++++ + +This section explains the key concepts behind Nebari Software Packs and the +deployment methods the template supports. + +## The NebariApp integration point + +Every software pack has exactly one integration point with the Nebari platform: the +**NebariApp** custom resource. When you create a NebariApp, the +[nebari-operator](https://github.com/nebari-dev/nebari-operator) watches for it and +automatically configures routing, TLS, and authentication. + +The NebariApp is just a Kubernetes resource - it can live in a plain YAML file, a +Kustomize base, or a Helm template. In Helm charts, you typically make it conditional +so the chart works both standalone and on Nebari: + +```yaml +{{- if .Values.nebariapp.enabled }} +apiVersion: reconcilers.nebari.dev/v1 +kind: NebariApp +metadata: + name: {{ include "my-pack.fullname" . }} +spec: + hostname: {{ required "nebariapp.hostname is required" .Values.nebariapp.hostname }} + service: + name: {{ include "my-pack.fullname" . }} + port: 80 +{{- end }} +``` + +With plain YAML or Kustomize, the NebariApp manifest is always present. When deploying +standalone, skip that file or exclude it from your apply command. + +## Deployment methods + +All three Kubernetes deployment methods are first-class in the template and in ArgoCD. + +### Plain YAML + +The lowest barrier to entry. Your pack is a set of `.yaml` files - `deployment.yaml`, +`service.yaml`, and `nebariapp.yaml`. Users run `kubectl apply -f .`. + +Best for: packs with no configuration variability, or internal tools where simplicity +trumps flexibility. + +### Kustomize + +Kustomize overlays let you patch environment-specific values (hostname, auth settings, +resource limits) on top of a shared base without duplicating the base manifests. + +``` +base/ + kustomization.yaml + deployment.yaml + service.yaml + nebariapp.yaml +overlays/ + dev/ + kustomization.yaml + nebariapp-patch.yaml # dev hostname, no auth + production/ + kustomization.yaml + nebariapp-patch.yaml # prod hostname, auth + groups +``` + +Best for: packs deployed to multiple environments with known configuration differences. + +### Helm + +Helm is the most common choice for packs that wrap existing upstream software. You add +the upstream chart as a dependency and add a NebariApp template that points to its +service - you do not rewrite the app. + +```yaml +# Chart.yaml - add the upstream chart as a dependency +dependencies: + - name: podinfo + version: 6.10.1 + repository: oci://ghcr.io/stefanprodan/charts +``` + +```yaml +# templates/nebariapp.yaml - the only template you write +{{- if .Values.nebariapp.enabled }} +apiVersion: reconcilers.nebari.dev/v1 +kind: NebariApp +spec: + hostname: {{ .Values.nebariapp.hostname }} + service: + name: {{ .Release.Name }}-podinfo # upstream service name + port: 9898 +{{- end }} +``` + +Best for: wrapping existing Helm charts and for packs with rich configuration needs. + +## Template examples + +The template repo ships five examples of increasing complexity. + +### Example 1: Vanilla YAML + +Plain Kubernetes manifests. `kubectl apply -f examples/vanilla-yaml/`. No tooling +beyond `kubectl`. The NebariApp sits alongside `deployment.yaml` and `service.yaml` +as a peer file. + +### Example 2: Kustomize (Nginx) + +Same nginx app as the vanilla example, structured with Kustomize overlays for dev and +production. Demonstrates patching hostname and auth settings per environment without +duplicating the base. + +### Example 3: Helm - Basic Pack (Nginx) + +The simplest possible Helm chart - nginx with a conditional NebariApp template and a +`nebariapp.enabled` toggle. Shows the `standalone / Nebari` mode switch. + +### Example 4: Helm - Auth-Aware FastAPI + +A custom Python app that reads the `IdToken-*` cookie set by Envoy Gateway after +Keycloak authentication. The key snippet: + +```python +def get_id_token(request: Request) -> str | None: + for name, value in request.cookies.items(): + if name.startswith("IdToken-"): + return value + return None +``` + +Shows how to extract and decode the JWT to get `preferred_username`, `email`, and +`groups`. + +### Example 5: Helm - Wrapping an Existing Chart (Podinfo) + +**This is the most realistic use case.** Most Helm-based packs wrap existing software. +You add the upstream chart as a dependency, override values, and add a NebariApp that +points to the upstream service. You do not write a Deployment or Service of your own. + +## Local development + +The `dev/` directory provides a Makefile for local development with +[kind](https://kind.sigs.k8s.io/). Running any `up-*` target automatically creates a +kind cluster with the full Nebari infrastructure stack - MetalLB, Envoy Gateway, +cert-manager, Keycloak, and the nebari-operator. + +```bash +cd dev + +make up-vanilla # deploy vanilla YAML example +make up-kustomize # deploy kustomize example (dev overlay) +make up-basic # deploy Helm nginx example +make up-fastapi # deploy FastAPI Helm example (auth enabled) +make update-hosts # update /etc/hosts with NebariApp hostnames +make down # delete the kind cluster +``` + +The first `make up-*` run takes 5-10 minutes (cluster and infrastructure setup). +Subsequent runs reuse the existing cluster and are fast. + +## Pack metadata + +Every tracked pack has a `pack-metadata.yaml` at its repo root. Key fields: + +```yaml +level: experimental # experimental | alpha | beta | ga +owner: github-username +nebariapp_integration: full # none | partial | full | na +scope: + standalone-supported: yes +``` + +The [software pack dashboard](https://github.com/nebari-dev/software-pack-dashboard) +aggregates these files and renders a single view of every tracked pack. See the +[Release Readiness](/release-readiness/) page for the full field reference and the +promotion checklist. diff --git a/docs/site/content/nebariapp-crd-reference.md b/docs/site/content/nebariapp-crd-reference.md new file mode 100644 index 0000000..e18af72 --- /dev/null +++ b/docs/site/content/nebariapp-crd-reference.md @@ -0,0 +1,342 @@ ++++ +title = "NebariApp CRD Reference" ++++ + +Complete field-by-field reference for the NebariApp custom resource. + +**API Version:** `reconcilers.nebari.dev/v1` +**Kind:** `NebariApp` +**Source:** [nebari-operator/api/v1/nebariapp_types.go](https://github.com/nebari-dev/nebari-operator/blob/v0.1.0-alpha.19/api/v1/nebariapp_types.go) +**Operator version this doc tracks:** `v0.1.0-alpha.19` + +## Full Example + +```yaml +apiVersion: reconcilers.nebari.dev/v1 +kind: NebariApp +metadata: + name: my-pack + namespace: my-pack +spec: + hostname: my-pack.nebari.example.com + serviceAccountName: my-pack + service: + name: my-pack + port: 80 + routing: + routes: + - pathPrefix: / + pathType: PathPrefix + publicRoutes: + - pathPrefix: /healthz + pathType: Exact + tls: + enabled: true + annotations: + argocd.argoproj.io/tracking-id: my-pack + auth: + enabled: true + provider: keycloak + provisionClient: true + enforceAtGateway: true + forwardAccessToken: false + # redirectURI defaults to /oauth2/callback - omit to use the operator default + scopes: + - openid + - profile + - email + groups: + - admin + gateway: public + landingPage: + enabled: true + displayName: My Pack + description: A short description for the landing page card. + icon: jupyter + category: Development + priority: 100 +``` + +## spec + +| Field | Type | Required | Default | Description | +|-------|------|----------|---------|-------------| +| `hostname` | string | Yes | - | FQDN where the app will be accessible. Used to generate the HTTPRoute and TLS certificate. Must match pattern `^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$`. | +| `service` | [ServiceReference](#specservice) | Yes | - | The backend Kubernetes Service that receives traffic. | +| `routing` | [RoutingConfig](#specrouting) | No | - | Routing behavior including path rules, TLS, and HTTPRoute annotations. **Omitting `routing` disables operator-managed routing entirely** - the operator skips HTTPRoute creation and cleans up any existing HTTPRoute. TLS is also considered disabled in that case. | +| `auth` | [AuthConfig](#specauth) | No | - | Authentication/authorization configuration. | +| `gateway` | string | No | `"public"` | Which shared Gateway to use. Valid values: `public`, `internal`. | +| `serviceAccountName` | string | No | NebariApp name | Name of the ServiceAccount used by the app's pods. The operator scopes RBAC on the OIDC client Secret to this ServiceAccount, so only the app's pods can read its credentials. | +| `landingPage` | [LandingPageConfig](#speclandingpage) | No | - | Controls how this service appears on the Nebari landing page. | + +## spec.service + +| Field | Type | Required | Default | Description | +|-------|------|----------|---------|-------------| +| `name` | string | Yes | - | Name of the Kubernetes Service. | +| `port` | int32 | Yes | - | Port number on the Service to route traffic to. Range: 1-65535. | +| `namespace` | string | No | NebariApp's namespace | Namespace of the Service. Allows referencing services in other namespaces for centralized service architectures. The operator has cluster-scoped read permission on Services. | + +## spec.routing + +| Field | Type | Required | Default | Description | +|-------|------|----------|---------|-------------| +| `routes` | [][RouteMatch](#routematch) | No | - | Path-based routing rules. If omitted, all traffic to the hostname is routed to the service. | +| `publicRoutes` | [][RouteMatch](#routematch) | No | - | Paths that bypass OIDC authentication. When `auth.enabled=true` and `auth.enforceAtGateway=true`, these paths are routed via a separate HTTPRoute that is not protected by the SecurityPolicy. Default `pathType` is `Exact` here (vs `PathPrefix` for `routes`) - safer for auth bypass. | +| `tls` | [RoutingTLSConfig](#specroutingtls) | No | - | TLS certificate management configuration. | +| `annotations` | map[string]string | No | - | Additional annotations merged onto the generated HTTPRoute. Useful for tools like ArgoCD that track resources via annotations (e.g. `argocd.argoproj.io/tracking-id`). Operator-managed annotations always take precedence. | + +### RouteMatch + +Used in both `spec.routing.routes[]` and `spec.routing.publicRoutes[]`. + +| Field | Type | Required | Default | Description | +|-------|------|----------|---------|-------------| +| `pathPrefix` | string | Yes | - | Path to match. Must start with `/`. Examples: `/`, `/api/v1`, `/dashboard`. | +| `pathType` | string | No | `PathPrefix` (in `routes`), `Exact` (in `publicRoutes`) | How the path is matched. Values: `PathPrefix`, `Exact`. | + +### spec.routing.tls + +| Field | Type | Required | Default | Description | +|-------|------|----------|---------|-------------| +| `enabled` | *bool | No | `true` | Whether to provision a TLS certificate via cert-manager and configure an HTTPS listener on the Gateway. When `false`, only HTTP listeners are used. | + +## spec.auth + +> **Validation rule:** `forwardAccessToken: true` requires `enforceAtGateway: true`. + +| Field | Type | Required | Default | Description | +|-------|------|----------|---------|-------------| +| `enabled` | bool | No | `false` | Whether to enforce OIDC authentication. | +| `provider` | string | No | `"keycloak"` | OIDC provider. Values: `keycloak`, `generic-oidc`. | +| `provisionClient` | *bool | No | `true` | Auto-provision an OIDC client in the provider. Only supported for `keycloak`. The operator creates the client and stores credentials in a Secret named `-oidc-client`. The client ID follows the convention `-`. See [Authentication Flow](/auth-flow/#2-kubernetes-secret) for the full secret structure. | +| `enforceAtGateway` | *bool | No | `true` | Create an Envoy Gateway SecurityPolicy for gateway-level auth. When `false`, the operator provisions the client and Secret but does NOT create a SecurityPolicy - the app handles OAuth natively. See [Authentication Flow](/auth-flow/#app-native-oauth) for wiring guidance. | +| `forwardAccessToken` | *bool | No | `false` | When `enforceAtGateway: true`, forward the user's OAuth access token to the upstream service via the `Authorization: Bearer ` header. Use when the app needs to read the JWT itself (e.g., to inspect the `groups` claim for per-user authorization). Without this, the gateway only stores the token in an encrypted session cookie that the backend cannot decode. | +| `denyRedirect` | [][DenyRedirectHeader](#denyredirectheader) | No | - | Headers that, when matched, prevent the OIDC filter from redirecting to the IdP and instead return 401. Helps avoid PKCE race conditions when SPAs fire multiple parallel requests on page load. Only applies when `enforceAtGateway: true`. | +| `redirectURI` | string | No | `"/oauth2/callback"` | OAuth2 callback path. The full URL is `https://`. | +| `clientSecretRef` | string | No | - | Name (string) of a Secret in the same namespace containing keys `client-id` and `client-secret`. **Note:** the spec field is a plain string (the Secret name), not the `{name, namespace}` object reference used by `status.clientSecretRef`. If omitted and `provisionClient` is true, the operator creates a Secret named `-oidc-client` with keys: `client-id`, `client-secret`, and `issuer-url`. | +| `scopes` | []string | No | `["openid", "profile", "email"]` | OIDC scopes to request during authentication. | +| `groups` | []string | No | - | Groups that have access. When specified, only users in these groups are authorized. Case-sensitive. | +| `issuerURL` | string | No | - | OIDC issuer URL. Required when `provider=generic-oidc`, ignored for `keycloak`. Example: `https://accounts.google.com`. | +| `spaClient` | [SPAClientConfig](#specauthspaclient) | No | - | Provisions a public Keycloak client for browser-based PKCE flows (e.g., React apps using `keycloak-js`). Distinct from the confidential client used by gateway-enforced auth. | +| `deviceFlowClient` | [DeviceFlowClientConfig](#specauthdeviceflowclient) | No | - | Provisions a public Keycloak client for the OAuth2 Device Authorization Grant (RFC 8628), for CLI/native apps. | +| `keycloakConfig` | [KeycloakClientConfig](#specauthkeycloakconfig) | No | - | Keycloak-specific configuration: realm groups (with optional member assignments) and client-level protocol mappers. Only applied when `provider=keycloak` and `provisionClient=true`. | +| `tokenExchange` | [TokenExchangeConfig](#specauthtokenexchange) | No | - | OAuth 2.0 Token Exchange (RFC 8693). When enabled, other NebariApp clients in the same Keycloak realm can exchange their access tokens for tokens with this client's audience. Requires `KC_FEATURES=token-exchange` on the Keycloak server. | + +### DenyRedirectHeader + +| Field | Type | Required | Default | Description | +|-------|------|----------|---------|-------------| +| `name` | string | Yes | - | Header name to match against. | +| `value` | string | Yes | - | Header value to match. | +| `type` | string | No | `Exact` | Match type. Values: `Exact`, `Prefix`, `Suffix`, `RegularExpression`. | + +### spec.auth.spaClient + +| Field | Type | Required | Default | Description | +|-------|------|----------|---------|-------------| +| `enabled` | bool | No | `false` | Provision a public OIDC client for SPA use (PKCE, no client secret). The public client ID is written to the OIDC Secret as `spa-client-id`. | +| `clientId` | string | No | `--spa` | Override the generated client ID. | + +### spec.auth.deviceFlowClient + +| Field | Type | Required | Default | Description | +|-------|------|----------|---------|-------------| +| `enabled` | bool | No | `false` | Provision a public Keycloak client configured for the Device Authorization Grant. The client ID is written to the OIDC Secret as `device-client-id`. | + +### spec.auth.keycloakConfig + +| Field | Type | Required | Default | Description | +|-------|------|----------|---------|-------------| +| `groups` | [][KeycloakGroup](#keycloakgroup) | No | - | Groups to ensure exist in the realm, with optional user membership assignments. | +| `protocolMappers` | [][KeycloakProtocolMapperConfig](#keycloakprotocolmapperconfig) | No | - | Client-level protocol mappers applied directly to the OIDC client. When specified, the operator's default mappers (e.g., group-membership) are not auto-created. | + +#### KeycloakGroup + +| Field | Type | Required | Default | Description | +|-------|------|----------|---------|-------------| +| `name` | string | Yes | - | Group name to create in Keycloak. | +| `members` | []string | No | - | Keycloak usernames to add to the group. Membership sync is **additive-only** - users not in this list are not removed. | + +#### KeycloakProtocolMapperConfig + +| Field | Type | Required | Default | Description | +|-------|------|----------|---------|-------------| +| `name` | string | Yes | - | Protocol mapper name. | +| `protocolMapper` | string | Yes | - | Mapper type identifier (e.g., `oidc-group-membership-mapper`). | +| `config` | map[string]string | No | - | Mapper configuration as arbitrary key-value pairs. Keys/values are mapper-type specific. | + +### spec.auth.tokenExchange + +| Field | Type | Required | Default | Description | +|-------|------|----------|---------|-------------| +| `enabled` | bool | No | `false` | Enable authorization services on the Keycloak client and create policies allowing other NebariApp clients in the same realm to exchange tokens for this client's audience. | + +## spec.landingPage + +| Field | Type | Required | Default | Description | +|-------|------|----------|---------|-------------| +| `enabled` | bool | No | `false` | Whether this service appears on the Nebari landing page. Set to `true` to opt in. | +| `displayName` | string | No (required when enabled) | - | Human-readable name on the landing page. Max 64 chars. | +| `description` | string | No | - | Supplementary text on the service card. Max 256 chars. | +| `icon` | string | No | - | Icon identifier or URL. Built-in icons: `jupyter`, `grafana`, `prometheus`, `keycloak`, `argocd`, `kubernetes`. | +| `category` | string | No | - | Group services together. Common categories: `Development`, `Monitoring`, `Platform`, `Data Science`. | +| `priority` | *int | No | `100` | Sort order within a category (lower = higher priority). Range: 0-1000. | +| `externalUrl` | string | No | `https://` | Override the default URL. | +| `healthCheck` | [HealthCheckConfig](#speclandingpagehealthcheck) | No | - | Health-status monitoring for the service card. | + +### spec.landingPage.healthCheck + +| Field | Type | Required | Default | Description | +|-------|------|----------|---------|-------------| +| `enabled` | bool | No | `false` | Whether health checks are performed. | +| `path` | string | No | `/health` | HTTP path to check. | +| `port` | *int32 | No | `spec.service.port` | Port to use for health checks. Range: 1-65535. | +| `intervalSeconds` | *int | No | `30` | Health check interval. Range: 10-300. | +| `timeoutSeconds` | *int | No | `5` | Per-check request timeout. Range: 1-30. | + +## Status + +The operator writes status conditions and several status fields for downstream consumers +(the webapi watcher, ArgoCD, etc.). + +### Conditions + +| Condition | Description | +|-----------|-------------| +| `RoutingReady` | HTTPRoute has been created and the Gateway is routing traffic. | +| `TLSReady` | TLS certificate is provisioned and the HTTPS listener is configured. | +| `AuthReady` | SecurityPolicy is created and the OIDC client is available. Only set when `auth.enabled=true`. | +| `Ready` | Aggregate condition - all components are ready. | + +### Condition Reasons + +| Reason | Description | +|--------|-------------| +| `Available` | Resource is functioning correctly. | +| `Reconciling` | Reconciliation is in progress. | +| `ReconcileSuccess` | Reconciliation completed successfully. | +| `ValidationSuccess` | Validation passed successfully. | +| `Failed` | Reconciliation failed. | +| `NamespaceNotOptedIn` | Namespace is missing the `nebari.dev/managed=true` label. | +| `ServiceNotFound` | The referenced Service does not exist. | +| `SecretNotFound` | The referenced Secret does not exist. | +| `GatewayNotFound` | The target Gateway does not exist. | +| `GatewayListenerConflict` | The per-app Gateway listener conflicts with another NebariApp on the same hostname. | +| `CertificateNotReady` | The cert-manager Certificate is not yet ready. | + +### Status Fields + +| Field | Type | Description | +|-------|------|-------------| +| `observedGeneration` | int64 | Most recent `metadata.generation` observed by the controller. | +| `hostname` | string | Mirror of `spec.hostname` for easy reference. | +| `gatewayRef` | object `{name, namespace}` | The Gateway resource routing traffic to this app. | +| `clientSecretRef` | object `{name, namespace}` | The Secret containing OIDC client credentials. | +| `authConfigHash` | string | SHA-256 hash of the last successfully provisioned OIDC client config. Used to skip re-provisioning when the spec is unchanged. To force re-provisioning, set the `nebari.dev/force-reprovision` annotation; the operator removes it once the forced re-provision completes. | +| `serviceDiscovery` | object | URL-resolved view of `spec.landingPage` for the webapi/landing-page watcher. Includes `enabled`, `displayName`, `description`, `url`, `icon`, `category`, `priority`, `visibility`, `requiredGroups`. | + +## Namespace Opt-In + +The namespace containing the NebariApp must be labeled for the operator to process it: + +```bash +kubectl label namespace my-pack nebari.dev/managed=true +``` + +Without this label, the NebariApp will show `NamespaceNotOptedIn` and no resources +will be created. + +## Deployment Patterns + +The NebariApp resource can be included in your pack using any deployment method. + +### Plain YAML + +The NebariApp is just another manifest file alongside your Deployment and Service: + +```yaml +# nebariapp.yaml +apiVersion: reconcilers.nebari.dev/v1 +kind: NebariApp +metadata: + name: my-pack +spec: + hostname: my-pack.nebari.example.com + service: + name: my-pack + port: 80 +``` + +When deploying standalone (without Nebari), skip this file in your `kubectl apply`. + +### Kustomize + +Include the NebariApp in your base `kustomization.yaml` and use overlays to patch +environment-specific values like `hostname` and `auth`: + +```yaml +# overlays/production/nebariapp-patch.yaml +apiVersion: reconcilers.nebari.dev/v1 +kind: NebariApp +metadata: + name: my-pack +spec: + hostname: my-pack.nebari.example.com + auth: + enabled: true + groups: + - admin +``` + +### Helm + +In Helm charts, you can make the NebariApp conditional so the chart works both +standalone and on Nebari: + +```yaml +{{- if .Values.nebariapp.enabled }} +apiVersion: reconcilers.nebari.dev/v1 +kind: NebariApp +metadata: + name: {{ include "my-pack.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "my-pack.labels" . | nindent 4 }} +spec: + hostname: {{ required "nebariapp.hostname is required" .Values.nebariapp.hostname }} + service: + name: {{ .Values.nebariapp.service.name | default (include "my-pack.fullname" .) }} + port: {{ .Values.nebariapp.service.port | default 80 }} + {{- with .Values.nebariapp.auth }} + auth: + enabled: {{ .enabled | default false }} + provider: {{ .provider | default "keycloak" }} + provisionClient: {{ .provisionClient | default true }} + {{- with .scopes }} + scopes: + {{- toYaml . | nindent 6 }} + {{- end }} + {{- end }} +{{- end }} +``` + +The corresponding `values.yaml` section: + +```yaml +nebariapp: + enabled: false + # hostname: my-pack.nebari.example.com # Required when enabled + service: + name: "" # Defaults to release fullname + port: 80 + auth: + enabled: false + provider: keycloak + provisionClient: true + scopes: + - openid + - profile + - email + gateway: public +``` diff --git a/docs/site/content/release-readiness.md b/docs/site/content/release-readiness.md new file mode 100644 index 0000000..e56cd44 --- /dev/null +++ b/docs/site/content/release-readiness.md @@ -0,0 +1,251 @@ ++++ +title = "Release Readiness" ++++ + +This page defines the maturity levels for Nebari **first-party** software packs and the +requirements for promoting a pack between levels. It applies only to packs maintained by +the Nebari core team. Community-contributed packs are out of scope. + +Pack state is declared in a `pack-metadata.yaml` file at the root of each pack repo. The +Nebari pack dashboard ([`nebari-dev/software-pack-dashboard`](https://github.com/nebari-dev/software-pack-dashboard)) +aggregates these metadata files hourly and renders a single view of every tracked pack. +That dashboard is the canonical place pre-sales engineers consult before demos. + +## Maturity Levels + +A pack moves through four sequential active levels. **Deprecated** is an orthogonal +status that can apply to a pack at any level. + +### Experimental +- **Audience:** Contributors only. +- **Promise:** None. May not install. May not work. May disappear. +- **Pre-sales behavior:** Do not demo. Do not mention to customers. + +### Alpha +- **Audience:** Internal demos. +- **Promise:** Installs and runs the happy path on a current NIC dev cluster. Known limitations are documented. +- **Pre-sales behavior:** Can demo in customer meetings; must explicitly flag as early-stage and must not commit to availability or feature timelines. + +### Beta +- **Audience:** Customer pilots. +- **Promise:** Stable enough for a customer to deploy in their own environment with engineering support. APIs and values may still change between releases. +- **Pre-sales behavior:** Can demo without caveat. Customer pilots may be offered with engineering involvement. + +### GA (v1.0+) +- **Audience:** Production customers. +- **Promise:** Fully supported. Documented upgrade path between releases. We own the bug-fix and security-fix story. +- **Pre-sales behavior:** Pitch freely. + +### Deprecated (orthogonal status) + +A pack at any level can be marked Deprecated. A deprecated pack must: +- Set `deprecated: true` and `sunset_date: YYYY-MM-DD` in `pack-metadata.yaml` +- Have a `DEPRECATED.md` at the repo root with the migration path +- Show a deprecation banner at the top of `README.md` + +The dashboard automatically moves deprecated packs to a separate table. + +**Pre-sales behavior:** Do not pitch to new customers. Do not include in demos. Existing +customers using the pack should be referred to the documented migration path. + +## Pack Metadata File + +Every tracked pack has a `pack-metadata.yaml` at its repo root. It is the source of truth +for the pack's declared maturity level, ownership, and integration metadata. The schema is +owned by the dashboard repo (`nebari-dev/software-pack-dashboard/schema/pack-metadata.schema.json`) +and may be validated locally: + +```sh +check-jsonschema \ + --schemafile https://raw.githubusercontent.com/nebari-dev/software-pack-dashboard/main/schema/pack-metadata.schema.json \ + pack-metadata.yaml +``` + +Key fields the checklist depends on: + +- `level` - the declared maturity level (`experimental` | `alpha` | `beta` | `ga`) +- `owner` - accountable engineer's GitHub username +- `product_owner` - required when `level: ga` +- `deprecated` + `sunset_date` - see Deprecated status above +- `nebariapp_integration` - `none` | `partial` | `full` | `na` +- `scope.standalone-supported` - `yes` | `no` +- `last_promoted_at` / `last_promoted_pr` - updated on every promotion +- `demo_notes` - current known gotchas, surfaced in the dashboard Notes column (first ~100 chars) + +### Scope Flags + +Scope flags are declared in `pack-metadata.yaml` under the `scope:` key. Each flag affects +which checklist items apply to a pack at each level. + +- **`standalone-supported: yes | no`** - Whether the pack is intended to install and function + without the Nebari Operator (i.e., as a plain Helm chart). If `no`, standalone-related items + in the checklist do not apply at any level. + +## Version Tagging Convention + +Packs version releases with [EffVer](https://jacobtomlinson.dev/effver/) (Effort Versioning): +`vMACRO.MESO.MICRO`. The three numbers describe how much work an upgrade costs the people +consuming the pack, not how large the change was to build: + +- **MICRO**: a drop-in. Bug fixes and additive features that need no action from existing users. +- **MESO**: some effort. A larger fix or a small breaking change that needs a little adoption work. +- **MACRO**: significant effort. A breaking overhaul; users should plan time to upgrade. + +While a pack is pre-1.0, EffVer collapses to `0.MACRO.MICRO`: the leading `0` marks the +in-development phase, a breaking or significant change bumps the middle number (`0.2.0`), and a +drop-in fix bumps the last (`0.1.1`). + +Tag rules: + +- Always prefix the tag with `v` (`v0.1.0`, `v1.2.0`). Do not prefix with the repo name or any + other string. One repo, one tag scheme. +- Tags are three numeric segments; prereleases use a SemVer-style suffix (`-alpha.N`). + +Maturity maps to the tag shape as follows. The declared `level` in `pack-metadata.yaml` remains +the source of truth; the tag is expected to line up with it but does not define it. + +| Level | Tag shape | Example | +|---|---|---| +| Experimental | usually unreleased; if tagged, a prerelease | `v0.1.0-alpha.1` | +| Alpha | prerelease `v0.1.0-alpha.N` | `v0.1.0-alpha.3` | +| Beta | bare `v0.x.y` (the whole `0.x` line) | `v0.2.1` | +| GA | `v1.0.0` and up | `v1.0.0` | + +Beta is the entire `v0.x.y` line, not a single tag: once `v0.1.0` ships you keep versioning +within `0.x` (a fix is `v0.1.1`, a breaking change is `v0.2.0`), and leaving `0.x` for +`v1.0.0` is the deliberate "this is stable now" signal. Do not renumber an already-published +higher version downward to match a label; correct the `level` field instead, or cut the next +release at a corrected number. + +## How to Use This Checklist + +Each item below is tagged with the maturity level at which it becomes a **blocker** for promotion: + +- `[E]` - Experimental +- `[A]` - Alpha +- `[B]` - Beta +- `[GA]` - GA / v1.0 + +To promote a pack from one level to the next, every item tagged with that level or earlier must +be checked. Items tagged at later levels are encouraged but not required. Items that don't apply +(e.g., auth items for a pack with no auth) should be marked **N/A** with a brief justification +in the promotion PR. + +## Promotion Process + +A promotion is a PR in the pack repo that: + +1. Updates `pack-metadata.yaml`: + - `level` set to the new target + - `last_promoted_at` set to the PR merge date + - `last_promoted_pr` set to the PR number + - `product_owner` set (required when promoting to `ga`) +2. Updates the pack's `README.md` with the new declared level +3. Has the required reviewers listed below + +The dashboard regenerates hourly; no manual dashboard update is needed. + +Required reviewers per promotion: + +| From to | Required reviewers | +|---|---| +| (new repo) to Experimental | None - default state | +| Experimental to Alpha | Pack owner + pre-sales rep | +| Alpha to Beta | Pack owner + pre-sales rep + tech lead | +| Beta to GA | Pack owner + pre-sales lead + tech lead + product owner | +| Active to Deprecated | Tech lead | + +If "product owner" has not been named for a pack, the tech lead acts as product owner for +promotion purposes, but this should be resolved before the next promotion attempt. + +## The Checklist + +### Ownership and Identity +- `[E]` Repo is created from the software pack template +- `[E]` `CODEOWNERS` names at least one accountable engineer +- `[E]` `pack-metadata.yaml` exists at the repo root, validates against the schema, and declares level + owner + scope flags +- `[E]` Pack is listed in `nebari-dev/software-pack-dashboard/tracked-packs.yaml` +- `[E]` README explains what the pack does and who it's for +- `[B]` `product_owner` field is populated in `pack-metadata.yaml` (may be the tech lead by default) + +### Installation +- `[A]` Installs cleanly on a fresh current-release NIC dev cluster following only the README instructions +- `[A]` Prerequisites documented (NIC version, cluster sizing, namespace labels, external dependencies) +- `[B]` `helm lint` passes in CI +- `[B]` `helm template` renders correctly with NebariApp enabled and disabled +- `[B]` Schema validation passes (kubeconform or equivalent) in CI +- `[GA]` Integration test in CI against the full NIC stack (nebari-operator, Envoy Gateway, cert-manager, Keycloak) +- `[GA]` *If `standalone-supported: yes`:* Standalone install test in CI (`nebariapp.enabled=false`) + +### NebariApp Integration +- `[A]` `NebariApp` reaches Ready condition with all applicable sub-conditions healthy (RoutingReady, TLSReady, AuthReady) +- `[A]` All configurable NebariApp fields used by the pack are documented in the pack's values reference +- `[A]` `nebariapp_integration` field in `pack-metadata.yaml` accurately reflects the integration depth (`none` | `partial` | `full` | `na`) +- `[B]` Auth-protected routes reject unauthenticated requests (if auth enabled) +- `[B]` Auth-protected routes allow authenticated users with correct group membership (if auth enabled) +- `[B]` Health/readiness probes configured and verified + +### Documentation +- `[E]` README exists with: what the pack does, who it's for, deploy command +- `[A]` README includes prerequisites and a "Known Limitations" section +- `[B]` Authentication setup is documented (if applicable) +- `[B]` Troubleshooting section covers common failure modes +- `[B]` Upstream chart values that users need to customize are documented +- `[GA]` Performance and sizing guidance documented +- `[GA]` Documented upgrade path from the latest pre-1.0 release to 1.0 +- `[GA]` `CHANGELOG.md` exists with notable changes summarized + +### Examples +- `[A]` At least one example values file that deploys without modification (other than hostname) +- `[B]` Example values file for full Nebari deployment (`nebari-values.yaml`) +- `[B]` *If `standalone-supported: yes`:* Example values file for standalone deployment (`standalone-values.yaml`) +- `[B]` ArgoCD Application example that references the published Helm repo + +### Telemetry +- `[B]` `ServiceMonitor` or `PodMonitor` exposed, or documented justification for not exposing metrics +- `[B]` Application logs are written to stdout/stderr in a structured format (JSON preferred) +- `[GA]` Example dashboard or Grafana panel definition for the LGTM stack (if applicable) + +### Security +- `[B]` Containers do not run as root (or have documented justification if they must) +- `[B]` No secrets hardcoded in templates or default values +- `[B]` OIDC scopes minimally scoped to what the app needs +- `[B]` Upstream container images pinned to a specific tag or digest (never `latest`) +- `[GA]` `securityContext` sets `readOnlyRootFilesystem`, `runAsNonRoot`, `allowPrivilegeEscalation: false` where possible +- `[GA]` `NetworkPolicy` or equivalent restricts unnecessary pod-to-pod communication (if applicable) + +### Release Engineering +- `[B]` Chart is publishable to `nebari-dev.github.io/helm-repository` +- `[B]` Release workflow is configured and at least one pre-1.0 release has been published +- `[B]` `appVersion` in `Chart.yaml` matches the upstream application version being wrapped +- `[GA]` Chart version is set to `1.0.0` and follows the version tagging convention +- `[GA]` Custom container images (if any) are published and accessible +- `[GA]` Helm repo index updates correctly after release +- `[GA]` Upgrade smoke test in CI: `helm install` + `helm upgrade` succeeds without errors + +### Pre-sales Verification +- `[A]` Pre-sales engineer has run the demo end-to-end and signed off on the happy path +- `[B]` Pre-sales engineer has confirmed the pack can be demoed without engineering on the call +- `[B]` `demo_notes` in `pack-metadata.yaml` reflects current known demo gotchas (or is empty if none) +- `[GA]` Pre-sales engineer has verified the demo flow on the GA release commit after release + +### Sign-off +- `[A]` Pack owner approves the promotion PR +- `[A]` Pre-sales rep approves the promotion PR +- `[B]` Tech lead approves the promotion PR +- `[GA]` Product owner has documented and verified acceptance criteria +- `[GA]` Product owner approves the promotion PR + +## What the Dashboard Surfaces Automatically + +Once `pack-metadata.yaml` is populated and the pack is in `tracked-packs.yaml`, the dashboard +will automatically flag: + +- **`stale`** - no commits in 90 days (and not deprecated) +- **`no-product-owner`** - at GA but `product_owner` is null +- **`metadata-missing`** / **`metadata-invalid`** - the file is missing or fails schema validation +- **`repo-not-found`** - the pack repo could not be reached at all +- **`deprecated`** - pack is marked `deprecated: true`; it also moves to the Deprecated packs table + +These are visibility flags, not formal blockers, but a pack with persistent flags is signaling +that something in this checklist has decayed. Tech lead reviews flags weekly. diff --git a/docs/site/content/what-is-a-software-pack.md b/docs/site/content/what-is-a-software-pack.md new file mode 100644 index 0000000..d6a80fd --- /dev/null +++ b/docs/site/content/what-is-a-software-pack.md @@ -0,0 +1,52 @@ ++++ +title = "What is a software pack" +weight = 10 ++++ + +A software pack is a self-contained, installable artifact published to a git repository that delivers a specific capability to a Nebari cluster - a chat assistant, a BI dashboard, a model-serving stack, and so on. Once installed, a pack is automatically wired into the platform's login, routing, and TLS systems. + +## Why packs exist + +The pack architecture gives four concrete benefits: + +- **Composable:** Teams install only the capabilities they need, without pulling in unrelated components. +- **Independent upgrades:** Each pack updates on its own schedule without affecting the others. +- **Bring your own pack:** Anyone can build a custom pack using the same template and resources as the official ones. +- **Smaller blast radius:** When one pack fails, others keep running. Troubleshooting and rollbacks stay scoped to that pack. + +## What a pack contains + +A pack is a set of Kubernetes manifests: + +- Application workloads: Deployments, Services, ConfigMaps, and any other resources the app needs. +- A `NebariApp` custom resource that tells the Nebari Operator to wire the pack into the platform's routing, TLS, and authentication systems. + +An ArgoCD Application deploys a pack by pointing at the pack repository and supplying configuration values. Because the config is separate from the pack itself, the same pack can be deployed multiple times with different settings. + +## The NebariApp resource + +The `NebariApp` manifest is the integration point between your application and the Nebari platform. It declares the hostname to expose, the Kubernetes Service to route traffic to, whether to provision a TLS certificate, and whether to require login. + +From that declaration, the Nebari Operator creates: + +- An **HTTPRoute** directing traffic to the target service +- A **Certificate** for HTTPS connections (via cert-manager) +- A **Keycloak client** for user authentication (via an Envoy Gateway SecurityPolicy) + +See the [NebariApp CRD reference](/nebariapp-crd-reference/) for the full field list, and [Authentication flow](/auth-flow/) for how the OIDC login sequence works end-to-end. + +## Pack lifecycle + +When a pack is deployed, it passes through four stages: + +1. An ArgoCD Application is committed to the gitops repo, and ArgoCD syncs it. +2. ArgoCD applies the pack's workloads and the `NebariApp` resource to the cluster. +3. The Nebari Operator processes the `NebariApp` and creates the routing, certificate, and authentication client. +4. The pack appears on the Nebari landing page as an available capability. + +## Official vs community packs + +Packs come from two places: + +- **Official packs** are maintained by OpenTeams under the `nebari-dev` GitHub organization. +- **Community packs** are created by external developers. They follow the same structural requirements as official packs but are maintained independently. diff --git a/docs/site/go.mod b/docs/site/go.mod new file mode 100644 index 0000000..8c67866 --- /dev/null +++ b/docs/site/go.mod @@ -0,0 +1,5 @@ +module github.com/nebari-dev/nebari-software-pack-template/docs/site + +go 1.26.3 + +require github.com/nebari-dev/nebari-hugo-theme v0.1.1 // indirect diff --git a/docs/site/go.sum b/docs/site/go.sum new file mode 100644 index 0000000..40007d8 --- /dev/null +++ b/docs/site/go.sum @@ -0,0 +1,6 @@ +github.com/nebari-dev/nebari-hugo-theme v0.0.0-20260618141623-0042a6b600fc h1:LGtkIXkV6AXjgbYU78G059Be6CpJi6+2FADG0/3AH+w= +github.com/nebari-dev/nebari-hugo-theme v0.0.0-20260618141623-0042a6b600fc/go.mod h1:N0PJCjHPJ69fOjfhNfaZsXcr+0CryBTJ7xEx7pmARSk= +github.com/nebari-dev/nebari-hugo-theme v0.1.0 h1:xfzAx5ZEjjn5jrxKox8FtmnaDaBGi8uBZ7NCkziuXPY= +github.com/nebari-dev/nebari-hugo-theme v0.1.0/go.mod h1:N0PJCjHPJ69fOjfhNfaZsXcr+0CryBTJ7xEx7pmARSk= +github.com/nebari-dev/nebari-hugo-theme v0.1.1 h1:yhWGv1J1ipsGW9jZRKo/J7cN84neh4w+jIwBe6mAmXc= +github.com/nebari-dev/nebari-hugo-theme v0.1.1/go.mod h1:N0PJCjHPJ69fOjfhNfaZsXcr+0CryBTJ7xEx7pmARSk= diff --git a/docs/site/hugo.toml b/docs/site/hugo.toml new file mode 100644 index 0000000..f6a903d --- /dev/null +++ b/docs/site/hugo.toml @@ -0,0 +1,57 @@ +baseURL = "https://packs.nebari.dev/building-a-software-pack/" +languageCode = "en-us" +title = "Building a Software Pack" +enableGitInfo = true + +# Theme imported as a Hugo Module, pinned to a release tag in go.mod (currently v0.1.1). +[module] + [[module.imports]] + path = "github.com/nebari-dev/nebari-hugo-theme" + +[markup] + [markup.tableOfContents] + startLevel = 2 + endLevel = 3 + [markup.highlight] + noClasses = false + codeFences = true + guessSyntax = true + [markup.goldmark.renderer] + unsafe = true + +[outputs] + home = ["HTML", "RSS", "JSON"] + +[params] + description = "A deep-dive guide to building, deploying, and maintaining Nebari Software Packs - Kubernetes applications with routing, TLS, and OIDC wired in." + repo = "https://github.com/nebari-dev/nebari-software-pack-template" + editBase = "https://github.com/nebari-dev/nebari-software-pack-template/edit/main/docs/site/content" + search = true + + [[params.tabs]] + name = "Guide" + url = "/" + + [[params.sidebar]] + heading = "Getting Started" + [[params.sidebar.items]] + label = "Introduction" + url = "/" + [[params.sidebar.items]] + label = "What is a software pack" + url = "/what-is-a-software-pack/" + [[params.sidebar.items]] + label = "Build your own" + url = "/build-your-own/" + + [[params.sidebar]] + heading = "Reference" + [[params.sidebar.items]] + label = "NebariApp CRD" + url = "/nebariapp-crd-reference/" + [[params.sidebar.items]] + label = "Authentication Flow" + url = "/auth-flow/" + [[params.sidebar.items]] + label = "Release Readiness" + url = "/release-readiness/"