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>
228 lines
7.5 KiB
Go
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)
|
|
}
|
|
}
|