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) } }