Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
8a43167
docs: scaffold Hugo docs site on nebari-hugo-theme (pinned to theme-g…
dcmcand Jun 18, 2026
fe7b551
feat: scaffold Hugo docs site consuming nebari-hugo-theme (theme-gap-…
dcmcand Jun 18, 2026
6e2e1be
chore: re-pin nebari-hugo-theme to main now that PR #5 is merged
dcmcand Jun 18, 2026
9de90aa
docs: author Quickstart page
dcmcand Jun 18, 2026
a3fcd16
docs: clarify cluster-specific values in Quickstart ArgoCD example
dcmcand Jun 18, 2026
dbd57c2
docs: migrate production install runbook to Installation page
dcmcand Jun 18, 2026
0c8fe51
docs: move install runbook screenshots into the docs site and fix refs
dcmcand Jun 18, 2026
db81200
docs: author Local Development page
dcmcand Jun 18, 2026
73b5ae8
docs: author Shared Storage page
dcmcand Jun 18, 2026
1eba92a
docs: tighten source-default and preload:false accuracy in Shared Sto…
dcmcand Jun 18, 2026
3946baa
docs: author Troubleshooting page (Beta gate)
dcmcand Jun 18, 2026
908994e
docs: fix init container name (download-model -> model-downloader) in…
dcmcand Jun 18, 2026
82afdaa
docs: point LLMModel troubleshooting at the pack operator (nebari-llm…
dcmcand Jun 19, 2026
f00c34c
docs: author Configuration values + CRD reference (Beta gate)
dcmcand Jun 19, 2026
eb8ae60
docs: clarify spec.access.public required-ness in Configuration refer…
dcmcand Jun 19, 2026
1b7ecd2
docs: migrate design doc to Architecture page
dcmcand Jun 19, 2026
591d2c3
docs: author CI/CD and Releasing page
dcmcand Jun 19, 2026
c7e9a13
docs: correct semver image tag example to preserve v prefix
dcmcand Jun 19, 2026
b3b5c6d
docs: add Known Limitations, point references to docs site, remove mi…
dcmcand Jun 19, 2026
44f1399
docs: correct maturity wording in Known Limitations (alpha, not beta)
dcmcand Jun 19, 2026
058f0b2
test: add internal + edit-link checker for the docs site
dcmcand Jun 19, 2026
7841d6e
docs: address PR #108 review findings
dcmcand Jun 19, 2026
bf6670f
ci: validate the built artifact in-place via SKIP_BUILD instead of re…
dcmcand Jun 19, 2026
8a22178
docs: add purpose/overview to the docs home page
dcmcand Jun 19, 2026
1995dbf
chore: pin docs theme to nebari-hugo-theme v0.1.0 release
dcmcand Jun 19, 2026
69de352
docs: link each Quickstart prerequisite to its Installation section
dcmcand Jun 19, 2026
9c30b53
chore: bump docs theme pin to nebari-hugo-theme v0.1.1 (wider layout …
dcmcand Jun 19, 2026
5a5bd82
docs: add docs-site preview screenshots
dcmcand Jun 19, 2026
5c89161
docs: correct operator/key-manager behavior claims from PR review
dcmcand Jun 23, 2026
c537253
docs(astro): scaffold Astro + Starlight project
dcmcand Jun 24, 2026
10c9375
docs(astro): apply nebari-design tokens, fonts, and logo
dcmcand Jun 24, 2026
a4d96d8
docs(astro): add base-aware link rewrite helper
dcmcand Jun 24, 2026
60a952a
docs(astro): load Hugo TOML content in place via custom loader
dcmcand Jun 24, 2026
ad28616
docs(astro): configure sidebar and copy screenshot assets
dcmcand Jun 24, 2026
fefcdc0
docs(astro): wire rehype-mermaid for diagram parity
dcmcand Jun 24, 2026
a689da7
docs(astro): render nebari-design Button in header chrome
dcmcand Jun 24, 2026
6957466
docs(astro): use base-vega style and drop duplicate GitHub social icon
dcmcand Jun 24, 2026
3cb3c78
ci(astro): add non-deploying bake-off build job
dcmcand Jun 24, 2026
44c05bc
docs(astro): add bake-off screenshot script
dcmcand Jun 24, 2026
f3d39c4
docs(astro): bridge nebari dark tokens to Starlight data-theme
dcmcand Jun 24, 2026
092ce9f
docs(astro): harden content loader frontmatter handling and cleanup
dcmcand Jun 24, 2026
75f9fad
docs(astro): use nebari symbol favicon and ignore editor config
dcmcand Jun 24, 2026
eb54f47
build: add Makefile with docs (Hugo) and astro dev-server targets
dcmcand Jun 24, 2026
b2842e9
docs: keep flat docs/getting-started.md (out of scope for the docs-si…
dcmcand Jun 30, 2026
cb2f567
docs(astro): add remark-base-links plugin for base-path link prefixing
dcmcand Jun 30, 2026
6c3fdaa
docs(astro): migrate Hugo content into native Starlight collection
dcmcand Jun 30, 2026
2ce51a9
docs(astro): use native Starlight loader and base-path remark plugin
dcmcand Jun 30, 2026
4884191
docs(astro): port internal link checker to the Astro dist output
dcmcand Jun 30, 2026
727205a
ci(docs): build and deploy the Astro site to Cloudflare Pages
dcmcand Jun 30, 2026
d9d9f49
ci(docs): add Cloudflare Pages preview-cleanup workflow
dcmcand Jun 30, 2026
36ff1dd
docs: remove Hugo site, superseded by the Astro docs site
dcmcand Jun 30, 2026
92ff69d
docs(astro): update CI/CD page to document the Astro docs workflow
dcmcand Jun 30, 2026
4ff7b6a
ci(docs): bump CI Node to 22 (Astro 7 requires >=22.12)
dcmcand Jun 30, 2026
ded3452
docs: restructure docs site per PR #120 review
dcmcand Jul 1, 2026
4ca3d92
fix(webhook): read authoritative state for cross-kind name and namesp…
dcmcand Jul 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions .github/workflows/docs-preview-cleanup.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
name: Docs preview cleanup
# When a PR closes (merged or not), delete the Cloudflare Pages preview
# deployments for that branch. Direct Upload deploys are not tied to the git
# branch lifecycle, so without this they linger after the branch is gone.
on:
pull_request:
types: [closed]
permissions:
contents: read
concurrency:
group: docs-preview-cleanup-${{ github.event.pull_request.number }}
cancel-in-progress: false
env:
CF_PROJECT: llm-serving-pack
jobs:
cleanup:
# Fork PRs never get a preview (deploy is skipped, secrets unavailable),
# so there is nothing to clean up for them.
if: ${{ github.event.pull_request.head.repo.full_name == github.repository }}
runs-on: ubuntu-latest
steps:
- name: Delete this branch's preview deployments
env:
CF_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CF_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
BRANCH: ${{ github.event.pull_request.head.ref }}
run: |
set -euo pipefail
base="https://api.cloudflare.com/client/v4/accounts/${CF_ACCOUNT_ID}/pages/projects/${CF_PROJECT}/deployments"
ids=$(curl -fsS -H "Authorization: Bearer ${CF_API_TOKEN}" "${base}?env=preview&per_page=100" \
| jq -r --arg b "$BRANCH" '.result[] | select(.deployment_trigger.metadata.branch == $b) | .id')
if [ -z "$ids" ]; then
echo "No preview deployments found for branch '$BRANCH'."
exit 0
fi
for id in $ids; do
echo "Deleting preview deployment $id (branch '$BRANCH')"
curl -fsS -X DELETE -H "Authorization: Bearer ${CF_API_TOKEN}" "${base}/${id}?force=true" >/dev/null
done
echo "Cleaned up preview deployments for branch '$BRANCH'."
86 changes: 86 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
name: Docs
on:
push:
branches: [main]
paths: ['docs/**', 'scripts/check-links.sh', '.github/workflows/docs.yml']
pull_request:
paths: ['docs/**', 'scripts/check-links.sh', '.github/workflows/docs.yml']
workflow_dispatch:
permissions:
contents: read
pull-requests: write
concurrency:
group: docs-${{ github.ref }}
cancel-in-progress: true
env:
PACK_SLUG: llm-serving-pack
jobs:
docs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 }
- uses: actions/setup-node@v4
with:
node-version: '22'
cache: npm
cache-dependency-path: docs/package-lock.json

- name: Install deps
working-directory: docs
run: npm ci

- name: Install Playwright (mermaid rendering)
working-directory: docs
run: npx playwright install --with-deps chromium

- name: Unit tests
working-directory: docs
run: npm test

- name: Compute site, base, and deploy branch
id: base
env:
HEAD_REF: ${{ github.head_ref || github.ref_name }}
run: |
if [ "${{ github.ref }}" = "refs/heads/main" ]; then
echo "site=https://packs.nebari.dev" >> "$GITHUB_OUTPUT"
echo "base=/${PACK_SLUG}/" >> "$GITHUB_OUTPUT"
echo "branch=main" >> "$GITHUB_OUTPUT"
else
ALIAS=$(printf '%s' "$HEAD_REF" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g')
echo "site=https://${ALIAS}.${PACK_SLUG}.pages.dev" >> "$GITHUB_OUTPUT"
echo "base=/" >> "$GITHUB_OUTPUT"
echo "branch=${HEAD_REF}" >> "$GITHUB_OUTPUT"
fi

- name: Build
working-directory: docs
env:
SITE: ${{ steps.base.outputs.site }}
BASE: ${{ steps.base.outputs.base }}
run: npm run build

- name: Check internal links
env:
BASE: ${{ steps.base.outputs.base }}
run: SKIP_BUILD=1 bash scripts/check-links.sh

# Fork PRs cannot read secrets; skip deploy there (build + link-check still gate).
- 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/dist --project-name=${{ env.PACK_SLUG }} --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 }}
35 changes: 35 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# nebari-llm-serving-pack
#
# Tasks for the Astro/Starlight documentation site (lives in docs/).
# Run `make help` to list targets.
#
# For the local kind dev cluster, see the separate dev/Makefile
# (`cd dev && make help`). For Go tests, see operator/ and key-manager/.

DOCS_DIR := docs

.DEFAULT_GOAL := help
.PHONY: help docs docs-install docs-build docs-preview docs-test docs-check-links

help: ## List available targets
@grep -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) \
| sort \
| awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-16s\033[0m %s\n", $$1, $$2}'

docs: ## Run the docs site dev server with hot reload (installs deps on first run)
cd $(DOCS_DIR) && { [ -d node_modules ] || npm install; } && npm run dev

docs-install: ## Install docs site dependencies from the lockfile (npm ci)
cd $(DOCS_DIR) && npm ci

docs-build: ## Build the static docs site into docs/dist
cd $(DOCS_DIR) && npm run build

docs-preview: ## Serve the built docs/dist locally to preview the production build
cd $(DOCS_DIR) && npm run preview

docs-test: ## Run the docs unit tests (vitest)
cd $(DOCS_DIR) && npm test

docs-check-links: ## Build the site and verify every internal link resolves
bash scripts/check-links.sh
43 changes: 41 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

A [Nebari](https://github.com/nebari-dev/nebari-infrastructure-core) software pack for serving LLMs. Deploys a Kubernetes operator that manages LLM model serving via [llm-d](https://llm-d.ai), with per-model access control, API key management, and Envoy AI Gateway integration for token counting and rate limiting.

**Documentation:** [https://nebari-dev.github.io/nebari-llm-serving-pack/](https://nebari-dev.github.io/nebari-llm-serving-pack/)

## What this does

You apply an `LLMModel` custom resource and the operator handles the rest: model download, vLLM serving pods, inference scheduling, routing, and auth.
Expand All @@ -19,7 +21,7 @@ Models can be loaded from HuggingFace (default) or mounted as OCI/modelcar image
- Kubernetes 1.28+ cluster with [Nebari Infrastructure Core](https://github.com/nebari-dev/nebari-infrastructure-core) deployed
- [nebari-operator](https://github.com/nebari-dev/nebari-operator) running
- NVIDIA GPU Operator installed (auto-discovers GPU nodes and manages the device plugin). **Note**: nebari-infrastructure-core does not install this automatically yet - tracked in [nebari-dev/nebari-infrastructure-core#232](https://github.com/nebari-dev/nebari-infrastructure-core/issues/232). Until that is done, install it manually as an ArgoCD app (see [examples/nvidia-gpu-operator.yaml](examples/nvidia-gpu-operator.yaml)).
- **Envoy Gateway installed and configured for AI Gateway integration** - `extensionApis.enableBackend`, `extensionManager` pointing at the AI Gateway controller service, and `backendResources` allowing `inference.networking.k8s.io/InferencePool`. This is a **hard requirement**; without it, the routing layer 404s at runtime. Ready-to-apply example in [`examples/envoy-gateway.yaml`](examples/envoy-gateway.yaml); see [`docs/install-production.md`](docs/install-production.md#6-reconfigure-envoy-gateway-with-ai-gateway-extension-wiring) for details.
- **Envoy Gateway installed and configured for AI Gateway integration** - `extensionApis.enableBackend`, `extensionManager` pointing at the AI Gateway controller service, and `backendResources` allowing `inference.networking.k8s.io/InferencePool`. This is a **hard requirement**; without it, the routing layer 404s at runtime. Ready-to-apply example in [`examples/envoy-gateway.yaml`](examples/envoy-gateway.yaml); see the [Installation guide](https://nebari-dev.github.io/nebari-llm-serving-pack/installation/#6-reconfigure-envoy-gateway-with-ai-gateway-extension-wiring) for details.
- Envoy AI Gateway installed (v0.5.0+). **Note**: the `envoyAIGateway.install` flag in this chart is not yet implemented - tracked in [#44](https://github.com/nebari-dev/nebari-llm-serving-pack/issues/44). Until that is done, install it manually as an ArgoCD app (see [examples/envoy-ai-gateway.yaml](examples/envoy-ai-gateway.yaml)).
- [Gateway API Inference Extension](https://github.com/kubernetes-sigs/gateway-api-inference-extension) (GIE) installed (InferencePool / InferenceModel CRDs).
- A cert-manager `ClusterIssuer` the operator can use for the shared-hostname Certificate. Default expected name is `letsencrypt-production`; override with `platform.tls.clusterIssuer` in the chart values.
Expand Down Expand Up @@ -248,7 +250,7 @@ The pack expects the following to be available on the cluster:

## Development

See [docs/getting-started.md](docs/getting-started.md) for a full walkthrough of the local dev environment.
See the [Local Development guide](https://nebari-dev.github.io/nebari-llm-serving-pack/local-development/) for a full walkthrough of the local dev environment.

```bash
# Create kind cluster with all dependencies
Expand Down Expand Up @@ -281,6 +283,43 @@ cd operator && make test
cd key-manager && go test ./...
```

### Documentation site

The docs site is an [Astro](https://astro.build) + [Starlight](https://starlight.astro.build) project living in [`docs/`](docs/). The common tasks are wrapped in the root `Makefile` (run `make help` to list them):

```bash
make docs # dev server with hot reload at http://localhost:4321
make docs-build # build the static site into docs/dist
make docs-preview # serve the built site to preview the production build
make docs-test # run the unit tests (vitest)
make docs-check-links # build, then verify every internal link resolves
```

**Add a page:** create a Markdown file under `docs/src/content/docs/`, then add a `sidebar` entry for it in [`docs/astro.config.mjs`](docs/astro.config.mjs). Pages use standard Starlight frontmatter and support Mermaid diagrams (rendered to SVG at build time).

**Add or remove a component:** shared UI lives in `docs/src/components/` (Astro and React). The shadcn-style primitives under `docs/src/components/ui/` are tracked in `docs/components.json`; edit that directory and reference the component from a page or another component.

**Images:** put images in `docs/src/assets/` and reference them with a relative path (for example `../../assets/foo.png`) so Astro optimizes, hashes, and base-path-corrects them at build time. Only genuinely static files such as `favicon.svg` belong in `docs/public/`.

The site is built and deployed to Cloudflare Pages by [`.github/workflows/docs.yml`](.github/workflows/docs.yml); the [CI/CD and Releasing](docs/src/content/docs/cicd-and-releasing.md) page documents the pipeline.

## Known Limitations

This pack is at alpha maturity (`v0.1.0-alpha.x`). The following limitations apply:

- **Single-namespace model:** All LLMModels must be in the operator's own namespace. Per-team isolation requires running separate pack installs (one namespace per team). This is a hard constraint imposed by Envoy Gateway's `apiKeyAuth`, which does not support cross-namespace Secret references.
- **API keys are not continuously tied to group membership.** Keys are issued based on the user's OIDC groups at creation time. If a user later loses group access, existing keys continue to work. The periodic audit that revokes such keys (default interval: 5 minutes) is **off by default** - it only runs when `keyManager.oidcUserinfoURL` is configured, which ships empty. Without it, keys are never revoked on group change. Even when enabled, this is eventual consistency, not real-time revocation.
- **JWKS path is Keycloak-specific.** The internal endpoint's JWT SecurityPolicy constructs the JWKS URI as `<issuerURL>/protocol/openid-connect/certs`. Non-Keycloak OIDC providers will not work out of the box. Tracked in [#61](https://github.com/nebari-dev/nebari-llm-serving-pack/issues/61).
- **NVIDIA GPUs only.** AMD and Intel accelerators are not supported in v0.1.
- **No scale-to-zero.** Idle model pods are not scaled down automatically.
- **No per-key rate limiting or token quotas.** Rate limiting is applied at the model level via Envoy AI Gateway, not per individual API key.
- **No API key expiration.** Keys do not expire on a schedule; revocation requires either manual deletion or - only when the audit is enabled via `keyManager.oidcUserinfoURL` - the audit detecting lost group access.
- **No team-level shared API keys.** Each key is tied to an individual user's identity.
- **OCI model loading uses init-container copy.** Kubernetes image volumes (alpha/beta) are not used; every pod start copies files from the OCI image to a shared emptyDir. On pods with `storage.type: emptyDir` and `preload: true`, every restart triggers a full re-download.
- **API key and metadata storage is limited to ~1 MiB per model** (Kubernetes Secret/ConfigMap size limit). This supports several thousand keys per model; it is a known scaling ceiling for v0.1.
- **`envoyAIGateway.install` flag is not yet implemented.** Envoy AI Gateway must be installed separately. Tracked in [#44](https://github.com/nebari-dev/nebari-llm-serving-pack/issues/44).
- **Per-model subdomains are not yet wired into routing.** The `endpoints.external.subdomain` field is validated but unused. All models share `llm.<baseDomain>`; per-model dispatch is by the `model` field in the request body.

## License

Apache 2.0. See [LICENSE](LICENSE).
27 changes: 27 additions & 0 deletions docs/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# build output
dist/
# generated types
.astro/

# dependencies
node_modules/

# logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*


# environment variables
.env
.env.production

# macOS-specific files
.DS_Store

# bake-off screenshots (local only)
bakeoff-screenshots/

# Editor
.vscode/
73 changes: 73 additions & 0 deletions docs/astro.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// docs/astro.config.mjs
import { defineConfig } from 'astro/config';
import starlight from '@astrojs/starlight';
import react from '@astrojs/react';
import tailwindcss from '@tailwindcss/vite';
import rehypeMermaid from 'rehype-mermaid';
import { fileURLToPath } from 'url';
import path from 'path';
import remarkBaseLinks from './src/plugins/remark-base-links';

const __dirname = path.dirname(fileURLToPath(import.meta.url));

export default defineConfig({
// Base defaults to '/' for the local bake-off. Override via BASE env if deployed.
base: process.env.BASE || '/',
site: process.env.SITE,
integrations: [
react(),
starlight({
title: 'LLM Serving Pack',
description:
'Kubernetes operator for serving LLMs on Nebari via llm-d, with per-model OIDC access control, API key management, and Envoy AI Gateway integration.',
logo: {
light: './src/assets/logo.svg',
dark: './src/assets/logo-dark.svg',
replacesTitle: false,
},
customCss: [
'@fontsource-variable/geist',
'@fontsource/ibm-plex-mono',
'./src/styles/nebari-tokens.css',
'./src/styles/starlight-theme.css',
],
components: {
SocialIcons: './src/components/SocialIcons.astro',
},
sidebar: [
{
label: 'Documentation',
items: [
{ label: 'Quickstart', slug: 'quickstart' },
{ label: 'Installation', slug: 'installation' },
{ label: 'Local Development', slug: 'local-development' },
{ label: 'Shared Storage', slug: 'shared-storage' },
{ label: 'Troubleshooting', slug: 'troubleshooting' },
],
},
{
label: 'Reference',
items: [
{ label: 'Configuration', slug: 'configuration' },
{ label: 'Architecture', slug: 'architecture' },
{ label: 'CI/CD and Releasing', slug: 'cicd-and-releasing' },
],
},
],
}),
],
markdown: {
// syntaxHighlight false on mermaid so the plugin sees raw graph source.
syntaxHighlight: { type: 'shiki', excludeLangs: ['mermaid'] },
remarkPlugins: [[remarkBaseLinks, { base: process.env.BASE || '/' }]],
rehypePlugins: [[rehypeMermaid, { strategy: 'inline-svg' }]],
},
vite: {
plugins: [tailwindcss()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
},
});
24 changes: 24 additions & 0 deletions docs/components.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "base-vega",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/styles/nebari-tokens.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"aliases": {
"components": "@/components",
"ui": "@/components/ui",
"lib": "@/lib",
"utils": "@/lib/utils",
"hooks": "@/hooks"
},
"registries": {
"@nebari": "https://nebari-dev.github.io/nebari-design/r/{name}.json"
}
}
Loading
Loading