docs: rewrite CLAUDE.md to house conventions, fix flows.md and README

- CLAUDE.md in five sections with Go, testing, plan, changelog, branching
  and tagging rules; known sharp edges moved to Gotchas
- docs/flows.md: metrics are served on :8080, name collisions are rejected
  by the API server, last-cert deletion never cleans up
- README moved to the repo root, deploy command corrected

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-08 20:31:00 +02:00
co-authored by Claude Opus 5.5
parent 36030f4011
commit 7a559bacd0
4 changed files with 764 additions and 6 deletions
+244 -3
View File
@@ -1,5 +1,246 @@
# Claude Instructions
# CLAUDE.md
## Git
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
- Always ask for confirmation before running `git push`.
## What this is
A Kubernetes operator (plain controller-runtime, no CRDs) that bridges cert-manager
`Certificate` resources and Gateway API `Gateway` resources. When a `Certificate`
carries the right annotations, the operator manages HTTPS listeners on the target
Gateway: one listener per entry in `spec.dnsNames`, terminating TLS with the
Certificate's secret.
## Commands
```bash
go build ./... # build
go vet ./... # vet
go test ./... # all tests
go test -race -v -run TestName ./path/to/package # single test
golangci-lint run # lint
go run . # run against the current kubeconfig context
docker build -t gateway-cert-operator .
kubectl kustomize deploy/ # render the manifests
```
## Architecture
| Path | Role |
|---|---|
| `main.go` | Composition root: scheme, flags, manager, health probes. No leader election. |
| `internal/controller/certificate_controller.go` | `CertificateReconciler`: watches `Certificate` and `Gateway`, runs the reconcile loop |
| `internal/gateway/patch.go` | Pure functions: `BuildListenersFromCertificates`, `MergeListeners`, `ListenersEqual` |
| `deploy/` | Kustomize manifests; namespace `gateway-cert-operator-system`, single replica |
Annotations on `cert-manager.io/v1` `Certificate`:
| Annotation | Required | Default |
|---|---|---|
| `gateway-cert-operator.io/gateway-name` | yes | — |
| `gateway-cert-operator.io/gateway-namespace` | no | Certificate's namespace |
Reconcile loop (keyed by Certificate):
1. Fetch the `Certificate`; on not-found return without doing anything.
2. Resolve the target Gateway from the annotations; no annotation, no action.
3. List all Certificates targeting that Gateway through the `IndexFieldGatewayTarget`
field index (key `"namespace/name"`).
4. `BuildListenersFromCertificates` computes the desired `auto-*` listeners.
5. `MergeListeners` keeps every non-`auto-` listener and replaces the rest.
6. Patch the Gateway only when `ListenersEqual` reports a difference.
A reverse watch maps Gateway events back to the targeting Certificates through the
same index, so manual Gateway edits are reverted for `auto-*` listeners.
Listeners named `auto-{certName}-{dnsName with dots as dashes}` belong to the
operator. Any other listener is never modified or removed.
`docs/flows.md` has the full control-path reference (startup, watch wiring, decision
tree, scenarios).
## Conventions
### Go Conventions
- Use the latest stable Go version (check https://go.dev/dl/ before starting a new feature).
- Logging: `logr` through controller-runtime (`log.FromContext(ctx)`), not `slog` —
a deliberate deviation from the house rule, because the manager, client and
controller all log through logr. Keys are lowerCamel.
- `context.Context` is the first parameter of every function that does I/O or can be cancelled.
- No `init()` unless there is no other option.
- Error wrapping: `fmt.Errorf("...: %w", err)` for single errors; `errors.Join` for multiple.
- Prefer stdlib over third-party dependencies where reasonable.
- No vendoring unless explicitly requested.
- Fail fast, return early.
- No obvious comments.
### Testing
**All new Go code must have corresponding tests.**
- Use stdlib `testing` — no testify unless already present in the module.
- Table-driven tests are the default pattern.
- Test files live next to source: `foo.go` → `foo_test.go`.
- Name tests `Test<Function>_<scenario>` (e.g. `TestMergeListeners_keepsManual`).
- Use `t.Parallel()` where safe.
- Use subtests for table entries: `t.Run(tc.name, func(t *testing.T) { ... })`.
- Test behaviour, not implementation.
- Guard slow tests with `testing.Short()`: `if testing.Short() { t.Skip() }`.
- Reconciler tests use controller-runtime's fake client (`WithIndex` for the field
index). Reach for envtest only for what the fake cannot do (API-server validation
of listeners), and guard it with `testing.Short()`. This replaces the house
`testcontainers-go` rule, which does not fit an operator.
- A bug fix lands with a regression test.
### Plans
When Claude Code's plan mode is used, save the plan file inside the repo at
`docs/plans/YYYY-MM-DD-HHMM-<slug>.md` instead of the default `~/.claude/plans/`
location. Get the timestamp with `date "+%Y-%m-%d-%H%M"`. The `<slug>` is a short
kebab-case summary of the plan's topic.
Include the same timestamp in the plan's header, e.g.:
# Plan: <title>
**Created:** 2026-08-07 15:30
Plan files are committed to the repo so other contributors can review historical
decisions.
**Ordering matters:** plan mode itself restricts writes to the default
`~/.claude/plans/` location — that's a hard tool restriction during plan mode, not a
choice. So the copy into `docs/plans/` cannot happen until *after* `ExitPlanMode`.
When it happens, it must be the very first action taken post-exit, committed on its
own, *before* any implementation work starts. Do not fold the plan-file commit into
the first implementation commit.
### Plan execution summaries
After finishing each step of an approved plan, append a short, human-readable summary
to `docs/plans-executions/YYYY-MM-DD-HHMM-<slug>.md` — same timestamp and slug as the
originating plan, so the two files pair up 1:1. Sections go in chronological order
(bottom of the file), not newest-first like `CHANGELOG.md`.
- **Status checklist.** The top of the file is a markdown checklist with one line per
plan step (`- [ ] Step 0 — <title>`). Check a box in the same commit that adds that
step's entry. This checklist is the authoritative status record: a new session
should be able to read this file alone and know where to pick up.
- **Keep the prose light** — what was done, plus anything non-obvious (a judgment
call, a spec gap, something that did not work as expected). Skip steps that went
exactly as planned.
- **Include exact commands** for anything that is not a plain file edit, as fenced
blocks, each with one line on why it was run that way. Call out deviations from
the plan explicitly; close with a short "worth noting" paragraph.
- Commit the summary update together with that step's implementation commit.
`docs/plans-executions/2026-10-08-2028-house-conventions.md` is the reference example.
### Changelog
Maintain a running changelog in `CHANGELOG.md` at the repo root. After every
significant change, fix, or update — once the user confirms it works — append a new
entry **at the top** of the file in this format:
## YYYY-MM-DD HH:MM TZ — short title
- One-line summary of what changed and why.
- Key files touched (optional, only if useful for traceability).
Get the timestamp with `date "+%Y-%m-%d %H:%M %Z"`. Never write a literal placeholder
like `HH:MM` — always run `date` first to get the real current time. Skip trivial edits
(typos, formatting, comment tweaks); only log changes a future reader would care about.
### Reviews
Before opening an MR for a feature, run the `operator-reviewer` and
`go-operator-reviewer` agents on the branch and save the findings to
`docs/reviews/YYYY-MM-DD-HHMM-<slug>.md`, most severe first, as a `- [ ]` list that is
ticked as items are processed.
### Branching & Merge Requests
The remote is Gitea (`gitea.home.hrajfrisbee.cz/kacerr/gatewayapi-certificates-operator`).
For **features**, do not commit to `main` directly. Use a branch + merge request flow:
1. **Create a branch off `main`** before starting work: `feat/<slug>` for features,
`fix/<slug>` for bug-fix branches the user explicitly asks for; `<slug>` is short
kebab-case.
2. **Commit on the branch** following the commit conventions below.
3. **Push the branch** to `origin` with `-u` so it tracks.
4. **Open the MR with `tea`** rather than printing a compare URL:
```bash
tea pr create --title "<short title>" --description "<body>" --base main --head <branch>
```
Print the resulting PR URL. If `tea` is unavailable, fall back to printing the
compare URL
(`gitea.home.hrajfrisbee.cz/kacerr/gatewayapi-certificates-operator/compare/main...<branch>`).
5. **Do not merge or delete the branch** from the CLI — neither via `tea`, `gh`, nor
`git push --delete`. The user does that in Gitea.
**Exceptions — committing straight to `main` is fine for:** small bug fixes or
hotfixes the user describes as such; typo, comment and formatting tweaks; edits the
user explicitly says to push to `main`. When uncertain whether something is a feature
or a small fix, ask before branching.
### Git Commits, Pushes and Tags
- Subjects use a lowercase type prefix, as the existing history does: `feat:`, `fix:`,
`deploy:`, `ci:`, `docs:`, `test:`, `chore:`.
- Always append a `Co-Authored-By:` trailer for Claude.
- Ask for confirmation before running `git push`. The exception is `/ship`, which
commits and pushes without asking.
- Tags are semver `vMAJOR.MINOR.PATCH` and mark releases, not commits. They are
created only through `/release`, which confirms before pushing. The old `0.01`–`0.07`
tags predate this rule and stay as they are.
### CI/CD
Gitea Actions, workflows in `.gitea/workflows/`:
- `build.yaml` builds the image and pushes it to
`gitea.home.hrajfrisbee.cz/kacerr/gatewayapi-certificates-operator` on any tag push
or manual dispatch. Needs the `REGISTRY_TOKEN` repo secret.
- `kubernetes-deploy.yaml` applies `deploy/` to the home-lab cluster through a
Vault → Kanidm → OIDC auth chain.
### Containers
- Multi-stage builds; distroless/scratch final images.
- Non-root user, read-only rootfs for production builds.
- Pin base image digests in production Dockerfiles.
### K8s Manifests
- Always include resource requests/limits.
- Liveness/readiness probes with sane defaults.
- PodDisruptionBudgets for replicated workloads.
## Gotchas
- **Three names for one project.** Directory `gateway-helper-operator`; git remote and
image `gatewayapi-certificates-operator`; binary, manifests, namespace and annotation
prefix `gateway-cert-operator`.
- **`listener-port` annotation is not implemented.** `AnnotationListenerPort` is
declared, never read; every listener is on port 443.
- **Stale listeners.** Deleting a Certificate, removing its annotation or retargeting
it does not clean up the old Gateway. A sibling Certificate's next reconcile fixes
it; with no sibling the listener stays forever.
- **A single bad listener blocks the whole Gateway.** The patch replaces the full
listeners array, so a wildcard DNS name (`*` ends up in the listener name) or a name
collision makes the API server reject every update for that Gateway.
- **`ListenersEqual` is partial**: name, hostname, port and the first secret ref only.
Changing what `BuildListenersFromCertificates` generates in other fields will not
roll out to existing listeners.
- **`auto-` prefix is the only ownership marker.** A user listener named `auto-…` gets
deleted.
- **`--metrics-bind-address` is parsed but ignored**; metrics are still served on
controller-runtime's default `:8080`. Logging is hardcoded to zap dev mode.
- **No leader election**; safe only at `replicas: 1`.
- **Deployment uses the mutable `:latest` tag**, so a new image does not roll out on
its own.
- **Dockerfile base images are tags, not digests** (contradicts the Containers rule;
not yet fixed).
- **`kubernetes-deploy.yaml` runs on every push to any branch** and prints auth
tokens to the job log. Do not copy its patterns; fixing it is on the backlog.