Files
egress-proxies-operator/docs/plans-executions/2026-08-07-1747-proxy-operator.md
Jan Novak f28766fce3 Add Proxy API types with CEL validation (Step 1)
Full ProxySpec/ProxyStatus/Proxy types per the plan: PlacementSpec,
CloudInitSpec, EndpointSpec, HealthCheckSpec, SecretKeySelector, all
defaults, and 7 CEL XValidation rules enforcing mode/provider
immutability, provider/endpoint required-iff-Managed/External, and
cloud-init exactly-one-of inline/secretRef.

Applies the four corrections identified during planning that would
otherwise be silent bugs: MaxLeases as *int32 (so an explicit 0 survives
Go round-trips instead of re-defaulting to 5), HealthCheck's
default={} marker (so nested defaults apply even when the field is
omitted entirely), MinLength=1 on Provider/CloudInit.Inline (so the CEL
has() checks stay simple), and listType=map on Conditions.

Adds pure helpers (EffectivePort, EffectiveHost, HealthCheckOrDefault,
MaxLeasesOrDefault) with table-driven tests, for use by the health
engine, discovery API, and spec-hash computation in later steps.

Patches the scaffolded placeholder controller test's resource literal to
a schema-valid spec so it survives the new CRD validation — the test
itself is rewritten wholesale in Step 4 alongside the real reconciler.

Regenerated deepcopy and the CRD; make test green (envtest confirmed all
7 CEL rules enforced by a real apiserver).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 21:01:56 +02:00

151 lines
6.0 KiB
Markdown

# Execution log: proxy-operator
Pairs with [docs/plans/2026-08-07-1747-proxy-operator.md](../plans/2026-08-07-1747-proxy-operator.md).
## Status
- [x] Step 0 — Branch and scaffold
- [x] Step 1 — API types (`api/v1alpha1/proxy_types.go`)
- [ ] Step 2 — Provider contract (`internal/provider/`)
- [ ] Step 3 — Mock provider (`internal/provider/mock/`)
- [ ] Step 4 — Reconciler (`internal/controller/`)
- [ ] Step 5 — Health engine (`internal/health/`)
- [ ] Step 6 — Lease store (`internal/lease/`)
- [ ] Step 7 — Discovery API (`internal/discovery/`)
- [ ] Step 8 — GCP provider (`internal/provider/gcp/`)
- [ ] Step 9 — Orphan GC + metrics
- [ ] Step 10 — Wiring, config, docs
- [ ] Step 11 — Tests
- [ ] Verification (vet/test/kind e2e) + commit, push, open MR
## Step 0 — Branch and scaffold
Branched off the unborn `main`:
```bash
git checkout -b feat/proxy-operator
```
Installed kubebuilder v4.15.0 into a scratch `GOBIN` rather than the default
`$(go env GOPATH)/bin`, since the module's package layout changed and
`go install .../cmd/kubebuilder@v4.15.0` (the path from the plan) 404s — the binary is
now the module root itself:
```bash
GOBIN=<scratch>/bin go install sigs.k8s.io/kubebuilder/v4@v4.15.0
```
Scaffolded in place, with `kubebuilder` on `PATH`:
```bash
kubebuilder version
# KubeBuilder: v4.15.0, Kubernetes: 1.36.0
kubebuilder init --domain example.com \
--repo gitea.home.hrajfrisbee.cz/kacerr/egress-proxies-operator --plugins go/v4
# WARN: target directory not empty (expected — CLAUDE.md, docs/, .claude/ already existed)
kubebuilder create api --group crawl --version v1alpha1 --kind Proxy \
--resource --controller
```
`create api` auto-ran `make manifests` at the end, which pulled and ran
`controller-gen` itself:
```bash
sigs.k8s.io/controller-tools/cmd/controller-gen@v0.21.0
"$(bin)/controller-gen" object:headerFile="hack/boilerplate.go.txt",year=2026 paths="./..."
```
Confirmed the CRD group landed correctly (no doubling — `--domain example.com
--group crawl` was used specifically to avoid the `crawl.crawl.example.com` trap
called out in the plan):
```bash
grep -A2 "GroupVersion =" api/v1alpha1/groupversion_info.go
# SchemeGroupVersion = schema.GroupVersion{Group: "crawl.example.com", Version: "v1alpha1"}
```
Dropped the scaffolded GitHub Actions workflows (remote is Gitea, not GitHub):
```bash
git rm -r --cached .github 2>/dev/null; rm -rf .github
```
Ran the full manifest/codegen pass once more to confirm the toolchain is reproducible
end to end:
```bash
make manifests generate
# controller-gen rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases
# controller-gen object:headerFile="hack/boilerplate.go.txt",year=2026 paths="./..."
```
then `go build ./...` and `go vet ./...`, both clean with no output.
Worth noting: `CONTROLLER_TOOLS_VERSION` in the generated `Makefile` came out at
`v0.21.0` by default in this kubebuilder release, so the Makefile edit the plan
anticipated wasn't needed. Pre-existing `CLAUDE.md`/`CHANGELOG.md` content survived
untouched; kubebuilder added its own `README.md`, `AGENTS.md`, `.golangci.yml`,
`.devcontainer/`, `Dockerfile` on top of them — those get edited or left as-is in
later steps. Committed as `076bc66`.
## Step 1 — API types (`api/v1alpha1/proxy_types.go`)
Wrote the full `ProxySpec`/`ProxyStatus`/`Proxy` types per the plan, including the
four corrections called out there (`MaxLeases *int32`, `HealthCheck` with
`+kubebuilder:default={}`, `MinLength=1` on `Provider`/`CloudInit.Inline`,
`Conditions` with `+listType=map`), the 7 CEL `XValidation` rules (6 on `ProxySpec`,
1 on `CloudInitSpec`), and pure helpers in `helpers.go`
(`EffectivePort`/`EffectiveHost`/`HealthCheckOrDefault`/`MaxLeasesOrDefault`) with
table-driven tests in `helpers_test.go`.
Regenerated deepcopy and the CRD:
```bash
make manifests generate
# controller-gen rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases
# controller-gen object:headerFile="hack/boilerplate.go.txt",year=2026 paths="./..."
```
Confirmed all 7 CEL rules and the `healthCheck` `default: {}` block landed in the
generated CRD as expected:
```bash
grep -B1 "rule:" config/crd/bases/crawl.example.com_proxies.yaml
# 7 matches, one per XValidation marker written
```
Ran the full suite, not just `go build`/`go vet`, since this was a good opportunity to
confirm envtest itself works end to end for the first time:
```bash
make test
# Setting up envtest binaries for Kubernetes version 1.36...
# .../bin/k8s/1.36.2-darwin-arm64 (confirms the plan's envtest version note)
```
This failed on the first run — not because of anything in the new types, but because
kubebuilder's scaffolded placeholder test in `proxy_controller_test.go` creates a bare
`Proxy{}` with no `spec.mode`, which our new required/enum field correctly rejects:
```text
Proxy.crawl.example.com "test-resource" is invalid: [spec.mode: Unsupported value: "":
supported values: "Managed", "External", ...]
```
That's a real envtest apiserver enforcing our schema for the first time, which is
useful confirmation on its own. Patched just the resource literal in that scaffold
test to a minimal valid spec (`Mode: External` + `Endpoint.Host`) rather than
rewriting the file — that whole test gets replaced in Step 4 alongside the real
reconciler, so a deeper fix now would be thrown away. `make test` then passed clean:
`api/v1alpha1` at 20.5% coverage (helpers only — CEL itself isn't unit-testable, it's
exercised by the real apiserver as shown above), `internal/controller` at 66.7%.
Worth noting: the `go test ./...` command from the plan's own verification section
does *not* work directly for the envtest suite — it needs `KUBEBUILDER_ASSETS` set,
which only `make test` does via `setup-envtest`. Plain `go test ./...` fails the
`internal/controller` package with a `/usr/local/kubebuilder/bin/etcd: no such file`
error that has nothing to do with the code. Use `make test`, not `go test ./...`,
whenever the controller package is in scope.