proxmox: config block, IDs, marks, state mapping, classify, registration

Phase 2 of the proxmox provider plan. Adds the proxmox: config block with
its validation, registers the type in the config allow-list and the
constructor map, and starts internal/provider/proxmox: the api seam and
its data types, providerIDs (vms/<vmid>/<name>) with the UID-derived VMID,
the description mark, state mapping, and error classification.

Tests read the responses captured from pv1-1 in Phase 0 rather than
repeating Proxmox's wording, and every matcher is run against every
captured error so none fires on the wrong message. The contract methods
are Transient stubs until Phases 5 to 7.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-23 09:42:46 +00:00
co-authored by Claude Opus 5.5
parent 4e974f30f6
commit 6e996ae2c7
19 changed files with 1856 additions and 4 deletions
@@ -0,0 +1,57 @@
package proxmox
import (
"encoding/json"
"os"
"path/filepath"
"testing"
)
// capture is one response recorded from the real host in Phase 0: the
// status, the reason phrase after the status code, and the raw body.
type capture struct {
Status int `json:"status"`
Reason string `json:"reason"`
Body string `json:"body"`
}
// readCapture loads testdata/<name>.json and fails the test when it is
// missing, so every string a test relies on is backed by a real response.
func readCapture(t *testing.T, name string) capture {
t.Helper()
raw, err := os.ReadFile(filepath.Join("testdata", name+".json"))
if err != nil {
t.Fatalf("reading capture %s: %v", name, err)
}
var c capture
if err := json.Unmarshal(raw, &c); err != nil {
t.Fatalf("parsing capture %s: %v", name, err)
}
return c
}
// loadAPIError turns an error capture into the apiError the client builds.
func loadAPIError(t *testing.T, name string) *apiError {
t.Helper()
c := readCapture(t, name)
if c.Status < 400 {
t.Fatalf("capture %s has status %d, want an error status", name, c.Status)
}
return newAPIError(c.Status, c.Reason, []byte(c.Body))
}
// decodeData decodes the "data" member of a successful capture into T.
func decodeData[T any](t *testing.T, name string) T {
t.Helper()
c := readCapture(t, name)
if c.Status != 200 {
t.Fatalf("capture %s has status %d, want 200", name, c.Status)
}
var env struct {
Data T `json:"data"`
}
if err := json.Unmarshal([]byte(c.Body), &env); err != nil {
t.Fatalf("decoding capture %s: %v", name, err)
}
return env.Data
}