0005. controller-gen, kustomize, setup-envtest and golangci-lint come from the Makefile

Status: Accepted · Date: 2026-10-25 · Design section: 0.2, 0.3 · Amends ADR 0002

Decided: Moved controller-gen, kustomize, setup-envtest and golangci-lint out of flake.nix and into Makefile-managed ./bin

Context

ADR 0002 put all four of kubebuilder's build tools in the devShell and overrode CONTROLLER_GEN, KUSTOMIZE, ENVTEST and GOLANGCI_LINT with Nix store paths, so that the generated Makefile would use the flake's binaries and still not need editing. The reasoning was sound and the mechanism (?=) is real, but running it showed the approach cannot work:

  • The rule is $(CONTROLLER_GEN): $(LOCALBIN), with bin/ as a prerequisite. bin/ is newer than any store path, so Make always runs the recipe.
  • The recipe is go-install-tool, whose first act is rm -f "$(1)". When $(1) is a store path that is rm: cannot remove '/nix/store/…': Read-only file system, and set -e aborts. Every one of make build, make test, make manifests and make lint dies there, before reaching the tool.
  • Pre-creating bin/<tool>-<version> symlinks to satisfy the macro's freshness check doesn't survive a second run either: the macro ends with ln -sf "$$(realpath "$(1)-$(3)")" "$(1)", which repoints bin/<tool> at the resolved store path, so the next run's readlink comparison fails and it tries to reinstall.
  • golangci-lint settles it. Its target, when .custom-gcl.yml exists — and kubebuilder init scaffolds one — runs golangci-lint custom --destination $(LOCALBIN) to build a plugin-carrying binary and then mvs it over the tool path. A read-only store binary can never serve that purpose.

Meanwhile CLAUDE.md says to never call controller-gen, kustomize or envtest directly, and to build and generate through the Makefile targets. Tools nobody is allowed to invoke directly don't need to be on PATH; putting them there mostly creates the temptation the rule forbids.

Decision

Let the generated Makefile own its own build tools, and take them out of the devShell: drop kubernetes-controller-tools, kustomize, setup-envtest and golangci-lint from flake.nix, along with the four environment overrides. make installs them into ./bin at the versions it pins (CONTROLLER_TOOLS_VERSION, KUSTOMIZE_VERSION, GOLANGCI_LINT_VERSION, and ENVTEST_VERSION derived from go.mod), which is how kubebuilder intends its Makefile to work.

The devShell keeps what a person or an agent actually types: Go, kubebuilder, cobra-cli, talosctl, kubectl, helm, buf and the protoc plugins, goreleaser, govulncheck, QEMU, cni-plugins, Node, make, git.

Consequences

  • make build, make test, make manifests and make lint work as kubebuilder shipped them, and kubebuilder alpha update keeps working because the Makefile stays unedited apart from the GOLANGCI_LINT ?= one-character fix (now unnecessary, but harmless and left in place).
  • The versions of these four tools are the ones kubebuilder 4.15.0 was tested against, rather than nixpkgs' newer ones. For build tooling that is the safer side to be on.
  • This weakens the letter of the rule that every tool comes from flake.nix: these four come from the Go module proxy instead. It keeps the rule's intent — one toolchain for people, agents and CI, pinned, with no ad-hoc or global installs — because the versions are pinned in the Makefile and resolved the same way everywhere. The rule still holds for everything else.
  • make needs the module proxy the first time it runs in a fresh checkout, on top of the network it already needs for go mod download and for envtest's assets. An offline build would have to populate ./bin and the envtest cache beforehand; see the shortcut: note in flake.nix.

Search the docs

Search pages and headings