Lesson 09 of 9 · Part 3 — Go and Kubernetes
Testing, linting and shipping
Make Go tools trustworthy and easy to ship: table-driven tests, fake HTTP servers and fake Kubernetes clients, linting and vulnerability checks in CI, static binaries in tiny distroless images, and a final project that ties the whole track together: kaudit, a cluster-audit CLI.
Table-driven tests
package audit
import "testing"
func Severity(notReady, total int) string {
switch {
case total == 0:
return "unknown"
case notReady == 0:
return "ok"
case notReady*100/total >= 20:
return "critical"
default:
return "warning"
}
}
func TestSeverity(t *testing.T) {
tests := []struct {
name string
notReady, total int
want string
}{
{"empty cluster", 0, 0, "unknown"},
{"all ready", 0, 10, "ok"},
{"one of ten", 1, 10, "warning"},
{"a fifth down", 2, 10, "critical"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := Severity(tt.notReady, tt.total); got != tt.want {
t.Errorf("Severity(%d, %d) = %q, want %q", tt.notReady, tt.total, got, tt.want)
}
})
}
}
Each case is one line; adding an edge case is cheap. Tests live next to the code (audit_test.go) and run with go test ./....
Tests are the checklist a pilot runs before take-off: the same items, every time, quickly. Nobody trusts a plane, or a tool, that skipped the checklist because "it worked last time".
Fakes instead of real systems
- HTTP:
httptest.NewServer(handler)gives a real server on localhost; point your client atsrv.URLand return canned JSON, errors or slow responses. - Kubernetes:
k8s.io/client-go/kubernetes/fake.NewSimpleClientset(objects...)behaves like a clientset backed by memory; your code takeskubernetes.Interface, so tests pass the fake. - Controllers: envtest (lesson 08).
Design for it: functions take interfaces (kubernetes.Interface, a small Doer for HTTP) and a context, not global clients.
Lint and check in CI
# .github/workflows/ci.yml (excerpt)
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with: { go-version-file: go.mod }
- run: go test ./... -race -cover
- uses: golangci/golangci-lint-action@v6
- run: go run golang.org/x/vuln/cmd/govulncheck@latest ./...
golangci-lint bundles errcheck (ignored errors), staticcheck, govet and more; start with the defaults and add linters deliberately.
Ship: static binary, tiny image
FROM golang:1.26 AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w -X main.version=0.1.0" -o /kaudit .
FROM gcr.io/distroless/static:nonroot
COPY --from=build /kaudit /kaudit
USER nonroot:nonroot
ENTRYPOINT ["/kaudit"]
The result is a few megabytes, has no shell or package manager, and runs as non-root: ideal for Jobs and controllers. For CLIs people download, goreleaser builds every OS/architecture, checksums and a GitHub release from a tag.
Project: kaudit, a cluster-audit CLI
Bring the track together in one tool:
| Feature | Lessons |
|---|---|
kaudit nodes, kaudit pods, kaudit certs subcommands, --cluster, -o json |
04 |
| Report Not Ready nodes, Pending/CrashLooping pods, nodes behind the control-plane version | 07 |
| Audit many kubeconfig contexts in parallel, at most N at once, with a timeout | 06 |
| Check the latest Kubernetes patch release from the GitHub API and flag clusters behind it | 05 |
| Wrapped errors, exit code 1 when findings exist, 2 for usage errors | 03, 04 |
| Table tests, fake clientset tests, httptest for the release check | 09 |
| Static binary, distroless image, a CronJob manifest with read-only RBAC | 07, 09 |
Try it: build and ship kaudit
- Implement the features in the table, one subcommand at a time, with tests for each.
- Run it against two kind clusters (two kubeconfig contexts) in parallel.
- Add the CI workflow above to a GitHub repository and make it pass.
- Build the distroless image, push it to your registry, and run it in-cluster as a CronJob every hour with read-only RBAC.
- Tag
v0.1.0and publish binaries with goreleaser.
Going deeper: what good Go tools have in common
- Small interfaces at the edges, plain structs and functions inside.
- Every external call has a timeout and a context; every error says what was being done.
- Output is stable and machine-readable when asked (
-o json), friendly otherwise. - CI runs tests with
-race, linters and govulncheck on every pull request.
Recap
- Table-driven tests with
t.Run; fakes with httptest and the fake clientset; envtest for controllers. - golangci-lint and govulncheck in CI.
- Static binaries (
CGO_ENABLED=0) in distroless images; goreleaser for releases. - kaudit combines every lesson into a tool worth keeping.
This site is a public version of my personal engineering knowledge hub. It intentionally excludes confidential company information and internal operational details.