rules_kubectl

Bazel rules for a hermetic kubectl.

The module does two things and nothing else:

  • it fetches kubectl for the host and exposes it as @kubectl//:kubectl for bazel run, and
  • it defines @rules_kubectl//kubectl:toolchain_type so that any rule set needing kubectl in 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.

rules_kubectl

Bazel rules for a hermetic kubectl.

  • @kubectl//:kubectl runs the host platform's binary from the directory bazel run was invoked in.
  • @rules_kubectl//kubectl:toolchain_type is the toolchain type rules consume to run kubectl in an action.
  • @rules_kubectl//kubectl:current_toolchain exposes the resolved binary to genrule and friends as the KUBECTL (execpath) and KUBECTL_RLOCATIONPATH (rlocationpath) make variables.

Rules

kubectl_toolchain

load("@rules_kubectl//kubectl:defs.bzl", "kubectl_toolchain")

kubectl_toolchain(name, kubectl, version)

A kubectl toolchain.

Rules consume it through @rules_kubectl//kubectl:toolchain_type:

def _impl(ctx):
    toolchain = ctx.toolchains["@rules_kubectl//kubectl:toolchain_type"]
    ctx.actions.run(
        executable = toolchain.kubectl,
        ...
    )

The ToolchainInfo carries kubectl (the binary File) and version (a string, possibly empty).

@rules_kubectl//kubectl:current_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 = "pods",
    outs = ["pods.txt"],
    cmd = "$(KUBECTL) version --client > $@",
    toolchains = ["@rules_kubectl//kubectl:current_toolchain"],
)

sh_test(
    name = "pods_test",
    srcs = ["pods_test.sh"],
    data = ["@rules_kubectl//kubectl:current_toolchain"],
    env = {"KUBECTL": "$(KUBECTL_RLOCATIONPATH)"},
    toolchains = ["@rules_kubectl//kubectl:current_toolchain"],
)

ATTRIBUTES

NameDescriptionTypeMandatoryDefault
nameA unique name for this target.Namerequired
kubectlA kubectl binary.Labelrequired
versionThe version of kubectl, without a leading v. Informational.Stringoptional""

Bzlmod extension for kubectl

Module Extensions

kubectl

kubectl = use_extension("@rules_kubectl//kubectl:extensions.bzl", "kubectl")
kubectl.toolchain(name, version)

Module extension for fetching kubectl.

TAG CLASSES

toolchain

Fetch kubectl and register toolchains for it.

Each tag creates a host repository (default @kubectl) exposing the host platform's binary as @{name}//:kubectl. That target runs kubectl from the directory bazel run was invoked in, so relative paths such as -f ./deploy.yaml resolve as they would for a locally installed kubectl.

kubectl = use_extension("@rules_kubectl//kubectl:extensions.bzl", "kubectl")
kubectl.toolchain()
use_repo(kubectl, "kubectl")
bazel run @kubectl -- get pods --namespace my-namespace
bazel run @kubectl -- apply -f ./deploy/configmap.yaml

rules_kubectl registers the toolchains itself; the root module does not need a register_toolchains call. The registered toolchains serve the version requested by the root module's first toolchain tag (or the default when the root module has none).

Versions

rules_kubectl ships checksums for a single default version. Requesting any other version makes the extension download the upstream .sha256 files for that release while it is evaluated. On Bazel 8.5 and newer the digests are then stored as facts in MODULE.bazel.lock, so the lookup only happens once per version and airgapped builds work from the lockfile. Older Bazel versions repeat the lookup whenever the extension is re-evaluated.

kubectl.toolchain(version = "1.36.5")

Several versions can live side by side by giving each tag its own repository name. Only the first 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")

Attributes

NameDescriptionTypeMandatoryDefault
nameThe name of the host repository.Nameoptional"kubectl"
versionThe version of kubectl to fetch, without a leading v.Stringoptional"1.37.1"