Skip to content

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.

  • podman (this machine has no docker). The script exports KIND_EXPERIMENTAL_PROVIDER=podman itself; set KIND_ENGINE=docker to 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.
  • kind v0.33 or newer, kubectl, helm v3 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.
Terminal window
deploy/kind/kind-stack.sh up # build, load, install, seed
deploy/kind/kind-stack.sh verify # prove the four claims below
deploy/kind/kind-stack.sh down # delete the cluster

seed 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.

  1. Create the cluster

    Creates the kind cluster elitea-kind.

  2. 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 and qdrant/qdrant:v1.19.2-unprivileged) into the node.

  3. Install cert-manager and the two CA issuers

    Installs cert-manager, then applies manifests/ca-issuer.yaml: a self-signed ClusterIssuer, a CA Certificate in the cert-manager namespace, and the elitea-internal-ca ClusterIssuer built on top of it (the provider hops). Then it stores gen-runtime-certs.sh’s runtime CA in the cert-manager namespace and applies manifests/runtime-ca-issuer.yaml, the elitea-runtime-ca ClusterIssuer that issues the providers’ callback client certificates, the elitea-vector pair, and Qdrant’s TLS certificate.

  4. 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.

  5. Apply infrastructure manifests

    Applies manifests/infra.yaml: PostgreSQL (plain postgres:18, with CREATE EXTENSION pgcrypto run in its initdb; no vector extension) and an S3-compatible object store (rustfs), plus the remaining Secrets the chart names at their fixed defaults, among them elitea-qdrant-api and elitea-inventory-secrets.

  6. Create the artifact bucket

    Creates it before elitea-main starts; omitting it causes the Deployment to crash-loop.

  7. Install the chart

    Runs helm upgrade --install with values-kind.yaml. The chart’s own pre-install hooks run the migrations: elitea-main’s -all-tenants migration and each provider’s own storage migration.

  8. 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.

Services are ClusterIP only. Reach elitea-main via port-forward with a bearer token minted from the seeded PAT:

Terminal window
kubectl --context kind-elitea-kind -n elitea port-forward svc/elitea-main 18080:8080
curl -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.

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:

  1. Every pod is Ready (completed migration Jobs are excluded — a Succeeded Job is never Ready, 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, which verify sends no request through.
  2. The provider pod’s two containers — elitea-deepwiki (host, native runner, reaches the sidecar over the shared Unix socket) and engine (the native image, fixture runner) — are both present with the runner the chart set.
  3. The facade registered the provider. A kubectl exec into PostgreSQL reads provider_hub.provider_origin_registration joined to provider_hub.provider_health_projection and expects a wikis row with healthy = true — proof that elitea-main’s provider registrar armed itself and its health probe reached the provider over the mTLS hop.
  4. 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 under acme--e2e-generated--main/.

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.

  • Both engine sidecars run the fixture runner (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 — verify authenticates 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.

.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:

Terminal window
bash deploy/kind/kind-stack.sh up
bash deploy/kind/kind-stack.sh verify

CI 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.

Terminal window
deploy/kind/kind-stack.sh down

Deletes 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 ...).