Files
egress-proxies-operator/internal/provider/config_proxmox_test.go
T
kacerrandClaude Opus 5.5 6e996ae2c7 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>
2026-09-23 09:42:46 +00:00

133 lines
6.6 KiB
Go

package provider
import (
"strings"
"testing"
)
const validProxmoxYAML = `
providers:
- name: proxmox-home
type: proxmox
proxmox:
url: https://192.168.0.8:8006
tokenFile: /etc/proxy-operator/proxmox/token
caFile: /etc/proxy-operator/proxmox/ca.pem
nodes: [pv1-1, pv1-2]
isoStorage: proxy-seeds
storage: local-lvm
bridge: vmbr0
vlanTag: 4
vmidRange: {min: 150000, max: 150999}
templates:
debian-13: {vmid: 9001}
machineTypes:
small: {cores: 1, memoryMiB: 1024}
large: {cores: 4, memoryMiB: 4096, diskGiB: 20}
requestTimeoutSeconds: 45
installGuestAgent: false
`
func TestLoadConfig_proxmoxValid(t *testing.T) {
t.Parallel()
cfg, err := LoadConfig([]byte(validProxmoxYAML))
if err != nil {
t.Fatalf("LoadConfig() error = %v, want nil", err)
}
c := cfg.Providers[0].Proxmox
if c == nil {
t.Fatal("providers[0].proxmox = nil")
}
if c.URL != "https://192.168.0.8:8006" || c.ISOStorage != "proxy-seeds" || c.VLANTag != 4 {
t.Errorf("proxmox = %+v, want url, isoStorage and vlanTag parsed", c)
}
if len(c.Nodes) != 2 || c.Nodes[1] != "pv1-2" {
t.Errorf("nodes = %v, want [pv1-1 pv1-2]", c.Nodes)
}
if c.VMIDRange == nil || c.VMIDRange.Min != 150000 || c.VMIDRange.Max != 150999 {
t.Errorf("vmidRange = %+v, want 150000 to 150999", c.VMIDRange)
}
if c.Templates["debian-13"].VMID != 9001 {
t.Errorf("templates[debian-13] = %+v, want vmid 9001", c.Templates["debian-13"])
}
if mt := c.MachineTypes["large"]; mt.Cores != 4 || mt.MemoryMiB != 4096 || mt.DiskGiB != 20 {
t.Errorf("machineTypes[large] = %+v, want {4 4096 20}", mt)
}
if c.InstallGuestAgent == nil || *c.InstallGuestAgent {
t.Errorf("installGuestAgent = %v, want explicit false", c.InstallGuestAgent)
}
}
func TestValidateProxmox_table(t *testing.T) {
t.Parallel()
block := func(body string) string {
return "providers:\n - name: proxmox-home\n type: proxmox\n proxmox:\n" + indent(body, 6)
}
const base = `
url: https://192.168.0.8:8006
tokenFile: /t
nodes: [pv1-1]
isoStorage: proxy-seeds
templates: {debian-13: {vmid: 9001}}
machineTypes: {small: {cores: 1, memoryMiB: 1024}}
`
with := func(old, repl string) string {
if !strings.Contains(base, old) {
t.Fatalf("base has no %q", old)
}
return block(strings.Replace(base, old, repl, 1))
}
tests := []struct {
name string
yaml string
wantErrSub string
}{
{name: "valid minimal", yaml: block(base)},
{name: "valid full", yaml: validProxmoxYAML},
{name: "valid with trailing slash", yaml: with("url: https://192.168.0.8:8006", "url: https://192.168.0.8:8006/")},
{name: "missing block", yaml: "providers:\n - name: proxmox-home\n type: proxmox\n", wantErrSub: "proxmox block is required"},
{name: "missing url", yaml: with("url: https://192.168.0.8:8006\n", ""), wantErrSub: "proxmox.url is required"},
{name: "http url", yaml: with("https://", "http://"), wantErrSub: "must be an https URL"},
{name: "url with api path", yaml: with("8006", "8006/api2/json"), wantErrSub: "must have no path"},
{name: "missing tokenFile", yaml: with("tokenFile: /t\n", ""), wantErrSub: "proxmox.tokenFile is required"},
{name: "no nodes", yaml: with("nodes: [pv1-1]", "nodes: []"), wantErrSub: "proxmox.nodes must list at least one node"},
{name: "duplicate node", yaml: with("nodes: [pv1-1]", "nodes: [pv1-1, pv1-1]"), wantErrSub: `duplicate node "pv1-1"`},
{name: "node with a slash", yaml: with("nodes: [pv1-1]", "nodes: [pv1/1]"), wantErrSub: "is not a Proxmox node name"},
{name: "node in uppercase", yaml: with("nodes: [pv1-1]", "nodes: [PV1]"), wantErrSub: "is not a Proxmox node name"},
{name: "missing isoStorage", yaml: with("isoStorage: proxy-seeds\n", ""), wantErrSub: "proxmox.isoStorage is required"},
{name: "bad isoStorage id", yaml: with("isoStorage: proxy-seeds", "isoStorage: 1seeds"), wantErrSub: "proxmox.isoStorage \"1seeds\" is not a Proxmox storage id"},
{name: "bad storage id", yaml: with("isoStorage: proxy-seeds", "isoStorage: proxy-seeds\nstorage: a/b"), wantErrSub: "proxmox.storage \"a/b\""},
{name: "vlanTag too large", yaml: with("isoStorage: proxy-seeds", "isoStorage: proxy-seeds\nvlanTag: 4095"), wantErrSub: "proxmox.vlanTag must be between 1 and 4094"},
{name: "vlanTag negative", yaml: with("isoStorage: proxy-seeds", "isoStorage: proxy-seeds\nvlanTag: -1"), wantErrSub: "proxmox.vlanTag"},
{name: "vmidRange min below 100", yaml: with("isoStorage: proxy-seeds", "isoStorage: proxy-seeds\nvmidRange: {min: 50, max: 500}"), wantErrSub: "proxmox.vmidRange must satisfy"},
{name: "vmidRange min above max", yaml: with("isoStorage: proxy-seeds", "isoStorage: proxy-seeds\nvmidRange: {min: 2000, max: 1000}"), wantErrSub: "proxmox.vmidRange must satisfy"},
{name: "vmidRange max too large", yaml: with("isoStorage: proxy-seeds", "isoStorage: proxy-seeds\nvmidRange: {min: 100, max: 1000000000}"), wantErrSub: "proxmox.vmidRange must satisfy"},
{name: "no templates", yaml: with("templates: {debian-13: {vmid: 9001}}", "templates: {}"), wantErrSub: "proxmox.templates must define at least one template"},
{name: "template vmid below 100", yaml: with("vmid: 9001", "vmid: 99"), wantErrSub: `proxmox.templates["debian-13"].vmid must be at least 100`},
{name: "no machineTypes", yaml: with("machineTypes: {small: {cores: 1, memoryMiB: 1024}}", "machineTypes: {}"), wantErrSub: "proxmox.machineTypes must define at least one profile"},
{name: "cores zero", yaml: with("cores: 1", "cores: 0"), wantErrSub: `proxmox.machineTypes["small"] needs cores > 0`},
{name: "memory zero", yaml: with("memoryMiB: 1024", "memoryMiB: 0"), wantErrSub: "memoryMiB > 0"},
{name: "negative diskGiB", yaml: with("memoryMiB: 1024", "memoryMiB: 1024, diskGiB: -5"), wantErrSub: "diskGiB must not be negative"},
{name: "negative timeout", yaml: with("isoStorage: proxy-seeds", "isoStorage: proxy-seeds\nrequestTimeoutSeconds: -1"), wantErrSub: "proxmox.requestTimeoutSeconds must not be negative"},
{name: "unknown key is rejected", yaml: with("isoStorage: proxy-seeds", "isoStorage: proxy-seeds\nsshKeyFile: /k"), wantErrSub: "parsing providers config"},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
_, err := LoadConfig([]byte(tc.yaml))
if tc.wantErrSub == "" {
if err != nil {
t.Fatalf("LoadConfig() error = %v, want nil", err)
}
return
}
if err == nil {
t.Fatalf("LoadConfig() error = nil, want error containing %q", tc.wantErrSub)
}
if !strings.Contains(err.Error(), tc.wantErrSub) {
t.Errorf("LoadConfig() error = %q, want substring %q", err.Error(), tc.wantErrSub)
}
})
}
}