Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions k8s-tools/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# k8s-tools

A mixin kit that installs a complete Kubernetes toolchain inside the sandbox: **kubectl** v1.36.2, **helm** v4.2.2, **kind** v0.32.0, **k9s** v0.51.0, and **kustomize** v5.8.1. Every binary is downloaded from its official release channel, pinned to an exact version, and verified against the SHA256 checksum the upstream publishes. Pair it with any agent (Claude, Gemini, …) to let the agent spin up a throwaway Kubernetes cluster with `kind create cluster`, deploy to it with kubectl/helm/kustomize, and debug it with k9s — all without touching any cluster outside the sandbox.

## Usage

```console
$ sbx run claude --kit ./k8s-tools/ .
```

Or straight from this repository:

```console
$ sbx run --kit "git+https://github.com/docker/sbx-kits-contrib.git#dir=k8s-tools" claude
```

Prerequisites:

- Installing the binaries needs nothing beyond the kit itself.
- **Creating a cluster** (`kind create cluster`) needs a Docker daemon inside the sandbox. Use a sandbox template that ships one (the `*-docker` templates); on templates without a daemon the tools still install fine, but `kind create cluster` will fail to connect to Docker.

Inside the sandbox:

```console
$ kind create cluster # ~1 min; pulls kindest/node from Docker Hub
$ kubectl get nodes # context already points at kind-kind
$ helm install my-app <chart>
$ k9s # TUI debugging
$ kind delete cluster
```

## How it works

### Why kind, not minikube

kind runs a whole Kubernetes node as a single Docker container, so it works anywhere a Docker daemon exists — including the sandbox's own daemon. minikube's preferred drivers (VMs, or its own docker-machine plumbing) assume more host control than a sandbox provides, and its Docker driver is a heavier, slower path to the same result. kind is also what Kubernetes itself uses for CI, is a single static binary, and needs zero configuration for a one-node throwaway cluster.

### Why these versions are pinned by digest

Kits are cached in user workflows and re-run on every sandbox creation, so a floating `latest` would make sandbox builds non-reproducible and would silently pick up upstream changes. Each install command pins an exact version **and** verifies the download against the SHA256 published by the upstream for that release (`SHASUMS`/`checksums.txt`/`.sha256` files). To bump a tool, change its `*_VERSION` and the two per-arch `SHA256` values in `spec.yaml`.

### Architecture detection

Sandboxes run linux/amd64 on CI and Intel hosts, linux/arm64 on Apple Silicon. Each install command reads `dpkg --print-architecture` and selects the matching release asset and checksum, failing loudly on anything else — the same pattern the `mise`, `task`, and `trivy` kits use.

### Why /dev/kmsg is created at startup

The sandbox microVM does not populate `/dev/kmsg`, and kubelet inside a kind node hard-fails without it (`open /dev/kmsg: no such file or directory` in a crash loop, surfacing as `kind create cluster` timing out waiting for the control plane). The kit ships an idempotent `commands.startup` entry that runs `mknod /dev/kmsg c 1 11` when the device is missing. It's a startup (not install) command because `/dev` is rebuilt on every container start.

### Why these domains

`caps.network.allow` is the kit's complete outbound contract — CI runs e2e under a `deny-all` policy. Every host below was verified against `sbx policy log` output from a real install + `kind create cluster` + `registry.k8s.io` image pull:

| Domain | Why |
| --- | --- |
| `dl.k8s.io` | kubectl release download (install time; serves the binary directly, no CDN redirect observed) |
| `get.helm.sh` | helm release tarball (install time) |
| `github.com` | kind / k9s / kustomize release URLs (install time) |
| `release-assets.githubusercontent.com` | github.com 302-redirects release assets here |
| `auth.docker.io` | Docker Hub auth token when kind pulls `kindest/node` (runtime) |
| `registry-1.docker.io` | Docker Hub manifests for `kindest/node` (runtime) |
| `production.cloudfront.docker.com` | Docker Hub blob CDN observed in the policy log today (runtime) |
| `production.cloudflare.docker.com` | Docker Hub's other blob CDN — kept so the kit survives Docker routing between the two |
| `registry.k8s.io` | Kubernetes system images referenced by common manifests/charts (runtime; serves manifests, redirects blobs) |
| `*.pkg.dev` | registry.k8s.io redirects blobs to per-region Google Artifact Registry hosts (e.g. `asia-south1-docker.pkg.dev`) |

**Known gap:** registry.k8s.io also routes some blob downloads to per-region Amazon S3 dualstack hosts (e.g. `prod-registry-k8s-io-ap-south-1.s3.dualstack.ap-south-1.amazonaws.com`). Those are multi-label hostnames that the current allowlist grammar cannot express (`**.<domain>` support is pending), so an S3-routed pull fails under `deny-all`. `kind create cluster` is unaffected — the `kindest/node` image bakes in all control-plane images — but if a workload image pull from registry.k8s.io fails, run `sbx policy log <sandbox>`, and add your region's blocked host to a fork of this kit.

No apt packages are installed, so the Ubuntu/Docker apt mirrors are deliberately absent. Pulling images from registries not listed here (ghcr.io, quay.io, …) will fail under a deny-all policy — fork the kit and extend `caps.network.allow` for the registries your workloads need.

## Cleanup

Everything is sandbox-local. kind clusters are Docker containers on the sandbox's own daemon — they never touch the host's Docker, host kubeconfig, or any real cluster, and they disappear with the sandbox (`sbx rm <name>`). Inside a long-lived sandbox, `kind delete cluster` frees the resources.
174 changes: 174 additions & 0 deletions k8s-tools/spec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
schemaVersion: "2"
kind: mixin
name: k8s-tools
displayName: Kubernetes Tools
description: "Kubernetes toolchain for sandbox agents — kubectl, helm, kind, k9s, and kustomize, all version+digest pinned. kind clusters run on the sandbox's Docker daemon, so the whole cluster lives (and dies) inside the sandbox."

caps:
network:
allow:
# kubectl release binary (install time). Serves the binary directly —
# no CDN redirect observed (`sbx policy log`, July 2026).
- "dl.k8s.io:443"
# helm release tarballs (install time, official Helm CDN).
- "get.helm.sh:443"
# kind / k9s / kustomize release downloads (install time): github.com
# 302-redirects binary assets to release-assets.githubusercontent.com.
- "github.com:443"
- "release-assets.githubusercontent.com:443"
# Runtime: `kind create cluster` pulls the kindest/node image from
# Docker Hub — auth token from auth.docker.io, manifests from
# registry-1.docker.io, blobs from the Docker CDN. The CDN host
# observed via `sbx policy log` today is production.cloudfront.docker.com;
# the cloudflare host is Docker Hub's other blob CDN, kept so the kit
# survives Docker routing between the two.
- "auth.docker.io:443"
- "registry-1.docker.io:443"
- "production.cloudfront.docker.com:443"
- "production.cloudflare.docker.com:443"
# Runtime: Kubernetes system images (pause, coredns, metrics-server,
# ingress-nginx, …) referenced by common manifests and charts resolve
# from registry.k8s.io, which serves manifests itself and redirects
# blobs to per-region backends — Google Artifact Registry
# (<region>-docker.pkg.dev, covered by the wildcard) or Amazon S3
# dualstack hosts, which are multi-label and NOT expressible in the
# current allowlist grammar (`**.` is pending). S3-routed blobs will
# be blocked under deny-all; see the README for the workaround.
- "registry.k8s.io:443"
- "*.pkg.dev:443"

commands:
install:
# Each tool is installed from a pinned release with its SHA256 verified
# against the checksum the upstream publishes for that release. To bump a
# tool: change its *_VERSION and the per-arch SHA256 pair below.
- command: |
set -euo pipefail
KUBECTL_VERSION=1.36.2
ARCH=$(dpkg --print-architecture)
case "$ARCH" in
amd64) SHA256="1e9045ec32bea85da43de85f0065358529ea7c7a152eca78154fba5b58c27d82" ;;
arm64) SHA256="c957eb8c4bea27a3bb35b269edd9082e27f027f7b76b20b5bf4afebc726c6d3e" ;;
*)
echo "unsupported sandbox arch: $ARCH (expected amd64 or arm64)" >&2
exit 1
;;
esac
URL="https://dl.k8s.io/release/v${KUBECTL_VERSION}/bin/linux/${ARCH}/kubectl"
curl --proto '=https' --tlsv1.2 -fsSL -o /tmp/kubectl "$URL"
echo "${SHA256} /tmp/kubectl" | sha256sum -c -
install -m 0755 /tmp/kubectl /usr/local/bin/kubectl
rm /tmp/kubectl
kubectl version --client
user: "0"
description: "Install kubectl v1.36.2 from dl.k8s.io, version+digest pinned"
- command: |
set -euo pipefail
HELM_VERSION=4.2.2
ARCH=$(dpkg --print-architecture)
case "$ARCH" in
amd64) SHA256="9adafecab4d406853bba163a70e9f104f47dbbf65ce24b7653bae7e36150bcb6" ;;
arm64) SHA256="78803142087a0069fa4b50d3f32a84d3ef25c14d1ee8a40fbccf86a6216d2f36" ;;
*)
echo "unsupported sandbox arch: $ARCH (expected amd64 or arm64)" >&2
exit 1
;;
esac
URL="https://get.helm.sh/helm-v${HELM_VERSION}-linux-${ARCH}.tar.gz"
curl --proto '=https' --tlsv1.2 -fsSL -o /tmp/helm.tgz "$URL"
echo "${SHA256} /tmp/helm.tgz" | sha256sum -c -
tar -C /tmp -xzf /tmp/helm.tgz "linux-${ARCH}/helm"
install -m 0755 "/tmp/linux-${ARCH}/helm" /usr/local/bin/helm
rm -rf /tmp/helm.tgz "/tmp/linux-${ARCH}"
helm version
user: "0"
description: "Install helm v4.2.2 from get.helm.sh, version+digest pinned"
- command: |
set -euo pipefail
KIND_VERSION=0.32.0
ARCH=$(dpkg --print-architecture)
case "$ARCH" in
amd64) SHA256="50030de23cf40a18505f20426f6a8506bedf13c6e509244bd1fa9463721b0f54" ;;
arm64) SHA256="b92cd615e97585de8ddade28ed5cd7feb4248d717c233eea5b03c37298900f5d" ;;
*)
echo "unsupported sandbox arch: $ARCH (expected amd64 or arm64)" >&2
exit 1
;;
esac
URL="https://github.com/kubernetes-sigs/kind/releases/download/v${KIND_VERSION}/kind-linux-${ARCH}"
curl --proto '=https' --tlsv1.2 -fsSL -o /tmp/kind "$URL"
echo "${SHA256} /tmp/kind" | sha256sum -c -
install -m 0755 /tmp/kind /usr/local/bin/kind
rm /tmp/kind
kind version
user: "0"
description: "Install kind v0.32.0 from GitHub releases, version+digest pinned"
- command: |
set -euo pipefail
K9S_VERSION=0.51.0
ARCH=$(dpkg --print-architecture)
case "$ARCH" in
amd64) SHA256="c3752ad51a5a4015a113819c4eeb6e55a4d0e4b8e652494797532f6fc8161dd7" ;;
arm64) SHA256="3ee05c82e5f9198928a4e86133608ba6a2c10a2244d6a7789e820f78319d640c" ;;
*)
echo "unsupported sandbox arch: $ARCH (expected amd64 or arm64)" >&2
exit 1
;;
esac
URL="https://github.com/derailed/k9s/releases/download/v${K9S_VERSION}/k9s_Linux_${ARCH}.tar.gz"
curl --proto '=https' --tlsv1.2 -fsSL -o /tmp/k9s.tgz "$URL"
echo "${SHA256} /tmp/k9s.tgz" | sha256sum -c -
tar -C /tmp -xzf /tmp/k9s.tgz k9s
install -m 0755 /tmp/k9s /usr/local/bin/k9s
rm /tmp/k9s.tgz /tmp/k9s
k9s version
user: "0"
description: "Install k9s v0.51.0 from GitHub releases, version+digest pinned"
- command: |
set -euo pipefail
KUSTOMIZE_VERSION=5.8.1
ARCH=$(dpkg --print-architecture)
case "$ARCH" in
amd64) SHA256="029a7f0f4e1932c52a0476cf02a0fd855c0bb85694b82c338fc648dcb53a819d" ;;
arm64) SHA256="0953ea3e476f66d6ddfcd911d750f5167b9365aa9491b2326398e289fef2c142" ;;
*)
echo "unsupported sandbox arch: $ARCH (expected amd64 or arm64)" >&2
exit 1
;;
esac
URL="https://github.com/kubernetes-sigs/kustomize/releases/download/kustomize%2Fv${KUSTOMIZE_VERSION}/kustomize_v${KUSTOMIZE_VERSION}_linux_${ARCH}.tar.gz"
curl --proto '=https' --tlsv1.2 -fsSL -o /tmp/kustomize.tgz "$URL"
echo "${SHA256} /tmp/kustomize.tgz" | sha256sum -c -
tar -C /tmp -xzf /tmp/kustomize.tgz kustomize
install -m 0755 /tmp/kustomize /usr/local/bin/kustomize
rm /tmp/kustomize.tgz /tmp/kustomize
kustomize version
user: "0"
description: "Install kustomize v5.8.1 from GitHub releases, version+digest pinned"
startup:
# The sandbox microVM does not populate /dev/kmsg, and kubelet inside a
# kind node hard-fails without it ("open /dev/kmsg: no such file or
# directory"). /dev is rebuilt on every container start, so this must be
# a startup command, kept idempotent.
- command: ["sh", "-c", "[ -e /dev/kmsg ] || mknod /dev/kmsg c 1 11"]
user: "0"
description: "Create /dev/kmsg if missing (required by kubelet inside kind nodes)"

agentContext: |
## Kubernetes tools

Installed and on PATH: kubectl v1.36.2, helm v4.2.2, kind v0.32.0,
k9s v0.51.0, kustomize v5.8.1.

- Create a local cluster with `kind create cluster`. It runs on the
sandbox's Docker daemon and automatically points the kubectl context at
the new cluster (`kind-kind`). Verify with `kubectl get nodes`.
- Deploy with `kubectl apply -f <manifest>`, `helm install <name> <chart>`,
or `kubectl apply -k <kustomization-dir>`.
- Use `k9s` for interactive TUI debugging of a running cluster.
- Tear down with `kind delete cluster`. Clusters exist only inside the
sandbox and vanish with it.
- Image pulls are limited by sandbox network policy: Docker Hub and
registry.k8s.io are allowed; pulls from other registries will fail with
a proxy error unless their domains are added to the kit's
`caps.network.allow`.