kvm2's NoCloud seed builder moves to internal/provider/nocloud (exported, with an ErrEmptyName sentinel instead of permanentf) and its keyed locks to internal/provider/keyedlock, tests moved under their existing names. kvm2 behaviour is unchanged. proxmox gains the per-name lock and the pure seed helpers (seedISOName, seedVolID, buildSeed); unlike kvm2 (review finding S5.1), a seed staging failure there is Transient, not Permanent. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
230 lines
7.7 KiB
Go
230 lines
7.7 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestLockName_givesUpWithContextAsTransient(t *testing.T) {
|
|
t.Parallel()
|
|
p := newProvider("proxmox-home", testConfig(), nil)
|
|
release, err := p.lockName(context.Background(), "proxy-abc")
|
|
if err != nil {
|
|
t.Fatalf("lockName error = %v", err)
|
|
}
|
|
defer release()
|
|
other, err := p.lockName(context.Background(), "proxy-def")
|
|
if err != nil {
|
|
t.Fatalf("lockName on another name error = %v, want it free", err)
|
|
}
|
|
other()
|
|
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Millisecond)
|
|
defer cancel()
|
|
_, err = p.lockName(ctx, "proxy-abc")
|
|
if !errors.Is(err, provider.ErrTransient) || !errors.Is(err, context.DeadlineExceeded) {
|
|
t.Errorf("lockName on a held name error = %v, want ErrTransient wrapping DeadlineExceeded", err)
|
|
}
|
|
}
|