This guide documents how to run Kubernetes single-node e2e tests (e2e_node) locally on macOS using Lima. Lima creates lightweight Linux VMs that can run the full kubelet with systemd, containerd, and all dependencies required for e2e_node tests.
- Full Linux environment: e2e_node tests require systemd, cgroups v2, and a real container runtime
- Docker Desktop limitations: Nested containers don't work properly on Docker Desktop for Mac (fails with "failed to mount rootfs component: invalid argument")
- Fast iteration: Mount your local Kubernetes source code directly into the VM
- Architecture support: Works on both Intel and Apple Silicon Macs
# Using Homebrew
brew install lima
# Verify installation
limactl --versionmkdir -p ~/go/src/k8s.io
cd ~/go/src/k8s.io
git clone https://github.com/kubernetes/kubernetes.git
cd kubernetesImportant: Before starting, edit k8s-e2e-node.yaml to update the mount paths to match your local setup.
# Start the VM (first time takes ~2-3 minutes to download image and provision)
limactl start k8s-e2e-node.yaml
# Check VM status
limactl listlimactl shell k8s-e2e-nodeInside the VM:
# Navigate to Kubernetes source
cd ~/go/src/k8s.io/kubernetes
# Run a specific test
sudo -E make test-e2e-node \
FOCUS="your test pattern here" \
PARALLELISM=1 \
KUBELET_CONFIG_FILE=/etc/kubelet-config.yamlThe default make test-e2e-node command skips tests tagged with [Serial], [Slow], or [Flaky].
If your test has any of these tags (check the test name), you must run ginkgo directly:
# First, build the test binaries (run make once)
sudo -E make test-e2e-node FOCUS="." PARALLELISM=1 KUBELET_CONFIG_FILE=/etc/kubelet-config.yaml
# Then run ginkgo directly without the skip flags
sudo -E _output/local/go/bin/ginkgo \
-timeout=24h \
-focus="Does not keep device plugin assignments across node reboots" \
-skip="\[Flaky\]" \
_output/local/go/bin/e2e_node.test \
-- \
--v 4 \
--report-dir=/tmp/_artifacts \
--node-name $(hostname) \
--kubelet-flags="--cluster-domain=cluster.local" \
--dns-domain="cluster.local" \
--prepull-images=false \
--container-runtime-endpoint=unix:///run/containerd/containerd.sock \
--kubelet-config-file="/etc/kubelet-config.yaml"The Makefile runs ginkgo with these skip patterns:
-skip="\[Flaky\]|\[Slow\]|\[Serial\]"
So if you're running a test like:
[sig-node] Device Plugin [Serial] [Feature:DevicePlugin] DevicePlugin [Serial] [Disruptive] Does not keep device plugin assignments...
It will be silently skipped and show "0 of 1075 Specs" ran!
The default configuration allocates:
- CPUs: 4
- Memory: 8 GiB
- Disk: 50 GiB
Adjust these in k8s-e2e-node.yaml based on your needs. Some tests may require more memory.
The provisioning script installs:
| Component | Version | Purpose |
|---|---|---|
| Go | 1.25.5 | Build Kubernetes (update to match go.mod) |
| containerd | (Lima default) | Container runtime |
| runc | 1.2.4 | OCI runtime |
| CNI plugins | 1.6.2 | Container networking |
- containerd: Configured with
SystemdCgroup = truefor cgroups v2 compatibility - kubelet: Pre-configured with systemd cgroup driver at
/etc/kubelet-config.yaml - CNI: Bridge networking configured in
/etc/cni/net.d/
sudo -E make test-e2e-node \
FOCUS="exact test name here" \
PARALLELISM=1 \
KUBELET_CONFIG_FILE=/etc/kubelet-config.yaml# All device plugin tests (non-serial ones)
sudo -E make test-e2e-node \
FOCUS="DevicePlugin" \
PARALLELISM=1 \
KUBELET_CONFIG_FILE=/etc/kubelet-config.yaml# After building once with make, run directly:
sudo -E _output/local/go/bin/ginkgo \
-timeout=24h \
-focus="YourSerialTestPattern" \
-skip="\[Flaky\]" \
_output/local/go/bin/e2e_node.test \
-- \
--v 4 \
--report-dir=/tmp/_artifacts \
--node-name $(hostname) \
--kubelet-flags="--cluster-domain=cluster.local" \
--dns-domain="cluster.local" \
--prepull-images=false \
--container-runtime-endpoint=unix:///run/containerd/containerd.sock \
--kubelet-config-file="/etc/kubelet-config.yaml"sudo -E make test-e2e-node \
FOCUS="your test" \
PARALLELISM=1 \
KUBELET_CONFIG_FILE=/etc/kubelet-config.yaml \
2>&1 | tee /tmp/test-output.logfor i in {1..10}; do
echo "=== Run $i ==="
sudo -E _output/local/go/bin/ginkgo \
-timeout=24h \
-focus="Your Test Pattern" \
-skip="\[Flaky\]" \
_output/local/go/bin/e2e_node.test \
-- \
--kubelet-config-file="/etc/kubelet-config.yaml" \
--container-runtime-endpoint=unix:///run/containerd/containerd.sock \
2>&1 | tee /tmp/run_$i.log
if ! grep -q "1 Passed" /tmp/run_$i.log; then
echo "FAILED on run $i"
break
fi
echo "Run $i passed"
donelimactl stop k8s-e2e-nodelimactl start k8s-e2e-nodelimactl delete k8s-e2e-nodelimactl shell k8s-e2e-node
# Or use SSH directly
ssh -F ~/.lima/k8s-e2e-node/ssh.config lima-k8s-e2e-node# Copy from host to VM
limactl copy myfile.txt k8s-e2e-node:/tmp/
# Copy from VM to host
limactl copy k8s-e2e-node:/tmp/results.log ./Your test is probably tagged with [Serial] and being skipped. Run ginkgo directly without the -skip="\[Serial\]" flag. See "Running Serial/Disruptive Tests" section above.
Check containerd is running:
sudo systemctl status containerdCheck kubelet logs if it started:
sudo journalctl -u kubelet -fIf you see "port 6443 already in use", a previous test run didn't clean up:
sudo lsof -i :6443
sudo kill <PID>Verify CNI plugins are installed:
ls -la /opt/cni/bin/Check CNI configuration:
cat /etc/cni/net.d/10-containerd-net.conflistVerify cgroups v2 is in use:
mount | grep cgroupCheck containerd config has SystemdCgroup enabled:
grep SystemdCgroup /etc/containerd/config.tomlTest artifacts are written to /tmp/_artifacts/:
ls -la /tmp/_artifacts/Edit the mounts section in k8s-e2e-node.yaml:
mounts:
- location: "/path/to/your/kubernetes"
mountPoint: "/home/YOUR_USER.linux/go/src/k8s.io/kubernetes"
writable: trueNote: The mountPoint path inside the VM should match where you'll cd to run tests.
Check the go.mod file in your Kubernetes source for the required version, then edit the provisioning script:
provision:
- mode: system
script: |
GO_VERSION=1.23.0 # Change this to match go.mod
...Add to the apt-get install list in the provisioning script.
This example shows how to run the device plugin assignment test that was being investigated:
# Enter the VM
limactl shell k8s-e2e-node
# Navigate to source
cd ~/go/src/k8s.io/kubernetes
# Build test binaries first
sudo -E make test-e2e-node FOCUS="." PARALLELISM=1 KUBELET_CONFIG_FILE=/etc/kubelet-config.yaml 2>&1 | head -50
# Run the Serial test directly with ginkgo (skipping only Flaky, not Serial)
sudo -E _output/local/go/bin/ginkgo \
-timeout=24h \
-focus="Does not keep device plugin assignments across node reboots" \
-skip="\[Flaky\]" \
_output/local/go/bin/e2e_node.test \
-- \
--v 4 \
--report-dir=/tmp/_artifacts/deviceplugin \
--node-name $(hostname) \
--kubelet-flags="--cluster-domain=cluster.local" \
--dns-domain="cluster.local" \
--prepull-images=false \
--container-runtime-endpoint=unix:///run/containerd/containerd.sock \
--kubelet-config-file="/etc/kubelet-config.yaml"for i in {1..100}; do
echo "=== Run $i at $(date) ==="
sudo -E _output/local/go/bin/ginkgo \
-timeout=24h \
-focus="Does not keep device plugin assignments across node reboots" \
-skip="\[Flaky\]" \
_output/local/go/bin/e2e_node.test \
-- \
--report-dir=/tmp/_artifacts/run$i \
--node-name $(hostname) \
--container-runtime-endpoint=unix:///run/containerd/containerd.sock \
--kubelet-config-file="/etc/kubelet-config.yaml" \
2>&1 | tee /tmp/run_$i.log
if ! grep -q "1 Passed" /tmp/run_$i.log; then
echo "FLAKE DETECTED on run $i"
break
fi
echo "Run $i passed"
done- The VM uses your host's Kubernetes source via mounts, so any changes you make on the host are immediately visible in the VM
- Test artifacts are stored in
/tmp/_artifacts/inside the VM - The kubelet config at
/etc/kubelet-config.yamluses systemd cgroup driver which is required for cgroups v2 - Important:
make test-e2e-nodeskips[Serial]tests by default - use ginkgo directly for those - Each test run takes approximately 30-40 seconds
- Flaky tests in CI may be stable locally due to consistent timing in dedicated VM (vs shared GCE infrastructure)