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:
@@ -0,0 +1,208 @@
|
||||
package proxmox
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-logr/logr/funcr"
|
||||
logf "sigs.k8s.io/controller-runtime/pkg/log"
|
||||
|
||||
"gitea.home.hrajfrisbee.cz/kacerr/egress-proxies-operator/internal/provider"
|
||||
)
|
||||
|
||||
// captureContext collects funcr output at V(2).
|
||||
func captureContext() (context.Context, *[]string) {
|
||||
lines := &[]string{}
|
||||
var mu sync.Mutex
|
||||
log := funcr.New(func(prefix, args string) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
*lines = append(*lines, prefix+" "+args)
|
||||
}, funcr.Options{Verbosity: 2})
|
||||
return logf.IntoContext(context.Background(), log), lines
|
||||
}
|
||||
|
||||
func testConfig() provider.ProxmoxConfig {
|
||||
return provider.ProxmoxConfig{
|
||||
URL: "https://192.0.2.8:8006",
|
||||
TokenFile: "/nonexistent/proxmox/token",
|
||||
Nodes: []string{"pv1-1"},
|
||||
ISOStorage: "proxy-seeds",
|
||||
Templates: map[string]provider.ProxmoxTemplate{"debian-13": {VMID: 9001}},
|
||||
MachineTypes: map[string]provider.ProxmoxMachineType{"small": {Cores: 1, MemoryMiB: 1024}},
|
||||
}
|
||||
}
|
||||
|
||||
func TestNew_doesNotDialAndWarnsOnMissingToken(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, lines := captureContext()
|
||||
cfg := testConfig()
|
||||
start := time.Now()
|
||||
p, err := New(ctx, provider.ProviderConfig{Name: "proxmox-home", Type: "proxmox", Proxmox: &cfg})
|
||||
if err != nil || p == nil {
|
||||
t.Fatalf("New() = %v, %v; want a provider", p, err)
|
||||
}
|
||||
if elapsed := time.Since(start); elapsed > 100*time.Millisecond {
|
||||
t.Errorf("New() took %v, want under 100ms (it must not make a request)", elapsed)
|
||||
}
|
||||
var warnings int
|
||||
for _, l := range *lines {
|
||||
if strings.Contains(l, "WARNING:") && strings.Contains(l, "token file") {
|
||||
warnings++
|
||||
}
|
||||
if !strings.Contains(l, `"provider"="proxmox-home"`) {
|
||||
t.Errorf("log line lacks the provider value: %s", l)
|
||||
}
|
||||
}
|
||||
if warnings != 1 {
|
||||
t.Errorf("got %d token WARNING: lines, want 1:\n%s", warnings, strings.Join(*lines, "\n"))
|
||||
}
|
||||
}
|
||||
|
||||
func TestNew_insecureSkipTLSVerifyWarns(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, lines := captureContext()
|
||||
cfg := testConfig()
|
||||
cfg.InsecureSkipTLSVerify = true
|
||||
if _, err := New(ctx, provider.ProviderConfig{Name: "proxmox-home", Type: "proxmox", Proxmox: &cfg}); err != nil {
|
||||
t.Fatalf("New() error = %v, want nil", err)
|
||||
}
|
||||
for _, l := range *lines {
|
||||
if strings.Contains(l, "WARNING:") && strings.Contains(l, "not verified") {
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Errorf("no insecureSkipTLSVerify warning in:\n%s", strings.Join(*lines, "\n"))
|
||||
}
|
||||
|
||||
func TestNew_nilBlockDoesNotPanic(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, _ := captureContext()
|
||||
p, err := New(ctx, provider.ProviderConfig{Name: "proxmox-empty", Type: "proxmox"})
|
||||
if err != nil {
|
||||
t.Fatalf("New() error = %v, want nil", err)
|
||||
}
|
||||
pp, ok := p.(*Provider)
|
||||
if !ok {
|
||||
t.Fatalf("New() = %T, want *Provider", p)
|
||||
}
|
||||
if pp.name != "proxmox-empty" || pp.cfg.VMIDRange == nil || pp.now == nil {
|
||||
t.Errorf("provider = %+v, want its name, defaults and a clock", pp)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithDefaults_table(t *testing.T) {
|
||||
t.Parallel()
|
||||
off := false
|
||||
tests := []struct {
|
||||
name string
|
||||
in provider.ProxmoxConfig
|
||||
wantRange provider.ProxmoxVMIDRange
|
||||
wantTimeout int
|
||||
wantAgent bool
|
||||
}{
|
||||
{name: "empty fills every default", wantRange: provider.ProxmoxVMIDRange{Min: 100000, Max: 199999}, wantTimeout: 30, wantAgent: true},
|
||||
{
|
||||
name: "explicit values kept",
|
||||
in: provider.ProxmoxConfig{VMIDRange: &provider.ProxmoxVMIDRange{Min: 500, Max: 600}, RequestTimeoutSeconds: 45, InstallGuestAgent: &off},
|
||||
wantRange: provider.ProxmoxVMIDRange{Min: 500, Max: 600},
|
||||
wantTimeout: 45,
|
||||
wantAgent: false,
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
got := withDefaults(tc.in)
|
||||
if *got.VMIDRange != tc.wantRange || got.RequestTimeoutSeconds != tc.wantTimeout || *got.InstallGuestAgent != tc.wantAgent {
|
||||
t.Errorf("withDefaults() = range %+v timeout %d agent %v; want %+v %d %v",
|
||||
*got.VMIDRange, got.RequestTimeoutSeconds, *got.InstallGuestAgent, tc.wantRange, tc.wantTimeout, tc.wantAgent)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithDefaults_doesNotShareWithCaller(t *testing.T) {
|
||||
t.Parallel()
|
||||
in := testConfig()
|
||||
in.VMIDRange = &provider.ProxmoxVMIDRange{Min: 500, Max: 600}
|
||||
got := withDefaults(in)
|
||||
got.Nodes[0] = "changed"
|
||||
got.VMIDRange.Min = 1
|
||||
got.Templates["debian-13"] = provider.ProxmoxTemplate{VMID: 1}
|
||||
got.MachineTypes["small"] = provider.ProxmoxMachineType{}
|
||||
if in.Nodes[0] != "pv1-1" || in.VMIDRange.Min != 500 || in.Templates["debian-13"].VMID != 9001 || in.MachineTypes["small"].Cores != 1 {
|
||||
t.Errorf("mutating the result changed the caller's config: %+v", in)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNode_table(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
nodes []string
|
||||
zone string
|
||||
want string
|
||||
wantErr string
|
||||
}{
|
||||
{name: "one node and empty zone selects it", nodes: []string{"pv1-1"}, zone: "", want: "pv1-1"},
|
||||
{name: "several nodes and empty zone is permanent", nodes: []string{"pv1-1", "pv1-2"}, zone: "", wantErr: "placement.zone is required"},
|
||||
{name: "known node", nodes: []string{"pv1-1", "pv1-2"}, zone: "pv1-2", want: "pv1-2"},
|
||||
{name: "unknown node is permanent", nodes: []string{"pv1-1"}, zone: "pv9", wantErr: `placement.zone "pv9" is not in proxmox.nodes`},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
cfg := testConfig()
|
||||
cfg.Nodes = tc.nodes
|
||||
got, err := newProvider("proxmox-home", cfg, nil).node(tc.zone)
|
||||
if tc.wantErr == "" {
|
||||
if err != nil || got != tc.want {
|
||||
t.Errorf("node(%q) = %q, %v; want %q, nil", tc.zone, got, err, tc.want)
|
||||
}
|
||||
return
|
||||
}
|
||||
if !errors.Is(err, provider.ErrPermanent) || !strings.Contains(err.Error(), tc.wantErr) {
|
||||
t.Errorf("node(%q) error = %v, want ErrPermanent containing %q", tc.zone, err, tc.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWrapErr_carriesProviderNameOpAndID(t *testing.T) {
|
||||
t.Parallel()
|
||||
p := newProvider("proxmox-home", testConfig(), nil)
|
||||
err := p.wrapErr("get", "vms/150042/proxy-x", loadAPIError(t, "err_vm_missing"))
|
||||
var pe *provider.Error
|
||||
if !errors.As(err, &pe) {
|
||||
t.Fatalf("wrapErr() = %T, want *provider.Error", err)
|
||||
}
|
||||
if pe.Provider != "proxmox-home" || pe.Op != "get" || pe.ID != "vms/150042/proxy-x" || !errors.Is(err, provider.ErrNotFound) {
|
||||
t.Errorf("wrapErr() = %+v, want configured name, op, id and class NotFound", pe)
|
||||
}
|
||||
var ae *apiError
|
||||
if !errors.As(err, &ae) || ae.Status != 500 {
|
||||
t.Errorf("wrapErr() lost the underlying apiError: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestStubs_answerTransient keeps a Proxy on this provider retrying, not
|
||||
// latched, until the methods are implemented in later phases.
|
||||
func TestStubs_answerTransient(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
p := newProvider("proxmox-home", testConfig(), nil)
|
||||
_, createErr := p.Create(ctx, provider.CreateRequest{Name: "proxy-x"})
|
||||
_, getErr := p.Get(ctx, "vms/150042/proxy-x")
|
||||
deleteErr := p.Delete(ctx, "vms/150042/proxy-x")
|
||||
_, listErr := p.ListByTag(ctx)
|
||||
for op, err := range map[string]error{"create": createErr, "get": getErr, "delete": deleteErr, "list": listErr} {
|
||||
if !errors.Is(err, provider.ErrTransient) {
|
||||
t.Errorf("%s error = %v, want ErrTransient", op, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user