Start here
Quickstart
Install labctl, stand up a cluster, run your first scenario.
README.mdOn this page
SnowOps Labs is a Kubernetes platform-engineering simulator. It stands up a realistic, production-shaped cluster on your laptop in minutes, breaks it in realistic ways, and grades you on how you fix it.
Practise the things production never lets you practise: diagnosing a CrashLoopBackOff with a pager going off, draining a node under load without breaking the SLO, deciding between two service meshes with evidence instead of opinion.
Early release. The core loop — stand up a cluster, run a scenario, break it, fix it, tear it down — works today; expect rapid iteration and the odd rough edge, and please file issues. See
docs/PRODUCT.mdfor what it is and who it is for, anddocs/architecture/ARCHITECTURE.mdfor how it works.Runs on macOS (Apple Silicon and Intel), Linux, and Windows via WSL2 — see Running on WSL. Local clusters (
k3d/kind); aSnowOps Labsproject.
The four loops
| Loop | What you do | Command |
|---|---|---|
| Build | Stand up a cluster and install a real platform stack from swappable providers | labctl init |
| Simulate | Activate a declarative scenario with objectives and verifiable checks | labctl scenario up <name> |
| Break | Inject a realistic, reversible fault while traffic flows and alerts fire | labctl incident inject <name> |
| Measure | Grade the outcome — checks passed, time taken, hints used | labctl scenario verify <name> |
Everything you install is the real thing: actual Prometheus, actual Istio, actual Kafka. Failures are injected into real systems, so the signal you debug is the signal you would see in production.
What's inside
| Layer | What it does |
|---|---|
| Runtimes | k3d (the golden path), kind (headless, powers CI), incluster (shared team server) |
| Platform | Ingress (Traefik/Nginx), monitoring (Prometheus + Grafana), logging (Loki), tracing (Tempo), GitOps (ArgoCD), mesh (Istio/Linkerd), data (Kafka/Postgres), secrets (Vault/ESO), autoscaling (KEDA), cost (OpenCost), security (Kyverno, cert-manager), chaos (Chaos Mesh) |
| Scenarios | Declarative playgrounds with objectives and machine-verifiable checks |
| Incidents | Reversible production faults with progressive hints and MTTR measurement |
| Learn | Ordered paths chaining scenarios and incidents into a curriculum |
| Challenges | Timed, graded runs with hidden hints |
| Apps | go-api (HTTP + metrics + tracing), echo-server (HTTP + Redis) |
| CLI + UI | labctl — one binary with the web dashboard embedded |
See it in action
Every asset below is real labctl output and the actual embedded UI — no mockups.
Inspect a scenario before you run it. labctl scenario info shows exactly
what a scenario installs — its stages, objectives, the machine-checked
verifications that grade you, and copy-paste explore hints:
Drive it from a browser. labctl ui serves an embedded web dashboard at
http://localhost:3939 — no extra install — and it follows your system
light/dark theme:
More of the dashboard — click any thumbnail for full size:
Prerequisites
You need two things before anything else: Docker with enough memory, and the
cluster tools (kubectl, helm 3, k3d). labctl installs the cluster
tools for you — pin versions live in config/versions.env
— so the only manual step is giving Docker enough resources.
Docker needs at least 4 CPUs and 8 GB of memory. labctl init runs a 3-node
k3d cluster plus the full platform stack (Prometheus, Grafana, Alertmanager,
Loki, …). The 2 GB a fresh Docker VM ships with is not enough; it shows up as
API-server "TLS handshake timeout" errors partway through the install. Set the
resources the way your platform expects:
macOS
- Docker Desktop: Settings → Resources → raise Memory to 8 GB (and CPUs to 4), then Apply & Restart.
- Colima (lightweight CLI alternative):
colima start --cpu 4 --memory 8
Linux
- Docker runs natively, so it already uses your machine's CPU and RAM — no VM to resize. Just make sure the host has ≥4 CPUs and ≥8 GB free.
- Install Docker Engine and add yourself to the
dockergroup (sudo usermod -aG docker $USER, then log out and back in) solabctlcan talk to the daemon withoutsudo.
Windows (WSL2)
- Run everything inside WSL2 (Ubuntu recommended) — never native Windows PowerShell. Treat the WSL shell as a normal Linux box for every command below.
- Either enable Docker Desktop → Settings → Resources → WSL Integration for
your distro, or run a native Docker daemon inside WSL. The same ≥4 CPU / 8 GB
applies — set it in
.wslconfigon the Windows side if you use a native daemon. Full details in Running on WSL.
Building from source? Contributors additionally need Go 1.24+ and Node 22+. End users following the Quickstart below do not — the released
labctlbinary already has the UI embedded.
Quickstart
The whole lab is driven by one command: labctl. Getting started is two
steps — (1) get the labctl binary and (2) check out the repo — then you
run the loop. make is optional; every step below uses labctl directly.
Why you still need the repo.
labctlruns the scripts underscenarios/ platform/ runtimes/ bootstrap/andsrc/engine/at runtime, so the release archive (which ships only the binary) is not standalone. You get the binary from a release and clone the repo — the binary just saves you from installing Go/Node and building it yourself.
1. Install labctl
Recommended: download a release (no Go/Node, no building). This is the easy
path for everyone who isn't changing labctl itself. Grab the archive for your
platform from the Releases page, verify the checksum, and move the
binary onto your PATH.
Pick the block for your platform (the one-liner auto-detects your OS/arch):
macOS or Linux
# Set the version you want from the Releases page, then run the block as-is.
VERSION=1.0.0
OS=$(uname -s | tr '[:upper:]' '[:lower:]') # darwin | linux
ARCH=$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/') # amd64 | arm64
BASE="https://github.com/sagar2395/snowopslabs/releases/download/v${VERSION}"
curl -fsSL "${BASE}/labctl_${VERSION}_${OS}_${ARCH}.tar.gz" -o labctl.tar.gz
curl -fsSL "${BASE}/checksums.txt" -o checksums.txt
# verify: sha256sum on Linux, shasum -a 256 on macOS
(command -v sha256sum >/dev/null && sha256sum -c checksums.txt --ignore-missing) \
|| shasum -a 256 -c checksums.txt --ignore-missing
tar xzf labctl.tar.gz labctl
sudo mv labctl /usr/local/bin/ # now `labctl` runs from anywhere
labctl --versionWindows (inside your WSL2 shell) — there is no separate Windows build; WSL is
Linux, so run the macOS or Linux block above exactly as written from inside
your distro (it resolves to the linux archive automatically).
Alternative: build from source (contributors, or anyone who wants the latest
main). Needs Go 1.24+ and Node 22+:
make cli-build # builds bin/labctl (UI embedded); run it as ./bin/labctl
make cli-install # …or install onto PATH: copies to $(go env GOPATH)/bin/labctlThe Go source lives under
src/(the rootmaketargets delegate there), somake cli-buildleaves the binary at./bin/labctl. Usemake cli-install(ensure$(go env GOPATH)/binis on yourPATH) to get a plainlabctl.
2. Check out the repo and create your config
git clone https://github.com/sagar2395/snowopslabs.git && cd snowopslabs
cp config/.env.example .envRun labctl from inside the repo (it auto-detects the project root), or from
anywhere with --project-dir /path/to/snowopslabs.
3. Run the loop — no make required
labctl doctor # check your OS has Docker + tools ready (fixes are printed inline)
labctl setup-tools # install kubectl/helm/k3d for your OS (or skip; init does it)
labctl init # setup-tools + create the cluster + install the platform
labctl hosts add # map *.${DOMAIN_SUFFIX:-k3d.local} -> 127.0.0.1 (needs sudo; one-time)
labctl app build go-api # build the image and import it into the cluster
labctl app deploy go-api # deploy it (needs the image built first)
labctl scenario list # then: labctl scenario info <name> for details
labctl scenario up observability-sre
labctl scenario verify observability-sre # did you achieve the objective?
labctl validate # check all content is well-formed
labctl ui # dashboard at http://localhost:3939
labctl teardown # remove everything (never hangs)Exposing the UI on a network.
labctl uibinds127.0.0.1by default and serves plain HTTP — fine for a local machine. To reach it from another host, pass--bind 0.0.0.0; the server then requires authentication (setLABCTL_AUTH=trueand add a user withlabctl users add) and refuses to start otherwise, so an unauthenticated cluster-control API is never exposed. For TLS, pass--tls-certand--tls-key.Prefer
make?make init/make teardown/make resetare equivalent to thelabctl init/labctl teardown/labctl resetcommands.
labctl app deployinstalls the Helm release but does not build the image — runlabctl app build <name>first (it builds and imports into k3d), or the pod lands inImagePullBackOff.labctl hosts addwrites a managed block to/etc/hostsso the ingress hostnames below resolve; skip it and URL-based access (and thegrafana-reachablescenario check) will fail withno such host. It is not needed for the labctl UI itself (a direct port).Want to scrape labctl itself? Start the server with
LABCTL_METRICS=trueto expose an optional Prometheus endpoint at/metrics(off by default). See the CLI reference.
Troubleshooting
| Symptom | Fix |
|---|---|
Browser can't reach *.k3d.local URLs (no such host) |
Ingress routes by hostname, which needs a local DNS entry. Run labctl hosts add once (sudo). The labctl UI at http://localhost:3939 never needs this. No sudo? Set DOMAIN_SUFFIX=127.0.0.1.nip.io in .env (a wildcard DNS that resolves to localhost, needs internet) and re-run labctl platform up. |
required app(s) not deployed when starting a scenario/challenge |
The scenario needs an app that isn't running. Deploy it (labctl app build <name> && labctl app deploy <name>) or re-run with --deploy-prereqs to do it automatically. |
| Something is wedged and you want a clean slate | labctl reset (teardown + init) rebuilds the lab; labctl scenario down <name> / labctl incident resolve undo a single activation. |
TLS handshake timeout / OOM-killed pods partway through init |
The Docker VM is too small. Give it ≥4 CPU / 8 GB (colima start --cpu 4 --memory 8, or Docker Desktop → Resources), then labctl reset. |
init seems stuck |
First run pulls many images. Watch progress with kubectl get pods -A -w. |
| A tool is missing or too old | labctl doctor names each missing/outdated tool and how to fix it. |
| Teardown left something behind | labctl teardown is safe to re-run; for k3d/kind it deletes the whole cluster. |
| Which runtime should I use? | k3d (default, local), kind (CI parity), incluster (team server). See runtime profiles. |
On WSL, labctl ui doesn't open a browser |
Install wslu (sudo apt install wslu) so wslview can hand the URL to Windows; labctl falls back to powershell.exe/cmd.exe automatically. Or open http://localhost:3939 in your Windows browser manually. |
On WSL, *.k3d.local URLs work in WSL but not the Windows browser |
The Windows browser reads the Windows hosts file. Add the same 127.0.0.1 <host> lines to C:\Windows\System32\drivers\etc\hosts (as Administrator), or use DOMAIN_SUFFIX=127.0.0.1.nip.io. labctl doctor prints the full WSL guidance. |
Running on WSL
SnowOps Labs runs on WSL2 as a normal Linux environment — install the Linux tools inside your distro and use it exactly as on Linux. Two Windows-specific gotchas to know:
- Docker. Either enable Docker Desktop → Settings → Resources → WSL
Integration for your distro, or run a native Docker daemon inside WSL. The
same ≥4 CPU / 8 GB requirement applies (set it in
.wslconfigon the Windows side if you use a native daemon). - Reaching services from a Windows browser.
- The UI (
labctl ui,http://localhost:3939) just works — WSL2 forwardslocalhostto Windows.labctl uiopens it viawslview(installwslu:sudo apt install wslu) and falls back topowershell.exe/cmd.exe. - Ingress hostnames (
grafana.k3d.local, …) opened in a Windows browser resolve against the Windows hosts file, not WSL's. Add the entries toC:\Windows\System32\drivers\etc\hosts(as Administrator), reach them from inside WSL withcurl(wherelabctl hosts addapplies), or setDOMAIN_SUFFIX=127.0.0.1.nip.ioin.envto avoid hosts edits entirely. - If WSL rewrites
/etc/hostson restart, addgenerateHosts=falseunder[network]in/etc/wsl.conf.
- The UI (
labctl doctor detects WSL and prints this checklist.
Project structure
The repository root is a content/authoring workspace: it surfaces what a user
of the project edits (scenarios, incidents, apps, platform). The Go/labctl
source is self-contained under src/, so a later "the engine gets its own repo"
split is a straight lift of that directory (issue #7).
# User-facing content and config at the root
scenarios/ incidents/ declarative content
learn/ challenges/
platform/<cat>/<prov>/ install.sh / uninstall.sh / status.sh / values.yaml
runtimes/<profile>/ k3d | kind | incluster
apps/<name>/ sample workloads
bootstrap/ tool installation
config/ versions.env, .env.example (non-dotfile config)
docs/ PRODUCT, ROADMAP, TESTING, architecture/, adr/, runbooks/
Makefile make/ thin root Makefile + orchestration/shell targets
# The labctl Go module — self-contained under src/
src/go.mod module github.com/sagar2395/snowopslabs (imports unchanged)
src/Makefile src/make/ build + test targets (delegated to from the root)
src/cmd/labctl/ entrypoint
src/internal/cli/ cobra commands — thin adapters
src/internal/httpapi/ REST/WS — thin adapters
src/internal/run/ durable run engine (cancel, timeouts, locks, logs)
src/internal/store/ SQLite persistence
src/internal/toolchain/ kubectl/helm/k3d adapters + fakes for tests
src/pkg/ public SDK: checks, scenario types, extension seam
src/ui/ React SPA, embedded into the binary
src/engine/ app build/deploy/check strategy scripts
src/services/ shared services (redis, pager, traffic)
src/test/shell/ bats suites with kubectl/helm stubbedlabctl discovers content by walking up from the working directory to the
content root (it keys on scenarios/ + runtimes/), so it still runs unchanged
from the repo root even though the binary is built under src/.
Documentation
| Document | What it covers |
|---|---|
| Product | What SnowOps Labs is, who it's for, what's out of scope |
| Roadmap | The wave plan, exit criteria, Definition of Done |
| Architecture | How the system fits together |
| Decisions | Why it was built this way, with alternatives considered |
| Testing | The four mandatory test layers and CI gates |
| Runbooks | Hands-on validation a human performs before a wave merges |
| CLI Reference | Every labctl command and flag |
| Runtime profiles | k3d / kind / incluster and the profile contract |
| Releasing | How a versioned release is cut (goreleaser) |
| Scenarios | The scenario format and how to author one |
| Authoring | Your first scenario, extension seams, stability policy |
| Contributing | Golden rules and the PR bar |
Testing
All four layers are mandatory for every change — see docs/TESTING.md.
make test # Go unit + shell, race detector, coverage gate
make test-ui # vitest component tests
make test-e2e # playwright journeys
make lint # every static-analysis gateConfiguration
Global settings live in .env (from config/.env.example):
PROFILE=k3d # k3d | kind | incluster
CLUSTER_NAME=snowops
INGRESS_PROVIDER=traefik # traefik | nginx
METRICS_PROVIDER=prometheusPer-app settings live in apps/<name>/app.env:
BUILD_STRATEGY=docker
DEPLOY_STRATEGY=helm
HELM_VALUES=values-dev.yaml # values-dev | values-prod-like | values-testOwn scenarios stay in your own repository — point SNOWOPS_CONTENT_PATH at
them and they appear alongside the built-in catalog. No pack format, no
registry (ADR-0008).
Bring your own app. Drop a directory under apps/<name>/ with an app.env
(as above) and, for the Helm strategy, a chart under apps/<name>/deploy/helm/.
It shows up in labctl app list and builds/deploys/destroys like the samples:
labctl app build <name> && labctl app deploy <name>Reference it from a scenario's prerequisites.apps and labctl scenario up
will check it's deployed (and, with --deploy-prereqs, deploy it for you).
Key URLs (k3d)
| Service | URL | Credentials |
|---|---|---|
| go-api | http://go-api.k3d.local | — |
| echo-server | http://echo-server.k3d.local | — |
| Grafana | http://grafana.k3d.local | admin / admin |
| Prometheus | http://prometheus.k3d.local | — |
| ArgoCD | http://argocd.k3d.local | admin / (see install output) |
| SnowOps Labs UI | http://localhost:3939 | — |
Domains follow ${DOMAIN_SUFFIX:-k3d.local} — never hardcoded. Run
labctl hosts add once (it needs sudo) so these hostnames resolve to
127.0.0.1; labctl hosts remove cleans the managed block back out.
License
Apache-2.0 — see LICENSE, NOTICE and TRADEMARKS.md.