Files
egress-proxies-operator/docs/plans-executions/2026-09-16-1108-provider-docs-split.md
T

7.0 KiB

Execution: Split the provider blueprint into contract, appendix, template and index

Plan: docs/plans/2026-09-16-1108-provider-docs-split.md

  • Step 0 — Save and commit the plan
  • Step 1 — Write docs/provider-contract.md
  • Step 2 — Write docs/provider-spec-template.md
  • Step 3 — Write docs/providers-existing.md
  • Step 4 — Rewrite docs/providers.md as the index; link edits
  • Step 5 — Verification sweep
  • Step 6 — Final summary, CHANGELOG after confirmation

Step 0 — Save and commit the plan

Committed as cf89ac4 on the existing feat/provider-blueprint branch, on its own, before any document work. The pre-existing local modification of .claude/settings.json was left out of the commit.

Step 1 — Write docs/provider-contract.md

2026-09-16 11:17. Written from scratch rather than edited down from the old blueprint, so that nothing provider-specific could survive by accident. Every MUST names its caller by function (reconcileManaged, replaceInstance, reconcileDelete, providerFailure, deletionFailure, Sweeper.sweep, Monitor.checkOnce, Monitor.run, registry.Build) and states what breaks.

Axis codes frozen (19, §4.1 to §4.19): readiness, create-return, delete-return, ip-source, gc-mark, created-at, list-partial, quota, not-found, primary-key, provisioning-state, async-ops, secondary-resources, credentials, client-kind, timeouts, logging, child-spans, backend-test. The appendix table and the template are keyed on these strings; the §4.20 summary table's last column gives the template entry number each axis maps to.

Two rules that were not in the old blueprint, both found by reading the callers while verifying:

  • Sweeper.sweep loops providers sequentially in one goroutine, and no caller puts a deadline on the context (only Monitor.checkOnce uses WithTimeout). So a hung ListByTag stalls orphan GC for every provider. Recorded in §1.5 and the timeouts axis.
  • The Stopped branch of reconcileManaged calls Delete and requeues without clearing ProviderID, then deletes again on every poll until NotFound. A backend that reports a freshly created, never started instance as stopped would have it deleted right after creation. Recorded as a MUST in §2.2 and as the provisioning-state axis with three allowed profiles.

Checks run on the draft (all clean):

grep -niE "kubernetes|gcp|kvm|libvirt|squid|google|compute engine" docs/provider-contract.md
grep -n "—" docs/provider-contract.md
grep -nE '\.go:[0-9]+|:[0-9]+-[0-9]+' docs/provider-contract.md
grep -ohE "(internal|cmd|config|docs|api|hack|test)/[A-Za-z0-9_./-]+" docs/provider-contract.md | sort -u | while read p; do test -e "$p" || echo "MISSING $p"; done

The path check reports three expected misses: config/samples/proxy_<type>.yaml is a pattern, docs/provider-spec-template.md is written in Step 2, and internal/provider/providertest is a proposal named in §13.

Worth noting: the contract refers to "the operator pod" and "the cluster's own API" rather than naming the platform, so that the abstractness grep stays meaningful. The hack/providers-dev.yaml rule is phrased as "only if the type needs no credentials" because that file exists for make run-dev without any Secret.

Step 2 — Write docs/provider-spec-template.md

2026-09-16 11:28. Twenty-four entries in a fixed shape (Question, Allowed answers, Obligations, Answer, Evidence). Thirteen of them carry an axis code and their numbers are the ones the contract's §4.20 table points at; the other eleven (identity, placement, image, cloud-init, machine type, config block, Checker, concurrency, deployment, docs, accepted limits) are decisions the contract leaves open without being a profile axis. Entries 9, 13 and 15 carry an empty table to fill in (state mapping, error classification, config fields); entry 21 carries the contract §11 test list as checkboxes so a filled spec doubles as the test plan.

Check that every axis code appears in the template:

for c in $(grep -oE '^### 4\.[0-9]+ `[a-z-]+`' docs/provider-contract.md | grep -oE '`[a-z-]+`' | tr -d '`'); do
  grep -q "\`$c\`" docs/provider-spec-template.md || echo "template missing $c"
done

Clean. Worth noting: the preamble allows "verify against the target version" only on an Evidence line, never as an Answer, because the whole point of the file is that the implementer does not guess.

Step 3 — Write docs/providers-existing.md

2026-09-16 11:35. The profile table has exactly one row per contract axis code, checked by the same loop as Step 2 with a row count. Everything else comes from the old blueprint's §6, §7 and §9 plus a fresh read of the three packages, with every file:line replaced by a symbol name.

Corrections to earlier notes applied while writing: kvm's Checker Details also carry user and pool; the kubernetes provider has no seam interface (newWithClient injects a client.Client, the fake client stands in); kvm's classify has an isClassified pass-through. Timeouts were checked per provider for the timeouts row: kubernetes sets no rest.Config.Timeout, gcp relies on SDK defaults, kvm bounds only the SSH handshake (defaultHandshakeTimeout in dialSSH), so all three are weaker than the contract's SHOULD and the table says so.

Symbol check on the appendix (every backticked identifier grepped for in Go, Makefile and YAML sources):

grep -ohE '`[A-Za-z_][A-Za-z0-9_.]*`' docs/providers-existing.md | tr -d '`' | sort -u | while read s; do
  grep -rqwF --include='*.go' --include='Makefile' --include='*.yaml' -- "$s" . || echo "$s"
done

The residue is file names, dotted Type.method forms, the timeouts axis code, libvirt's nostate, and the API names OwnerReference and rest.Config.Timeout; nothing unexpected.

Worth noting: matrix rows that the profile table now carries (Create non-blocking, GC marks, IP source, quota detection, Checker presence, credentials, logging, tracing, tests) were dropped from "Other differences" rather than kept twice.

2026-09-16 11:40. docs/providers.md was rewritten in place (same path, so the inbound links from README.md, CLAUDE.md, docs/architecture.md and the historical plan files still resolve, and the pre-split blueprint stays reachable in the path's git history). The three inbound sentences were updated to name the new files:

  • README.md "### Adding a provider": the first sentence now names the contract, the template and the appendix, with docs/providers.md as the index; the "Start there" sentence is unchanged.
  • CLAUDE.md "## Architecture": the providers.md sentence now names all three files and the index.
  • docs/architecture.md components row "Provider contract": "rules in provider-contract.md, per-provider profile table in providers-existing.md".

The historical files under docs/plans/ and docs/plans-executions/ keep their providers.md links on purpose.