11 KiB
Plan: Bring gateway-helper-operator up to house conventions
Created: 2026-10-08 20:28 CEST
Context
This repo dates from March 2026: 381 lines of Go, no tests, no Makefile, no lint
config, a /init-generated CLAUDE.md (uncommitted, with factual errors) and a
one-line /ship. The newer repos share a consistent set of practices, distilled in
the go-project-bootstrap skill (which names this repo as its operator example) and
extended in egress-proxies-operator, the closest analog. This plan ports those
practices here so the following bug-fix and feature work runs on the same rails:
tests, a quality gate, plans with execution logs, a changelog, and branch + MR flow.
No reconcile behaviour changes in this plan. Bugs found during the survey are listed at the end as the backlog for the next phase.
What the newer projects have in common
| Practice | Source | Here today |
|---|---|---|
| CLAUDE.md in five sections (What this is, Commands, Architecture, Conventions, Gotchas), under ~150 lines | skill | /init output, three factual errors |
House Go rules (ctx first, %w / errors.Join, no init(), stdlib first, fail fast) |
skill, uh-cli, egress | absent |
Testing rules: stdlib testing, table-driven, Test<Func>_<scenario>, t.Parallel(), -short guard, -race |
skill, uh-cli, egress | no tests at all |
Plans in docs/plans/YYYY-MM-DD-HHMM-<slug>.md, committed alone before work |
uh-cli, egress, capi-lab | absent |
Paired docs/plans-executions/ log with a resumable step checklist |
egress, capi-lab | absent |
Timestamped CHANGELOG.md, newest first, written after you confirm it works |
all three | absent |
feat/<slug> branch, tea pr create, never merge or delete from the CLI |
all three | everything on main |
/ship = commit + push with branch guard; /release = tag + confirm + push |
egress, capi-lab | /ship tags every commit 0.NN |
Reviewer agents, findings saved to docs/reviews/ |
egress | absent |
Committed .claude/settings.json allowlist, settings.local.json ignored |
uh-cli, egress, capi-lab | only an untracked, un-ignored local file |
Makefile with help and a one-shot check gate |
uh-cli | absent |
.golangci.yml v2 |
uh-cli, egress | absent |
Commit baked into the binary (internal/version, --version, startup log, OCI label, sha-<12> image tag) |
egress | CI passes build args the Dockerfile discards |
CI check job gating the image build; :latest moves only on tag pushes |
egress | build only, :latest on every run |
Decisions
- Docs workflow: egress-style (plans, execution logs, changelog, reviews). No decisions/sessions/future-tasks files.
/shipand/releasesplit; new tags are semvervMAJOR.MINOR.PATCH, starting atv0.1.0. Old0.NNtags stay as they are.kubernetes-deploy.yamlis not touched here (deferred to the bug-fix phase).- Deviations from the house text, written into CLAUDE.md as such:
- Logging stays on logr/zap through controller-runtime, not
slog(same as egress does in practice). - Integration tests mean envtest behind
testing.Short(), not testcontainers. - Commit subjects keep this repo's existing prefixes (
feat:,fix:,deploy:,ci:,docs:).
- Logging stays on logr/zap through controller-runtime, not
.agents/,.codex/,AGENTS.mdare not ported: in egress they are untracked auto-migrations or kubebuilder scaffold, not conventions.
Steps (one commit each, execution-log entry per step)
Branch: feat/house-conventions off main.
Step 0 — Save and commit the plan
Copy this file to docs/plans/2026-10-08-2028-house-conventions.md, create
docs/plans-executions/2026-10-08-2028-house-conventions.md with the step
checklist, commit alone.
Step 1 — CLAUDE.md
Rewrite CLAUDE.md in the skill's five-section shape
(~/.claude/skills/go-project-bootstrap/references/claude-md-structure.md).
- What this is / Commands / Architecture: keep the annotation table and
reconcile-loop write-up; point to
docs/flows.mdfor detail. Fix the errors: leader election is not enabled; cert deletion does not clean up;listener-portmust not be listed as supported. - Conventions: paste verbatim from the skill references
(
go-conventions.md,docs-conventions.md,git-workflow.mdGitea variant,containers-k8s.md) plus egress's "Plan execution summaries" and plan "Ordering matters" paragraphs from/Users/jan.novak/srv/go/egress-proxies-operator/CLAUDE.md. Remote:gitea.home.hrajfrisbee.cz/kacerr/gatewayapi-certificates-operator. Add the deviations above, the semver tag rule, and the push rule (confirm beforegit push, except inside/ship). - Gotchas: three project names (directory, remote/image, module/manifests);
port annotation declared but unused; stale listeners on cert delete, annotation
removal and retarget;
--metrics-bind-addressparsed but ignored (metrics still serve on:8080);ListenersEqualis partial;auto-prefix is the only ownership marker; deployment uses mutable:latest; Dockerfile base images are not digest-pinned;kubernetes-deploy.yamlruns on every branch push.
Also in this step: commit docs/flows.md after correcting its three wrong claims
(metrics "never bound"; cleanup via resync; name collisions being "last write
wins" — the API server rejects the patch), and move docs/README.md to the repo
root with kubectl apply -f deploy/manifests.yaml corrected to
kubectl apply -k deploy/.
Step 2 — .claude/ and docs scaffolding
.claude/settings.json: the skill'sassets/settings.jsonstarter allowlist plusBash(tea pr create:*),Bash(make <target> *)for the Step 3 targets andBash(docker build:*). Nogit commit,git pushorgit tag..claude/commands/ship.mdandrelease.md: copy fromegress-proxies-operator/.claude/commands/; inrelease.mdreplace the "no tagging scheme" paragraph with the semver rule..claude/agents/operator-reviewer.md,go-operator-reviewer.md: copy fromegress-proxies-operator/.claude/agents/unchanged.CHANGELOG.md(# Changelogheader only),docs/reviews/.gitkeep..gitignore: add.claude/settings.local.json,bin/,cover.out; fix the missing trailing newline.
Step 3 — Makefile, lint, formatting
Makefilemodelled on/Users/jan.novak/srv/go/uh-cli/Makefile:help(self-documenting),build(tobin/manager),run,test(-race -cover),test-short,vet,fmt,lint,tidy,check(lint + vet + test-short),docker-build(passesGIT_COMMIT, using egress's dirty-aware definition),clean..golangci.yml: v2, default linters, as in uh-cli. Egress's config is unmodified kubebuilder scaffold and currently does not pass, so it is not the model..dockerignore: egress's allowlist form.gofmtmain.goandinternal/gateway/patch.go; fix whatevermake lintreports (the deadgatewayGroup/gatewayKindvars are expected).- Add the quality-gate sentence for
make checkto CLAUDE.md.
Step 4 — Module path and version identity
- Rename the module from
github.com/example/gateway-cert-operatortogitea.home.hrajfrisbee.cz/kacerr/gatewayapi-certificates-operator(go.mod, the import inmain.go). internal/version/version.go+ test, ported fromegress-proxies-operator/internal/version/(Commitvia ldflags, fallback todebug.ReadBuildInfo).main.go:--versionflag; first log line carries commit and Go version; zap options bound to flags (opts.BindFlags,Development: truedefault, so output is unchanged unless flags are passed); passmetricsAddrtometricsserver.Optionsso the existing flag takes effect (default stays:8080).Dockerfile:ARG GIT_COMMIT=unknown,-ldflags -X …/internal/version.Commit,GOARCH=${TARGETARCH:-amd64},org.opencontainers.image.revisionlabel, drop the unusedGIT_TAG/BUILD_DATEexpectations.
Step 5 — Baseline tests
internal/gateway/patch_test.go: table-driven tests for
BuildListenersFromCertificates, MergeListeners, ListenersEqual and
listenerNameFor, covering current correct behaviour only (single and multiple
DNS names, sort order, manual listeners preserved, stale auto- listeners
dropped, equality ignoring order). Known-buggy inputs (wildcards, collisions,
truncation) are left for the bug-fix phase so the tests do not pin wrong
behaviour. Reconciler tests with the fake client come with the first reconcile fix.
Step 6 — CI build workflow
.gitea/workflows/build.yaml, following
egress-proxies-operator/.gitea/workflows/build.yaml: check job (go vet,
go build, go test -short) gating build; GIT_COMMIT build arg and OCI
labels; push :<tag> and :sha-<12>; :latest only on real tag pushes;
concurrency group. Update the CI/CD paragraph in CLAUDE.md to match.
Not verifiable locally. If actions/setup-go@v5 fails on the runner (uh-cli hit
that), fall back to uh-cli's curl install from the go.mod version.
Step 7 — Close out
Run both reviewer agents on the branch, save findings to
docs/reviews/<timestamp>-house-conventions.md, fix what is in scope. Push with
/ship, open the MR with tea pr create. The CHANGELOG entry is added after you
confirm it works.
Verification
make check # lint + vet + test-short, all green
go test -race -cover ./... # new tests pass under -race
go build ./... && go vet ./...
make build && ./bin/manager --version # prints the 12-char commit
make docker-build IMG=gateway-cert-operator:dev
docker run --rm gateway-cert-operator:dev --version
kubectl kustomize deploy/ >/dev/null # manifests still render
Live check after merge: push tag v0.1.0, confirm the check job runs before
build, and that the registry has :v0.1.0, :sha-<12> and :latest.
Out of scope — backlog for the bug-fix phase
Found during the survey, with file references; each is high confidence unless noted.
- Stale listeners after cert deletion, annotation removal or retarget
(
certificate_controller.go:71-91). Root cause: the reconcile key is the Certificate but the unit of desired state is the Gateway. - Wildcard DNS names produce an invalid listener name, and the rejected patch
blocks every cert on that Gateway (
patch.go:145). - Listener name collisions: across namespaces, across cert/DNS splits, and after
truncation at 253 chars (
patch.go:143-151). listener-portannotation never read (certificate_controller.go:25,patch.go:44).- Whole-array merge patch from a cached read, no optimistic lock
(
certificate_controller.go:118). ListenersEqualignores protocol, allowed routes, TLS mode and extra refs (patch.go:93-139).- Missing Gateway retried forever as an error (
certificate_controller.go:105-108). - Cross-namespace secret refs without a ReferenceGrant; any Certificate author
can add
AllowedRoutes: Alllisteners to any Gateway. - No events, status or metrics; no watch predicates; no leader election.
kubernetes-deploy.yaml: Vault, Kanidm and OIDC tokens echoed to the log, TLS verification off, deploys on every branch push.- Deployment pinned to
:latest, so new builds do not roll out.