Run a local Kubernetes cluster
deploy/kind/ brings up a disposable Kubernetes cluster with kind and installs this repository’s own Helm chart (deploy/helm/elitea) into it — not a rendering exercise, an actual helm upgrade --install. Where helm lint and helm template catch a malformed manifest, this stack catches what only shows up after kubectl apply: a hook Job that runs before its ServiceAccount exists, a Deployment that crash-loops on a bucket nobody created, a provider registrar that never arms because a required project isn’t configured.
Prerequisites
Section titled “Prerequisites”podman(this machine has nodocker). The script exportsKIND_EXPERIMENTAL_PROVIDER=podmanitself; setKIND_ENGINE=dockerto use Docker instead, which also switches every build, save, and exec step and is how CI runs this same stack on a GitHub-hosted runner.kindv0.33 or newer,kubectl,helmv3 or v4,python3,curl.- Network access, to pull the cert-manager chart and its images, the NATS images, and the upstream NATS and Qdrant charts — everything else builds locally or side-loads from podman.
- About 10 GB of free disk (images run 14 MB to 467 MB each, plus the ~932 MB kind node image) and about 4 GB of memory for the podman machine.
The commands
Section titled “The commands”deploy/kind/kind-stack.sh up # build, load, install, seeddeploy/kind/kind-stack.sh verify # prove the four claims belowdeploy/kind/kind-stack.sh down # delete the clusterseed also exists as its own subcommand (applying seed.sql again without a full reinstall), but up already runs it as its last step. up is idempotent: an existing cluster, an already-built image, and an already-installed cert-manager are all reused, and the chart install itself is a helm upgrade --install.
Measured timing on a developer machine (podman, warm build cache)
up took 3m50s end to end, and verify about 13s. That’s faster than a fully cold run — the README’s own estimate is roughly 2m26s for up once images are built, plus about 8 minutes to build them from nothing; a warm podman build cache (layers shared with other local image builds) cut that second number close to 2 minutes here. Only one third-party image (the PostgreSQL image, then pgvector/pgvector:0.8.5-pg16-trixie at ~467 MB, now plain postgres:18) needed a fresh pull; rustfs and the kind node image were already cached locally (that run also side-loaded the since-removed runtime Redis image). Built images: elitea-subapp-host:kind (~14 MB), elitea-main:kind (~146 MB), and the engine sidecar. That run predates the switch to the Rust-native sidecar (elitea-deepwiki-engine-native:kind); compiling it from nothing (~420 crates) is now the longest build in a cold up.
What up does, step by step
Section titled “What up does, step by step”Create the cluster
Creates the kind cluster
elitea-kind.Build and side-load images
Builds five images with podman (elitea-main, the sub-application host, the DeepWiki and Inventory engines, and
elitea-vector), skipping existing ones, then side-loads them and the third-party images (PostgreSQL, rustfs andqdrant/qdrant:v1.19.2-unprivileged) into the node.Install cert-manager and the two CA issuers
Installs cert-manager, then applies
manifests/ca-issuer.yaml: a self-signedClusterIssuer, a CACertificatein thecert-managernamespace, and theelitea-internal-caClusterIssuerbuilt on top of it (the provider hops). Then it storesgen-runtime-certs.sh’s runtime CA in thecert-managernamespace and appliesmanifests/runtime-ca-issuer.yaml, theelitea-runtime-caClusterIssuerthat issues the providers’ callback client certificates, theelitea-vectorpair, and Qdrant’s TLS certificate.Mint runtime material
Mints (or reuses) the production-authentication material through
deploy/scripts/gen-runtime-certs.sh— the same idempotent script the compose stacks use — and writes it into the Secrets the chart names. No callback client certificate is minted by hand: the chart issues it.Apply infrastructure manifests
Applies
manifests/infra.yaml: PostgreSQL (plainpostgres:18, withCREATE EXTENSION pgcryptorun in itsinitdb; novectorextension) and an S3-compatible object store (rustfs), plus the remaining Secrets the chart names at their fixed defaults, among themelitea-qdrant-apiandelitea-inventory-secrets.Create the artifact bucket
Creates it before elitea-main starts; omitting it causes the Deployment to crash-loop.
Install the chart
Runs
helm upgrade --installwithvalues-kind.yaml. The chart’s own pre-install hooks run the migrations: elitea-main’s-all-tenantsmigration and each provider’s own storage migration.Seed
Applies
seed.sql: project 1, toolkits, credentials, artifacts bucket, and a PAT for API authentication.
A harmless restart in the first 20 seconds
elitea-main may restart briefly while its dependencies come up; Kubernetes’ restart backoff resolves it in under a minute. This is expected and harmless.
Reaching the API — there is no UI here
Section titled “Reaching the API — there is no UI here”Services are ClusterIP only. Reach elitea-main via port-forward with a bearer token minted from the seeded PAT:
kubectl --context kind-elitea-kind -n elitea port-forward svc/elitea-main 18080:8080curl -H "Authorization: Bearer <token minted from the seeded PAT>" \ http://localhost:18080/api/v2/...elitea-deepwiki-svc, Postgres, and rustfs are all in-cluster only — elitea-main reaches the DeepWiki provider over mTLS, and the script itself reaches Postgres with kubectl exec.
What verify proves
Section titled “What verify proves”Four checks, each exiting non-zero on failure — this is the project’s helm test equivalent; the chart carries no separate helm test hook, and this is exactly what .github/workflows/helm-install-smoke.yml runs on every relevant pull request:
- Every pod is Ready (completed migration Jobs are excluded — a
SucceededJob is neverReady, and counting it as a failure would fail a correct install forever). This is the check that covers Qdrant,elitea-vector, and the Inventory provider, whichverifysends no request through. - The provider pod’s two containers —
elitea-deepwiki(host,nativerunner, reaches the sidecar over the shared Unix socket) andengine(the native image,fixturerunner) — are both present with the runner the chart set. - The facade registered the provider. A
kubectl execinto PostgreSQL readsprovider_hub.provider_origin_registrationjoined toprovider_hub.provider_health_projectionand expects awikisrow withhealthy = true— proof that elitea-main’s provider registrar armed itself and its health probe reached the provider over the mTLS hop. - A fixture generation completes through the facade. The check port-forwards elitea-main, mints the seeded PAT into a bearer token,
POSTs the same wiki-generation request the web app sends, polls until it completes, then lists the resulting artifact objects — one manifest and a handful of generated pages underacme--e2e-generated--main/.
Why Form authentication, not OIDC
Section titled “Why Form authentication, not OIDC”The provider registrar only arms when ELITEA_AI_PROJECT_ID names a public project. This stack uses Form authentication instead of OIDC; its sign-in state is in PostgreSQL, so no Redis and no worker is involved.
Known limits
Section titled “Known limits”- Both engine sidecars run the
fixturerunner (canned results, no repository and no model). The real engine is exercised on compose, not kind; see Run the standalone stack. - No browser login and no edge —
verifyauthenticates with a PAT, not a session. - No web UI, LLM gateway, scheduler, worker, or OTel collector; all disabled through values.
- Nothing is durable — PostgreSQL and the object store use
emptyDir, so a node restart loses the database. This is a proving stack, not a place to keep state.
What CI proves
Section titled “What CI proves”.github/workflows/helm-install-smoke.yml runs on changes to the chart, the kind stack, or the composition code the chart’s values reach into (weekly otherwise, to catch drift in a third-party image or cert-manager). It builds the same five images with docker/bake-action, sets up a kind cluster with helm/kind-action (version: v0.33.0), then runs the same two commands with KIND_ENGINE=docker:
bash deploy/kind/kind-stack.sh upbash deploy/kind/kind-stack.sh verifyCI reported a cold run landing in under 15 minutes with three application images warm from the shared layer cache, before the Inventory engine, elitea-vector and Qdrant joined the stack. On failure it dumps every non-Ready pod’s description, recent cluster events, and every container’s logs before the final step — bash deploy/kind/kind-stack.sh down || true, run unconditionally (if: always()) so a failed run doesn’t leak a cluster on the runner.
Tear down
Section titled “Tear down”deploy/kind/kind-stack.sh downDeletes the elitea-kind cluster. Nothing else on the machine is affected — the images up built or pulled stay in podman’s local store unless you remove them yourself (podman rmi ...).