Files
egress-proxies-operator/internal/provider/proxmox/list_test.go
T
kacerrandClaude Opus 5.5 803716459a proxmox: Get, ListByTag and task polling
Get follows spec entry 9's check order (lock and unknown status before
the name, suspended is Stopped after the name and mark checks), reads
status/current so paused is Stopped, and maps the three agent messages
of entry 7. ListByTag reads one config per candidate and fails whole
only on the resources call or an ended ctx. waitTask polls the UPID's
node every budgets.poll, logs failed tasks at V(1) and counts
"WARNINGS: <n>" as success; waitUnlocked serves Create's ownership check.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 15:11:20 +00:00

228 lines
7.5 KiB
Go

package proxmox
import (
"context"
"errors"
"slices"
"strings"
"testing"
"gitea.home.hrajfrisbee.cz/kacerr/egress-proxies-operator/internal/provider"
)
func listIDs(list []provider.Instance) []string {
ids := make([]string, len(list))
for i, inst := range list {
ids[i] = inst.ID
}
return ids
}
func (fx *fixture) list() []provider.Instance {
fx.t.Helper()
list, err := fx.p.ListByTag(fx.ctx)
if err != nil {
fx.t.Fatalf("ListByTag() error = %v", err)
}
return list
}
func TestListByTag_excludesUnmarkedVMs(t *testing.T) {
t.Parallel()
fx := newFixture(t, 1)
fx.addOurVM(testVMID, testName, "running")
fx.fake.addVM(150100, "proxy-nomark0000000000", "running")
fx.fake.addVM(150101, "proxy-managedfalse0000", "stopped").config["description"] = "proxy-operator-managed=false\nproxy-operator-uid=x"
fx.fake.addVM(150102, "tagged-by-hand", "running").config["tags"] = proxyTag
list := fx.list()
if got := listIDs(list); !slices.Equal(got, []string{fx.id()}) {
t.Fatalf("ListByTag = %v, want only %s", got, fx.id())
}
if list[0].UID != testUID || !list[0].CreatedAt.Equal(testCreatedAt) || list[0].State != "" || list[0].IP != "" {
t.Errorf("listed = %+v, want uid and createdAt only", list[0])
}
lines := fx.logs.matching(`"msg"="proxmox vms listed"`)
if len(lines) != 1 || !strings.Contains(lines[0], `"candidates"=4`) || !strings.Contains(lines[0], `"managed"=1`) || !strings.Contains(lines[0], `"skipped"=0`) {
t.Errorf("listed line = %v, want candidates 4, managed 1, skipped 0", lines)
}
}
func TestListByTag_emptyUIDStillListed(t *testing.T) {
t.Parallel()
fx := newFixture(t, 1)
fx.addOurVM(testVMID, testName, "stopped").config["description"] = "proxy-operator-managed=true"
list := fx.list()
if len(list) != 1 || list[0].UID != "" || list[0].ID != fx.id() {
t.Errorf("ListByTag = %+v, want the VM listed with an empty UID", list)
}
}
func TestListByTag_candidatesByTagOrNamePrefix(t *testing.T) {
t.Parallel()
fx := newFixture(t, 1)
// Named like a proxy, no tag: a candidate.
fx.addOurVM(150001, "proxy-notag00000000000", "running").config["tags"] = ""
// Tagged, renamed by an admin: a candidate. Tags as Proxmox lists them.
fx.addOurVM(150002, "renamed", "running").config["tags"] = "web;proxy_operator"
// Marked but neither tagged nor named like a proxy: never read.
fx.addOurVM(150003, "unrelated", "running").config["tags"] = "web"
got := listIDs(fx.list())
want := []string{formatProviderID(150001, "proxy-notag00000000000"), formatProviderID(150002, "renamed")}
if !slices.Equal(got, want) {
t.Errorf("ListByTag = %v, want %v", got, want)
}
if n := fx.fake.callCount("VMConfig"); n != 2 {
t.Errorf("VMConfig calls = %d, want 2, one per candidate", n)
}
}
func TestHasTag_table(t *testing.T) {
t.Parallel()
tests := []struct {
tags string
want bool
}{
{tags: "proxy_operator", want: true},
{tags: "a;proxy_operator;b", want: true},
{tags: "a,proxy_operator", want: true},
{tags: "a proxy_operator", want: true},
{tags: "Proxy_Operator", want: true},
{tags: ""},
{tags: "proxy_operator2"},
{tags: "proxy-operator"},
}
for _, tc := range tests {
if got := hasTag(tc.tags, proxyTag); got != tc.want {
t.Errorf("hasTag(%q) = %v, want %v", tc.tags, got, tc.want)
}
}
}
func TestListByTag_skipsLXCAndTemplates(t *testing.T) {
t.Parallel()
fx := newFixture(t, 1)
fx.addOurVM(150001, "proxy-container0000000", "running").kind = "lxc"
fx.addOurVM(150002, "proxy-template00000000", "stopped").template = true
if list := fx.list(); len(list) != 0 {
t.Errorf("ListByTag = %+v, want nothing", list)
}
if n := fx.fake.callCount("VMConfig"); n != 0 {
t.Errorf("VMConfig calls = %d, want 0", n)
}
}
func TestListByTag_midCloneEntryIsNotACandidate(t *testing.T) {
t.Parallel()
fx := newFixture(t, 1)
fx.fake.addTemplate(9001, "debian-13-template")
if _, err := fx.fake.Clone(fx.ctx, fakeNode, 9001, cloneParams{NewID: testVMID, Name: testName, Description: formatDescription(testUID, testCreatedAt)}); err != nil {
t.Fatalf("Clone error = %v", err)
}
if list := fx.list(); len(list) != 0 {
t.Errorf("ListByTag = %+v, want nothing while the clone runs", list)
}
}
func TestListByTag_resourcesFailureFailsWhole(t *testing.T) {
t.Parallel()
fx := newFixture(t, 1)
fx.addOurVM(testVMID, testName, "running")
fx.fake.failWith("ClusterResources", fakeErr(595, "Connection refused"))
list, err := fx.p.ListByTag(fx.ctx)
if list != nil {
t.Errorf("ListByTag = %+v, want nil", list)
}
assertProviderError(t, err, "list", provider.ErrTransient)
}
func TestListByTag_unreadableVMSkipped(t *testing.T) {
t.Parallel()
fx := newFixture(t, 1)
fx.addOurVM(150001, "proxy-destroyedmidswee", "running")
fx.addOurVM(testVMID, testName, "running")
// The first candidate is destroyed between the listing and its config
// read.
mutateOn(fx.fake, "VMConfig", func() { delete(fx.fake.vms, 150001) })
if got := listIDs(fx.list()); !slices.Equal(got, []string{fx.id()}) {
t.Errorf("ListByTag = %v, want only %s", got, fx.id())
}
lines := fx.logs.matching(`"msg"="skipping unreadable proxmox vm"`)
if len(lines) != 1 || !strings.Contains(lines[0], `"vmid"=150001`) || !strings.Contains(lines[0], "does not exist") {
t.Errorf("skip lines = %v, want one naming 150001 and the error", lines)
}
if lines := fx.logs.matching(`"skipped"=1`); len(lines) != 1 {
t.Errorf("listed line with skipped 1 = %v", lines)
}
}
func TestListByTag_unreadableVMWithEndedContextFails(t *testing.T) {
t.Parallel()
fx := newFixture(t, 1)
fx.addOurVM(testVMID, testName, "running")
ctx, cancel := context.WithCancel(fx.ctx)
defer cancel()
fx.fake.hookOn("VMConfig", func() error {
cancel()
return context.Canceled
})
list, err := fx.p.ListByTag(ctx)
if list != nil {
t.Errorf("ListByTag = %+v, want nil", list)
}
assertProviderError(t, err, "list", provider.ErrTransient)
if !errors.Is(err, context.Canceled) {
t.Errorf("error = %v, want it to wrap context.Canceled", err)
}
}
func TestListByTag_unknownStatusStillAttempted(t *testing.T) {
t.Parallel()
fx := newFixture(t, 1)
fx.addOurVM(testVMID, testName, "running").status = "unknown"
if got := listIDs(fx.list()); !slices.Equal(got, []string{fx.id()}) {
t.Errorf("ListByTag = %v, want %s despite status unknown", got, fx.id())
}
}
func TestListByTag_doesNotReadStatusOrAgent(t *testing.T) {
t.Parallel()
fx := newFixture(t, 1)
fx.addOurVM(testVMID, testName, "running").ifaces = capturedIfaces(t)
fx.addOurVM(150001, "proxy-second0000000000", "stopped")
fx.list()
want := []string{"ClusterResources", "VMConfig", "VMConfig"}
if got := fx.fake.callLog(); !slices.Equal(got, want) {
t.Errorf("calls = %v, want %v", got, want)
}
}
func TestListByTag_idsAcceptedByParse(t *testing.T) {
t.Parallel()
fx := newFixture(t, 1)
fx.addOurVM(testVMID, testName, "running")
fx.addOurVM(999999999, "proxy-topofrange000000", "stopped")
fx.addOurVM(100, "proxy-bottomofrange000", "stopped")
list := fx.list()
if len(list) != 3 {
t.Fatalf("ListByTag = %+v, want 3", list)
}
for _, inst := range list {
vmid, name, err := parseProviderID(inst.ID)
if err != nil || formatProviderID(vmid, name) != inst.ID {
t.Errorf("parseProviderID(%q) = %d, %q, %v; want a round trip", inst.ID, vmid, name, err)
}
}
}
func TestListByTag_quietAtV0(t *testing.T) {
t.Parallel()
fx := newFixture(t, 0)
fx.addOurVM(testVMID, testName, "running")
fx.addOurVM(150001, "proxy-destroyedmidswee", "running")
mutateOn(fx.fake, "VMConfig", func() { delete(fx.fake.vms, 150001) })
fx.list()
if lines := fx.logs.all(); len(lines) != 0 {
t.Errorf("V(0) lines = %v, want none", lines)
}
}