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
+118
View File
@@ -1,8 +1,12 @@
package provider
import (
"os"
"slices"
"strings"
"testing"
"sigs.k8s.io/yaml"
)
const validProxmoxYAML = `
@@ -130,3 +134,117 @@ machineTypes: {small: {cores: 1, memoryMiB: 1024}}
})
}
}
// commentedEntry cuts the commented-out providers entry named name out of a
// config file and strips its "# " at the entry's own indent, so a sample
// that is shipped commented out is still loaded by a test.
func commentedEntry(t *testing.T, path, name string) string {
t.Helper()
data, err := os.ReadFile(path)
if err != nil {
t.Fatalf("reading %s: %v", path, err)
}
var out []string
var prefix string
for line := range strings.SplitSeq(string(data), "\n") {
if prefix == "" {
if strings.TrimSpace(line) == "# - name: "+name {
prefix = line[:strings.Index(line, "#")]
out = append(out, prefix+"- name: "+name)
}
continue
}
if !strings.HasPrefix(line, prefix+"# ") {
break
}
out = append(out, prefix+strings.TrimPrefix(line, prefix+"# "))
}
if prefix == "" {
t.Fatalf("%s has no commented entry %q", path, name)
}
return strings.Join(out, "\n") + "\n"
}
func TestLoadConfig_proxmoxSampleEntry(t *testing.T) {
t.Parallel()
for name, path := range map[string]string{
"sample": "../../config/samples/providers-config.yaml",
"configmap": "../../config/manager/providers_config.yaml",
} {
t.Run(name, func(t *testing.T) {
t.Parallel()
cfg, err := LoadConfig([]byte("providers:\n" + commentedEntry(t, path, "proxmox-home")))
if err != nil {
t.Fatalf("LoadConfig(proxmox-home from %s) error = %v, want nil", path, err)
}
p := cfg.Providers[0]
// LoadConfig already refused a proxmox block under another type.
if p.Name != "proxmox-home" || p.Proxmox == nil {
t.Fatalf("provider %q has proxmox block %v, want proxmox-home with one", p.Name, p.Proxmox)
}
c := p.Proxmox
if c.URL != "https://192.168.0.8:8006" {
t.Errorf("url = %q, want https://192.168.0.8:8006 (the address in the node certificate)", c.URL)
}
if c.TokenFile != "/etc/proxy-operator/proxmox/token" || c.CAFile != "/etc/proxy-operator/proxmox/ca.pem" {
t.Errorf("tokenFile, caFile = %q, %q, want the proxmox-token Secret's mount paths", c.TokenFile, c.CAFile)
}
if c.InsecureSkipTLSVerify {
t.Error("insecureSkipTLSVerify = true, want it left commented out")
}
if len(c.Nodes) != 1 || c.Nodes[0] != "pv1-1" {
t.Errorf("nodes = %v, want [pv1-1]", c.Nodes)
}
if c.ISOStorage != "proxy-seeds" || c.Storage != "" || c.Bridge != "" || c.VLANTag != 4 {
t.Errorf("isoStorage, storage, bridge, vlanTag = %q, %q, %q, %d, want proxy-seeds, \"\", \"\", 4", c.ISOStorage, c.Storage, c.Bridge, c.VLANTag)
}
if len(c.Templates) != 1 || c.Templates["debian-13"].VMID != 9001 {
t.Errorf("templates = %v, want {debian-13: {vmid: 9001}}", c.Templates)
}
if mt, ok := c.MachineTypes["small"]; !ok || mt.Cores != 1 || mt.MemoryMiB != 1024 || mt.DiskGiB != 0 {
t.Errorf("machineTypes[small] = %+v (present %v), want {1 1024 0}", mt, ok)
}
if c.VMIDRange != nil || c.RequestTimeoutSeconds != 0 || c.InstallGuestAgent != nil || c.VendorData != "" {
t.Errorf("optional fields set: %+v, want vmidRange, requestTimeoutSeconds, installGuestAgent and vendorData left commented out", c)
}
})
}
}
func TestLoadConfig_proxmoxSampleProxyMatchesSampleEntry(t *testing.T) {
t.Parallel()
cfg, err := LoadConfig([]byte("providers:\n" + commentedEntry(t, "../../config/samples/providers-config.yaml", "proxmox-home")))
if err != nil {
t.Fatalf("LoadConfig() error = %v, want nil", err)
}
data, err := os.ReadFile("../../config/samples/proxy_proxmox.yaml")
if err != nil {
t.Fatalf("reading the sample Proxy: %v", err)
}
var px struct {
Spec struct {
Provider string `json:"provider"`
Placement struct {
Zone string `json:"zone"`
MachineType string `json:"machineType"`
Image string `json:"image"`
} `json:"placement"`
} `json:"spec"`
}
if err := yaml.Unmarshal(data, &px); err != nil {
t.Fatalf("parsing the sample Proxy: %v", err)
}
c := cfg.Providers[0].Proxmox
if px.Spec.Provider != cfg.Providers[0].Name {
t.Errorf("spec.provider = %q, want %q", px.Spec.Provider, cfg.Providers[0].Name)
}
if !slices.Contains(c.Nodes, px.Spec.Placement.Zone) {
t.Errorf("placement.zone = %q, not in nodes %v", px.Spec.Placement.Zone, c.Nodes)
}
if _, ok := c.MachineTypes[px.Spec.Placement.MachineType]; !ok {
t.Errorf("placement.machineType = %q, not in machineTypes", px.Spec.Placement.MachineType)
}
if _, ok := c.Templates[px.Spec.Placement.Image]; !ok {
t.Errorf("placement.image = %q, not in templates", px.Spec.Placement.Image)
}
}