This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Always use Jujutsu (jj) instead of Git for all version control operations in this repository.
Before committing: Run just precommit (formats, lints, tests, and regenerates docs). This is the single command to run before every jj commit. Do not run just fmt, just lint, just test, or just gendoc individually when preparing a commit — use just precommit instead.
Running just recipes: You do NOT need direnv exec . to run just recipes—they're already part of the dev environment. Only use direnv exec . when running Python introspection code (e.g., direnv exec . python3 -c ... or direnv exec . python3 << 'EOF') that depends on rebuilt dependencies in Nix.
nixkube is a Kubernetes plugin system that injects Nix stores into pods using two complementary protocols:
- CSI (Container Storage Interface) - Ephemeral volumes with explicit pod requests via volumeAttributes
- NRI (Node Resource Interface) - Container runtime hooks for automatic injection via pod annotations
The system consists of:
- Node DaemonSet - Runs on each Kubernetes node, implements both CSI and NRI protocols to mount Nix stores into pods
- Cache StatefulSet - Central cache/coordinator that manages distributed builds and binary substitution
- Optional Builder Pods - For offloading builds to dedicated builder nodes
The CSI layer supports two driver names for backwards compatibility:
- nixkube - Primary driver name (recommended for new deployments)
- nix.csi.store - Legacy driver name (enabled via
cfg.node.compat)
The project uses Nix with flake-compatish for backwards compatibility. Key build outputs are defined in default.nix:
-
Environments (
environments/): Separate Nix environments for cache and node, built using dinix (a service manager). Each environment:- Shares common services (openssh, nix-daemon, shared-setup)
- Has role-specific services defined in separate modules
- Builds for both x86_64-linux and aarch64-linux architectures
- Is deployed as a minimal container with services managed by dinit
-
Kubernetes Deployment (
kubenix/): Uses easykubenix to generate Kubernetes manifestsoptions.nix: Defines module options and builds separatecachePackageandnodePackagecache.nix: Cache StatefulSet with initContainer that copies environment artifactsdaemonset.nix: Node DaemonSet with CSI driver registration- SSH keys from
./keys/*.pubare automatically imported as authorized keys
Cache → Nodes: The cache service watches for pods labeled app.kubernetes.io/component=node and updates /etc/machines with builder DNS names (pod.name.nixkube-builders.namespace.svc.cluster.local). This enables distributed builds.
Nodes → Cache: Node pods use the cache as a binary substitute via ssh-ng://nix@pynixd?trusted=1 configured in kubenix/config.nix.
CSI Protocol: When a pod requests a volume with storePath or nixExpr or flakeRef in volumeAttributes, the node CSI driver:
- Builds/fetches the requested Nix store path
- Copies artifacts to the cache (if configured)
- Mounts the store path into the pod using bind mounts
The nixkube service is a single multi-protocol daemon running both CSI and NRI servers concurrently:
CSI Server (src/csi/):
grpclibfor async gRPC protocol implementationcsi-proto-pythonfor CSI protobuf definitions (upstream spec)- Handles explicit ephemeral volume requests via volumeAttributes
- Supports both nixkube and nix.csi.store driver names for backwards compatibility
NRI Plugin (src/nri/):
- ttrpc (transport-agnostic RPC) for NRI protocol
- Pod annotation parsing for automatic store injection
- OCI hook invocation for container initialization
- ZeroMQ-based coordination with build tasks
Shared Infrastructure:
kr8sfor Kubernetes API interactions and event reportingnri-waitfor container initialization coordination- Subprocess orchestration for Nix operations
Entry point (src/cli.py) starts both servers and features:
- Configurable logging via
/etc/nix/logging.jsonConfigMap - Environment variable controls for feature flags (ENABLE_COMPAT_DRIVER, NRI_ENABLED)
- Graceful multi-server error handling
Use the justfile for common development tasks. Run just --list to see all available recipes.
Format code before committing:
just fmtCheck formatting without changes:
just check-fmtRun Python type checker:
just lintRun Python tests:
just testThe integration test (runs in CI via .github/workflows/integration-test.yaml):
- Checks
/nix/storeis accessible in test pods - Validates CSI driver registration
- Confirms cache and node pods are operational
Build job (runs once, pushes to cachix and container registry):
- Builds and pushes Nix image
- Builds and pushes cache/node environments
- Builds and pushes scratch image
Test jobs (can run in parallel, pull from caches):
test-kind: Tests deployment on Kind cluster usingkubenixApplywithlocal="true"- Future test jobs can be added for different deployment scenarios (e.g., different K8s versions, configurations)
Build Kubernetes manifests:
just build-manifestsBuild development environment:
just build-devBuild all outputs for both architectures (requires builders):
just build-allDeploy to Kubernetes cluster (reads SSH keys from ./keys/*.pub):
just deployPush to cachix and container registry:
just pushThe development environment includes:
- Python with nixkube, csi-proto-python, kr8s
- xonsh shell
- ruff, pyright (linting/type checking)
- kluctl, stern, kubectx (Kubernetes tools)
- buildah, skopeo, regctl (container tools)
- just (for running recipes)
Enter the environment with direnv allow && direnv reload (or open a new terminal).
The Python code is in pkgs/nixkube/src/:
src/csi/server.py- CSI driver (gRPC NodeServicer implementation)src/nri/server.py- NRI plugin (ttrpc handler implementation)src/cli.py- Service entry point that runs both CSI and NRI servers
Version is managed in pkgs/nixkube/pyproject.toml and automatically imported into the Nix build.
Pods request Nix stores via CSI volumeAttributes (see README for examples):
storePath: Direct Nix store path to mountnixExpr: Nix expression to evaluate and mountflakeRef: Flake reference to build and mount
Both environments use dinit for service management with dependency chains:
- Cache:
shared-setup→nix-daemon→cache-daemon→cache-logger→cache(umbrella) - Node:
shared-setup→nix-daemon→csi-gc→csi-daemon→csi-logger→csi(umbrella)
The config-reconciler service runs continuously to sync SSH keys and Nix config from mounted volumes to runtime locations.
default.nix: Main entry point, defines all build outputsenvironments/cache/default.nix: Cache environment with shared + cache servicesenvironments/node/default.nix: Node environment with shared + CSI serviceskubenix/options.nix: Kubernetes module options and package buildskubenix/daemonset.nix: Node DaemonSet with CSI/NRI containers and registrationkubenix/cache.nix: Cache StatefulSet with build coordination serviceskubenix/builder.nix: Optional builder pods for distributed buildspkgs/nixkube/src/csi/server.py: CSI driver gRPC serverpkgs/nixkube/src/nri/server.py: NRI plugin ttrpc handlerniximage.nix: Builds the Nix container used by initContainers
The code should be approachable for both AI and beginners. When reviewing or modifying code, follow these guidelines for comments:
- ✅ Non-obvious design decisions: Why a particular approach was chosen over alternatives
- ✅ Complex algorithms: Explain the logic when it's not immediately clear from the code
- ✅ Protocol requirements: CSI interface requirements, Kubernetes API quirks, Nix behavior
- ✅ Error handling philosophy: Why fail-fast vs. graceful degradation (see
NodeUnpublishVolumefor good example) - ✅ Performance implications: Why bind mounts vs. overlayfs, concurrency limits, retry strategies
- ✅ Footguns and gotchas: Things that could easily be misunderstood or break
- ❌ Restating what's already obvious: Variable names, function names, and code structure should be self-documenting
- ❌ Duplicating log messages: Log messages serve as inline documentation - don't repeat them in comments
- ❌ Explaining standard patterns:
exp_backoff,async with lock, walrus operators, set comprehensions - trust the reader - ❌ Obvious control flow: for-else with clear error logging, early returns, simple conditionals
This project uses structlog. All log calls use a plain event string plus structured kwargs — no f-string interpolation in the event itself:
# ✅ Good — plain event + kwargs
logger.info("fetched_packages", count=n, store=str(path))
logger.debug("build_task_started", container_id=container_id, count=len(store_paths))
# ❌ Avoid — f-strings in event, or % formatting
logger.info(f"Fetched {n} packages from {path}")
logger.info("Fetched %d packages", n)Exception logging:
- Inside
exceptblocks that re-raise:logger.exception("event_name")— shorthand for ERROR + full traceback - Inside
exceptblocks that swallow:logger.warning("event_name", exc_info=True)— WARNING + full traceback - In callbacks with an exception object (not in
except):logger.error("event_name", exc_info=t.exception())
Context binding — attach per-request metadata to a logger:
log = structlog.get_logger("nixkube.nri.buildtask").bind(container_id=container_id)
log.info("build_task_started") # container_id included in every call- Integration tests over unit tests: This project orchestrates subprocess calls to Nix, rsync, mount, etc. Unit testing these would require excessive mocking and wouldn't catch real issues.
- Test in real environments: Use the GitHub Actions integration tests on actual Kind clusters to validate behavior.
- Unit tests are only valuable for pure business logic isolated from subprocess orchestration.