This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
k3d creates containerized k3s (lightweight Kubernetes) clusters using Docker. It runs multi-node k3s clusters on a single machine. The Go module is github.com/k3d-io/k3d/v5.
make build # Build for local platform -> bin/k3d
make build-debug # Build with debug symbols
make build-cross # Cross-compile for all platforms (needs gox)
make install-tools # Install required tools (golangci-lint, gox)Dependencies are vendored (-mod=vendor). The go.work workspace includes ., ./docgen, and ./tools.
When developing helper images locally, use these env vars to point k3d at your local builds instead of the published ones (see pkg/types/env.go):
K3D_IMAGE_LOADBALANCER=ghcr.io/k3d-io/k3d-proxy:dev # Override the load balancer (proxy) image
K3D_IMAGE_TOOLS=ghcr.io/k3d-io/k3d-tools:dev # Override the tools helper imagemake test # Run all unit tests
go test ./pkg/config/... -run TestProcessConfig # Run a specific test
make e2e # Run E2E tests (requires Docker, uses DIND)
make e2e -e E2E_INCLUDE="test_basic" # Run specific E2E test
make e2e -e E2E_LOG_LEVEL=trace -e E2E_FAIL_FAST=true # E2E with debug outputE2E tests are bash scripts in tests/ that run inside Docker-in-Docker containers. Unit tests use testify for assertions and go-test/deep for deep equality. Config test assets are in pkg/config/test_assets/.
Execution chain: make e2e -> tests/dind.sh (builds DIND image, starts privileged container) -> tests/runner.sh (discovers and batches tests) -> test_*.sh scripts.
Writing a new E2E test:
- Create
tests/test_<name>.sh(thetest_prefix is required for auto-discovery) - Make it executable (
chmod +x) - Follow the standard boilerplate:
#!/bin/bash CURR_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" [ -d "$CURR_DIR" ] || { echo "FATAL: no current dir (maybe running in zsh?)"; exit 1; } source "$CURR_DIR/common.sh" ### Step Setup ### LOG_FILE="$TEST_OUTPUT_DIR/$( basename "${BASH_SOURCE[0]}" ).log" exec >${LOG_FILE} 2>&1 export LOG_FILE KUBECONFIG="$KUBECONFIG_ROOT/$( basename "${BASH_SOURCE[0]}" ).yaml" export KUBECONFIG ### Step Setup ### export CURRENT_STAGE="Test | <name>" # ... test logic using $EXE, info, failed, check_clusters, etc. exit 0
- Use unique cluster/resource names to avoid collisions with parallel tests
- Always clean up resources (cluster delete, registry delete) at the end
Key helpers from tests/common.sh: info, failed, passed, highlight, check_clusters, check_multi_node, check_registry, wait_for_pod_running_by_name, wait_for_pod_running_by_label, exec_in_node, docker_assert_container_label, k3s_assert_node_label.
E2E environment variables: E2E_INCLUDE (run only named tests), E2E_EXCLUDE (skip named tests), E2E_PARALLEL (default: 4), E2E_EXTRA (run extra_test_* files), E2E_FAIL_FAST, E2E_LOG_LEVEL, E2E_K3S_VERSION, E2E_DIND_VERSION, E2E_KEEP (keep runner container).
make fmt # Fix formatting (gofmt)
make check-fmt # Check formatting
make lint # Run golangci-lint (v2.4.0)
make ci-lint # Same with 5-minute timeout (CI)
make check # check-fmt + lintThree-layer design:
-
CLI layer (
cmd/) - Cobra commands for cluster, node, registry, kubeconfig, image, config, debug subcommands. Entry point:cmd/root.go->NewCmdK3d(). -
Client layer (
pkg/client/) - Business logic and orchestration. Key files:cluster.go(ClusterCreate/Start/Delete/Run),node.go(NodeCreate/Start/Delete),registry.go,loadbalancer.go,kubeconfig.go. -
Runtime layer (
pkg/runtimes/) - Container runtime abstraction via theRuntimeinterface inruntime.go. Only Docker is implemented (pkg/runtimes/docker/). The Docker runtime translates k3d types to Docker API calls (translate.go).
pkg/types/- Core domain types:Cluster,Node,Registry,Role(ServerRole, AgentRole, LoadBalancerRole, RegistryRole). Extensive label system (k3d.cluster,k3d.role, etc.) used to track Docker container metadata.pkg/config/- Versioned config system (current:v1alpha5, API versionk3d.io/v1alpha5). Includes migration (migrate.go), JSON schema validation (schema.jsonembedded via//go:embed), and config processing/transformation pipeline.pkg/actions/- Node lifecycle hooks (e.g.,WriteFileAction).pkg/logger/- Logging via logrus.
proxy/- nginx-based load balancer with confd for dynamic upstream config. Built asghcr.io/k3d-io/k3d-proxy.tools/- Separate Go module (tools/) for the k3d-tools helper binary. Built asghcr.io/k3d-io/k3d-tools.
Config files are YAML with apiVersion: k3d.io/v1alpha5 and kind: Simple. The processing pipeline: read YAML -> validate JSON schema -> migrate from older versions if needed -> transform to internal types -> merge with defaults. Config versions: v1alpha2 (legacy) -> v1alpha3 -> v1alpha4 -> v1alpha5 (current).
Version info is injected at build time via LDFLAGS into version/version.go:
version.Version- git tagversion.K3sVersion- latest stable k3s version (fetched from k3s update channel)version.HelperVersionOverride- optional override for helper image versions
This project is MIT licensed (Copyright 2019-2023 Thorsten Klein). All new Go source files must include the MIT copyright header present in existing files. See CONTRIBUTING.md for guidelines — check existing issues/PRs before opening new ones to avoid duplicates. The project follows the Contributor Covenant Code of Conduct (CODE_OF_CONDUCT.md).
- All Go source files have the MIT license copyright header
- Import alias:
k3d "github.com/k3d-io/k3d/v5/pkg/types"is used throughout as the canonical import for the types package - Docker is the only runtime, but all container operations go through the
Runtimeinterface - Node types are identified by
Roleand tracked via Docker container labels
Before starting work on any issue:
- Check for duplicates — search open and closed issues for the same topic (
gh issue list -S "keyword"/gh issue list -S "keyword" --state closed) - Check for existing PRs — look for open PRs that already address the issue (
gh pr list -S "keyword") - Check if it's a k3d issue — many reports are actually k3s, Docker, or CNI issues. If so, redirect the reporter to the appropriate upstream repo
- Read the full thread — issues often evolve through discussion; the original request may have been refined or scoped down
When replying to issues:
- For questions: answer concisely, point to env vars/flags/docs, close if resolved
- For bugs: confirm reproducibility scope, check if already fixed on
main - For features: check if there's a workaround, note if a PR would be welcome
When working on PRs:
- Check the linked issue — understand the full context and any decisions made in discussion
- Check the PR's CI status — don't start reviewing or building on a PR that's failing CI for unrelated reasons
- Check for related/conflicting PRs — look for other open PRs touching the same files (
gh pr list+ review changed files) - Verify the branch is up to date with
mainbefore investing effort
Scope awareness:
- k3d only controls the container orchestration layer around k3s. Issues about k3s behavior, CNI plugins, Kubernetes internals, or Docker engine bugs are upstream concerns
- The proxy (
k3d-proxy) and tools (k3d-tools) images are part of this project — issues about those are in scope - Registry issues may involve the upstream
registryimage vs k3d's registry wiring — distinguish which layer is at fault
For detailed analysis beyond this summary, see .planning/codebase/:
| Document | Contents |
|---|---|
STACK.md |
Full dependency inventory, Go version, build toolchain, Docker SDK usage |
ARCHITECTURE.md |
Layer diagrams, data flow through CLI->Client->Runtime, abstraction boundaries |
STRUCTURE.md |
Complete directory layout, file naming conventions, package organization |
CONVENTIONS.md |
Error handling patterns, logging conventions, code style, naming rules |
TESTING.md |
Unit test patterns, E2E test framework details, test helpers, fixture locations |
INTEGRATIONS.md |
Docker API interaction, k3s channel server, registry (wharfie), kubeconfig handling |
CONCERNS.md |
Technical debt, single-runtime limitation, config migration complexity, known fragile areas |