Go for Infrastructure Engineers›09 · Testing, linting and shipping

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.

Practitioner
Key wordsgo testtable-driven testst.Runhttptestfake clientsetcoveragegolangci-lintgovulncheckstatic binarymulti-stage Dockerfiledistrolessgoreleaserproject: 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 at srv.URL and 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 takes kubernetes.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

  1. Implement the features in the table, one subcommand at a time, with tests for each.
  2. Run it against two kind clusters (two kubeconfig contexts) in parallel.
  3. Add the CI workflow above to a GitHub repository and make it pass.
  4. Build the distroless image, push it to your registry, and run it in-cluster as a CronJob every hour with read-only RBAC.
  5. Tag v0.1.0 and 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.