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

6.0 KiB

Execution log: proxy-operator

Pairs with docs/plans/2026-08-07-1747-proxy-operator.md.

Status

  • Step 0 — Branch and scaffold
  • 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:

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:

GOBIN=<scratch>/bin go install sigs.k8s.io/kubebuilder/v4@v4.15.0

Scaffolded in place, with kubebuilder on PATH:

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:

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):

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):

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:

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:

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:

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:

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:

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.