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
+54
View File
@@ -0,0 +1,54 @@
package proxmox
import (
"strings"
"time"
"gitea.home.hrajfrisbee.cz/kacerr/egress-proxies-operator/internal/provider"
)
// createdAtKey is the third line of the mark; the first two use the
// contract's label names.
const createdAtKey = "proxy-operator-created-at"
// instanceMark is the GC mark as read back from a VM's description (spec
// entry 8).
type instanceMark struct {
Managed bool
UID string
CreatedAt time.Time
}
// formatDescription renders the mark written in the clone call.
func formatDescription(uid string, createdAt time.Time) string {
return provider.LabelManaged + "=" + provider.LabelManagedYes + "\n" +
provider.LabelUID + "=" + uid + "\n" +
createdAtKey + "=" + createdAt.UTC().Format(time.RFC3339)
}
// parseDescription reads key=value lines, tolerating CRLF, blank lines and
// lines an admin added. The first occurrence of a key wins. Managed only
// when the value is exactly LabelManagedYes; an unparsable created-at gives
// the zero time, which GC treats as old.
func parseDescription(desc string) instanceMark {
var m instanceMark
seen := map[string]bool{}
for line := range strings.SplitSeq(desc, "\n") {
key, value, ok := strings.Cut(strings.TrimSpace(line), "=")
if !ok || seen[key] {
continue
}
seen[key] = true
switch key {
case provider.LabelManaged:
m.Managed = value == provider.LabelManagedYes
case provider.LabelUID:
m.UID = value
case createdAtKey:
if t, err := time.Parse(time.RFC3339, value); err == nil {
m.CreatedAt = t
}
}
}
return m
}