229 lines
11 KiB
Markdown
229 lines
11 KiB
Markdown
# Execution: Split the provider blueprint into contract, appendix, template and index
|
|
|
|
Plan: [docs/plans/2026-09-16-1108-provider-docs-split.md](../plans/2026-09-16-1108-provider-docs-split.md)
|
|
|
|
- [x] Step 0 — Save and commit the plan
|
|
- [x] Step 1 — Write `docs/provider-contract.md`
|
|
- [x] Step 2 — Write `docs/provider-spec-template.md`
|
|
- [x] Step 3 — Write `docs/providers-existing.md`
|
|
- [x] Step 4 — Rewrite `docs/providers.md` as the index; link edits
|
|
- [x] Step 5 — Verification sweep
|
|
- [x] 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):
|
|
|
|
```sh
|
|
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:
|
|
|
|
```sh
|
|
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):
|
|
|
|
```sh
|
|
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.
|
|
|
|
## Step 4 — Rewrite `docs/providers.md` as the index; link edits
|
|
|
|
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.
|
|
|
|
## Step 5 — Verification sweep
|
|
|
|
2026-09-16 11:45. All ten checks from the plan, run against the working tree
|
|
after Step 4; nothing needed fixing.
|
|
|
|
Links (file targets and anchors) across the four new files plus `README.md`,
|
|
`CLAUDE.md` and `docs/architecture.md`, with anchors compared against
|
|
slugified headings of the target file:
|
|
|
|
```sh
|
|
python3 - <<'PY'
|
|
import re, pathlib
|
|
files = ["docs/provider-contract.md","docs/provider-spec-template.md","docs/providers-existing.md","docs/providers.md","README.md","CLAUDE.md","docs/architecture.md"]
|
|
def slug(h):
|
|
h = re.sub(r'`','',h).strip().lower(); h = re.sub(r'[^\w\s-]','',h)
|
|
return re.sub(r'\s+','-',h).strip('-')
|
|
def headings(p):
|
|
return {slug(m.group(2)) for m in (re.match(r'^(#{1,6})\s+(.*)$', l) for l in open(p)) if m}
|
|
bad = 0
|
|
for f in files:
|
|
for m in re.finditer(r'\]\(([^)\s]+)\)', open(f).read()):
|
|
t = m.group(1)
|
|
if t.startswith(('http://','https://','mailto:')): continue
|
|
path, _, anchor = t.partition('#')
|
|
target = pathlib.Path(f) if path == '' else pathlib.Path(f).parent/path
|
|
if not target.exists(): print(f, "missing", t); bad += 1; continue
|
|
if anchor and target.suffix == '.md' and anchor not in headings(target): print(f, "bad anchor", t); bad += 1
|
|
print("link problems:", bad)
|
|
PY
|
|
```
|
|
|
|
Output: `link problems: 0`.
|
|
|
|
Em-dashes, line references and the contract's abstractness:
|
|
|
|
```sh
|
|
grep -n "—" docs/provider-contract.md docs/provider-spec-template.md docs/providers-existing.md docs/providers.md
|
|
git diff 2d959af -- README.md CLAUDE.md docs/architecture.md | grep '^+' | grep "—"
|
|
grep -nE '\.go:[0-9]+|:[0-9]+-[0-9]+' docs/provider-contract.md docs/provider-spec-template.md docs/providers-existing.md docs/providers.md
|
|
grep -niE "kubernetes|gcp|kvm|libvirt|squid|google|compute engine" docs/provider-contract.md
|
|
```
|
|
|
|
The only em-dash hit is the pre-existing "Leader-elected" cell of the
|
|
`architecture.md` components row, which shows as an added line because the
|
|
row's last cell was edited; no new em-dash was written. The other three
|
|
greps are empty.
|
|
|
|
Nothing lost from the old blueprint's §9: the bold leads of every gap and
|
|
proposal bullet in `git show 0bb699b:docs/providers.md` were looked up in
|
|
the new files. The literal-prefix match reported 22 misses, all caused by
|
|
rewording ("isn't" to "is not", bold removed), so each topic was re-checked
|
|
by a distinctive phrase and all 22 are present in `providers-existing.md`
|
|
or contract §13.
|
|
|
|
Paths and symbols: every `path/...` token in the four files exists except
|
|
the `config/samples/proxy_<type>.yaml` pattern and the
|
|
`internal/provider/providertest` proposal; every backticked identifier in
|
|
the contract and template resolves in Go, Makefile or YAML sources except
|
|
file names, dotted `Type.method` forms, profile names and the `NewWith...`
|
|
placeholder.
|
|
|
|
Axis alignment: the 19 axis codes from contract §4 each appear once as a
|
|
row in the appendix table (six cells per row) and at least once in the
|
|
template.
|
|
|
|
`go build ./... && go test -short ./...`: all packages `ok`.
|
|
|
|
## Step 6 — Final summary
|
|
|
|
The split is complete on `feat/provider-blueprint`: five commits after the
|
|
plan commit, one per step. The CHANGELOG entry is deliberately not written
|
|
yet; per `CLAUDE.md` it is added once the user confirms the documents read
|
|
the way they want. Nothing has been pushed.
|
|
|
|
Worth noting for whoever picks this up:
|
|
|
|
- The contract's §4.20 table maps axis codes to template entry numbers; if
|
|
entries are renumbered, that column must follow.
|
|
- The `CHANGELOG.md` heading format in `CLAUDE.md` uses an em-dash; the
|
|
"no em-dashes" rule applied to the four new documents only.
|
|
- The stale `mock` comments in `internal/provider/provider.go`,
|
|
`internal/provider/registry/registry.go` and `api/v1alpha1/proxy_types.go`
|
|
are still listed as gaps, not fixed here.
|
|
- The next planned use of these files is a Proxmox provider: copy the
|
|
template to `docs/proxmox-provider-spec.md` and fill it in before any code.
|