rules_kubectl
Bazel rules for a hermetic kubectl.
The module does two things and nothing else:
- it fetches
kubectlfor the host and exposes it as@kubectl//:kubectlforbazel run, and - it defines
@rules_kubectl//kubectl:toolchain_typeso that any rule set needingkubectlin an action can share one binary instead of fetching its own.
There are no kubectl_apply style rules here. Opinions about kubeconfigs,
contexts and namespaces belong in the rule sets built on top of the toolchain.
Setup
bazel_dep(name = "rules_kubectl", version = "0.1.0")
kubectl = use_extension("@rules_kubectl//kubectl:extensions.bzl", "kubectl")
kubectl.toolchain()
use_repo(kubectl, "kubectl")
rules_kubectl registers the toolchains itself, so no register_toolchains
call is needed.
The bazel run launcher in front of kubectl is a small Go program that finds
the binary through runfiles, so rules_go and a Go SDK are fetched the first
time it is built.
Running kubectl
bazel run @kubectl -- get pods --namespace my-namespace
bazel run @kubectl -- apply -f ./deploy/configmap.yaml
bazel run @kubectl -- kustomize ./deploy/overlays/dev
bazel run passes your environment through, so KUBECONFIG, ~/.kube/config
and the usual --context / --namespace flags behave exactly as they do for
a locally installed kubectl. The @kubectl//:kubectl target also starts
kubectl from the directory you ran bazel in rather than from the runfiles
tree, which is why the relative paths above work.
Pinning a version
rules_kubectl ships checksums for a single default version. Clusters more
than one minor version away from it should pin a matching release:
kubectl.toolchain(version = "1.36.5")
Checksums for versions the module does not know about are looked up from the
upstream .sha256 files while the extension is evaluated. On Bazel 8.5 and
newer the digests are then stored as facts in MODULE.bazel.lock, so the
lookup happens once per version and airgapped builds keep working from the
lockfile. Older Bazel versions repeat the lookup whenever the extension is
re-evaluated.
Several versions can live side by side by giving each tag its own repository
name. Only the first toolchain tag in the root module decides which version
the registered toolchains serve:
kubectl.toolchain()
kubectl.toolchain(name = "kubectl_legacy", version = "1.35.6")
use_repo(kubectl, "kubectl", "kubectl_legacy")
Use in a genrule
The resolved toolchain exposes the binary through two make variables:
KUBECTL is its execpath, for use in actions, and KUBECTL_RLOCATIONPATH is
its rlocationpath, for looking it up in runfiles.
genrule(
name = "kubectl_version",
outs = ["kubectl_version.json"],
cmd = "$(KUBECTL) version --client --output json > $@",
toolchains = ["@rules_kubectl//kubectl:current_toolchain"],
)
sh_test(
name = "kubectl_version_test",
srcs = ["kubectl_version_test.sh"],
data = ["@rules_kubectl//kubectl:current_toolchain"],
env = {"KUBECTL": "$(KUBECTL_RLOCATIONPATH)"},
toolchains = ["@rules_kubectl//kubectl:current_toolchain"],
)
For rule authors
Depend on the toolchain type and read the binary from the resolved
ToolchainInfo:
def _impl(ctx):
toolchain = ctx.toolchains["@rules_kubectl//kubectl:toolchain_type"]
ctx.actions.run(
executable = toolchain.kubectl,
...
)
my_rule = rule(
implementation = _impl,
toolchains = ["@rules_kubectl//kubectl:toolchain_type"],
)
ToolchainInfo carries kubectl (the binary File) and version (the
version string, without a leading v).
Updating the default version
bazel run //tools:fetch_checksums -- 1.38.0
prints the table entry for kubectl/private/versions.bzl. Add it, update
DEFAULT_VERSION, and remove the previous entry.