proxmox: deployment files, sample, user guide and doc rows

The optional proxmox-token Secret (token, ca.pem) mounted at
/etc/proxy-operator/proxmox, make proxmox-token-secret with content
checks that never print the token, a commented proxmox-home entry in
both providers configs, and config/samples/proxy_proxmox.yaml.
TestLoadConfig_proxmoxSampleEntry loads the commented entry from both
files, and a second test ties the sample Proxy's placement to it.

docs/proxmox-provider.md is the user guide: prerequisites and privileges,
cluster setup, config reference, placement, what lands on Proxmox,
logging, tracing, timeouts, health check, error classes, known gaps and
testing. providers-existing.md gains the proxmox column and section;
architecture.md, the README, providers.md and CLAUDE.md gain their rows.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-23 21:14:18 +00:00
co-authored by Claude Opus 5.5
parent 6e9ef26dcb
commit 7f2073f94b
14 changed files with 1407 additions and 67 deletions
@@ -13,7 +13,7 @@ Pairs with [docs/plans/2026-09-18-1502-proxmox-provider.md](../plans/2026-09-18-
- [x] Phase 6 — Create
- [x] Phase 7 — Delete, Checker, CRD comment
- [x] Phase 8 — Contract and observability sweep, integration test, testing.md
- [ ] Phase 9 — Deployment and documentation
- [x] Phase 9 — Deployment and documentation
- [ ] Phase 10 — Review, real-host validation, changelog, MR
## Phase 0 — Plan file, branch, host recon, fixtures
@@ -1121,3 +1121,180 @@ Worth noting:
`go test -short ./...` run of this phase.
- `.claude/settings.json` stays out of the commit. Nothing in this phase
talked to the host.
## Phase 9 — Deployment and documentation
Deployment: the optional `proxmox-token` Secret (keys `token` and `ca.pem`)
is mounted read-only at `/etc/proxy-operator/proxmox` in
`config/manager/manager.yaml`, next to `kvm-ssh` and commented the same way.
The Makefile gains `proxmox-token-secret`. Both providers configs gain a
commented `proxmox-home` entry after kvm2's. `config/samples/proxy_proxmox.yaml`
is listed in the samples kustomization. There is no RBAC change and no
`hack/providers-dev.yaml` entry. Docs: `docs/proxmox-provider.md` follows
kvm2's guide heading for heading and has no `—`. The rows and sections
named below went into `providers-existing.md`, `architecture.md`, the
README, `providers.md` and `CLAUDE.md`. `docs/testing.md` and the
integration Makefile target were done in Phase 8 and were not touched; the
guide's Testing section links to them.
Where the spec and the plan's Phase 9 text disagree, the spec won. The URL
is `https://192.168.0.8:8006`, not 192.168.2.8, which is not in the node
certificate. The Secret has two keys, and the target takes
`PROXMOX_CA_FILE` as well as `PROXMOX_TOKEN_FILE`.
Judgment calls:
- **The target also checks both files' content.** The token file must be
exactly one non-blank line matching
`<user>@<realm>!<tokenid>=<secret>`. That is the shape `readToken`
accepts: `TrimSpace`, then an anchored regexp. The check uses `grep -c`
and `grep -qx`, so the secret is never printed. The CA check mirrors
`kvm-ssh-secret`'s `PRIVATE KEY` check: the file must contain
`BEGIN CERTIFICATE`. With `insecureSkipTLSVerify` the guide says to
create the Secret by hand with the `token` key only.
- **The sample-entry test covers both files.** `commentedEntry` finds the
`# - name: proxmox-home` line and takes its indent. It keeps the lines
that continue at `<indent># ` and strips `# ` at that indent. The same
helper then works for the ConfigMap, whose entry sits six spaces deep
inside a `|` block. So `TestLoadConfig_proxmoxSampleEntry` has a
`sample` subtest and a `configmap` subtest. It checks every field the
spec names, and that the optional fields stay commented out.
- **A second test goes beyond the plan:**
`TestLoadConfig_proxmoxSampleProxyMatchesSampleEntry`. The sample Proxy's
provider, zone, machine type and image must resolve in the sample
entry. kvm has exactly this bug as a known gap (`proxy_kvm.yaml` places
on `nas1`).
- **The sample entry also has `medium: {cores: 2, memoryMiB: 2048,
diskGiB: 10}`**, next to the `small` the spec asks for, so `diskGiB`
appears somewhere. The optional fields are commented out with a short
note each, because no live proxmox entry exists to explain them. The
kvm2 entry can rely on kvm's.
- **In the ConfigMap the proxmox entry is commented out**, although kvm2's
is live there. It needs a host and a token.
- **providers-existing.md needed more than the column and the section.**
The intro now says five providers and links the guide. In the shared
gaps, six sentences would otherwise be false, and each now names
proxmox where it applies: adoption verified in two of the five, Create
blocking in all three VM providers, no Terminated, the list policy, the
collector gap, and the "kvm2 does this" in proposed features. The new
section is §8, so the shared gaps and proposed features move to §9 and
§10. No doc links those sections by anchor (grep).
- **architecture.md §11 now says "All five providers do".** The sentence
counts Checkers, and proxmox has one. The four Decisions bullets are
entry 23's: the VMID from the UID with the ownership check, the mark in
the description, the seed uploaded through the API, and the node
resolved per call.
- **The guide lists the ten design limits first, then four more.** They
are the hibernated-VM latch, the one-request seed upload, the open-proxy
sample (kvm2's guide lists it too), and "not yet proven end to end" with
the three answers never captured. The Phase 6 to 8 guide items are
each in the section where a reader looks for them. The config write
replacing `agent` and `tags` is in *What lands on Proxmox*, and so are
the seed cleanup, `qm unlock` and the 403 that makes Delete Permanent.
The certificate rule is in *Cluster-side setup*. CA caching versus
token rotation is in both.
- **The guide resumes a hibernated VM with `qm start`, not
`qm resume`.** A VM suspended to disk resumes from its state file on
its next start; `qm resume` is for a VM paused in RAM.
- **The guide's `pvesm add dir proxy-seeds --path /var/lib/proxy-seeds
--content iso` is the standard form, not a recorded command.** Phase 0
says only that the user created `proxy-seeds` as a `dir` storage with
ISO content; the path is illustrative.
The token-file check, run without touching a cluster: `KUBECTL=echo`
prints only the apply stage, and `KUBECTL=true` runs nothing. Scratch
files covered one good line, two lines, a bare secret, blank lines around
a good line, a non-PEM CA and a missing file. The real files on this
workstation pass:
```bash
make -n proxmox-token-secret
make -s proxmox-token-secret PROXMOX_TOKEN_FILE=$D/<case> PROXMOX_CA_FILE=$D/ca KUBECTL=echo
# good, ws: "apply -f -"; two, bad: "... is not one line of user@realm!tokenid=secret"; noca: "... holds no PEM certificate"
make -s proxmox-token-secret KUBECTL=true && echo "real files pass" # real files pass
```
The mutation check for the sample tests edited one line per run,
asserting a single match. It ran the two tests, restored the file from a
copy, and at the end `sha256sum -c` confirmed the three files. All five
mutations failed the tests: `vlanTag: 5000` and `isoStorag:` in the
sample, `192.168.2.8` and `type: kvm2` in the ConfigMap, and
`image: debian-12` in the sample Proxy. `type: kvm2` was rerun after lint
removed the test's own type comparison. It still fails, because
`LoadConfig` refuses a proxmox block under another type.
The link sweep is a short script. It takes the docs as arguments, so it
can be rerun after any later edit. It skips fenced code and inline code
spans. For each relative link it checks that the file exists, and that
the `#anchor` matches a GitHub-style slug of a heading in the target file
(or the same file). Before the real run it was checked on a scratch copy
of the guide with five links added. It reported exactly the three bad
ones: a missing file, a missing anchor in `testing.md`, and a missing
same-file anchor. It checked 78 relative links in the six edited docs,
plus this log's, and found 0 broken:
```bash
cat > doclinks.py <<'PY'
import os, re, sys
def slug(h, seen):
s = re.sub(r"[^\w\- ]", "", h.strip().lower()).replace(" ", "-")
n = seen.get(s, 0); seen[s] = n + 1
return s if n == 0 else f"{s}-{n}"
def prose(path):
out, fence = [], False
for line in open(path, encoding="utf-8"):
if line.lstrip().startswith("```"): fence = not fence; continue
if not fence: out.append(line)
return out
def anchors(path):
seen = {}
return {slug(re.sub(r"`", "", l.lstrip("#").strip()), seen) for l in prose(path) if re.match(r"#{1,6} ", l)}
bad = 0
for doc in sys.argv[1:]:
for n, line in enumerate(prose(doc), 1):
for target in re.findall(r"\]\(([^)\s]+)\)", re.sub(r"`[^`]*`", "", line)):
if re.match(r"[a-z]+:", target): continue
f, _, a = target.partition("#")
p = os.path.normpath(os.path.join(os.path.dirname(doc), f)) if f else doc
if not os.path.exists(p): print(f"{doc}: missing file {target}"); bad += 1
elif a and p.endswith(".md") and a not in anchors(p): print(f"{doc}: missing anchor {target}"); bad += 1
print(f"{bad} broken"); sys.exit(1 if bad else 0)
PY
python3 doclinks.py docs/proxmox-provider.md docs/providers-existing.md docs/architecture.md \
README.md docs/providers.md CLAUDE.md docs/plans-executions/2026-09-18-1502-proxmox-provider.md # 0 broken
```
Verification:
```bash
bin/kustomize build config/default >/dev/null && bin/kustomize build config/samples >/dev/null # ok
go test -count=1 -run 'TestLoadConfig_proxmoxSample' -v ./internal/provider/ # 3 PASS
grep -c '—' docs/proxmox-provider.md # 0
go build ./... && go vet ./... && go vet -tags=integration ./... && go test -short -count=1 ./... # all ok
bin/golangci-lint run --disable staticcheck --max-same-issues 0 --max-issues-per-linter 0 ./internal/provider/
git status --short config/ # the four edited files named above, plus proxy_proxmox.yaml
```
Worth noting:
- Lint on `internal/provider` is at its baseline of three goconst hits.
The first version of the new test pushed it to five: its
`p.Type != "proxmox"` made `"proxmox"` a three-time literal, flagged in
both `config.go` and the test. That comparison added nothing, so it
went. `internal/provider/proxmox` was not touched.
- The `tmp` volume comment in `manager.yaml` still says only the kvm
provider stages seeds there. kvm2 and proxmox do too. It was already
stale for kvm2, so it was left out of this phase's diff.
- The architecture §5 block's right-hand bracket was aligned by
character, not byte (`→` is three bytes), so every line puts `│` in
column 83.
- Nothing in this phase talked to the Proxmox host or a cluster.
`.claude/settings.json` stays out of the commit. The parked kvm2 items
(S5.1, and the 200 ms `TestCheck_hostsCheckedConcurrently`) did not
bite: `go test -short ./...` passed.
- Next is Phase 10: the first real run of `TestProxmox_lifecycle` and
`TestProxmoxCheck_realHost` on pv1-1, with the env block in
`docs/testing.md`. The three answers never captured go with it, and the
guide's "Not yet proven end to end" gap and the README's matching
clause come out once they pass. Then the CHANGELOG entry after the user
confirms, and the MR.