package kvm2
import (
"errors"
"strings"
"testing"
"time"
"github.com/digitalocean/go-libvirt"
"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 TestListByTag_excludesUnmarkedDomains(t *testing.T) {
t.Parallel()
fx := newFixture(t, 1)
id := fx.create(testName)
fx.fake.addDomain("legacy-vm", libvirt.DomainRunning, "")
fx.fake.addDomain("unmarked", libvirt.DomainRunning, `falsex`)
fx.fake.addDomain("unrelated", libvirt.DomainShutoff, `hand made`)
list, err := fx.p.ListByTag(fx.ctx)
if err != nil {
t.Fatalf("ListByTag() error = %v", err)
}
if got := listIDs(list); len(got) != 1 || got[0] != id {
t.Errorf("ListByTag = %v, want only %s", got, id)
}
if list[0].UID != testUID || list[0].CreatedAt.IsZero() {
t.Errorf("listed = %+v, want uid and createdAt", list[0])
}
if got := fx.logs.matching("kvm2 domains listed"); len(got) != 1 || !strings.Contains(got[0], `"hosts"=1`) || !strings.Contains(got[0], `"managed"=1`) {
t.Errorf("listed line = %v", got)
}
}
func TestListByTag_emptyUIDStillListed(t *testing.T) {
t.Parallel()
fx := newFixture(t, 1)
fx.fake.addDomain("proxy-nouid", libvirt.DomainRunning, managedMetadata("", time.Now()))
list, err := fx.p.ListByTag(fx.ctx)
if err != nil {
t.Fatalf("ListByTag() error = %v", err)
}
if len(list) != 1 || list[0].UID != "" || list[0].ID != formatProviderID(testHost, "proxy-nouid") {
t.Errorf("ListByTag = %+v, want the domain listed with an empty UID", list)
}
}
func TestListByTag_acrossHostsInConfigOrder(t *testing.T) {
t.Parallel()
cfg := twoHosts()
cfg.Hosts[1].Pool = ""
first, second := newFakeBackend(), newFakeBackend()
first.addDomain("proxy-z", libvirt.DomainRunning, managedMetadata("uid-z", time.Now()))
first.addDomain("proxy-a", libvirt.DomainShutoff, managedMetadata("uid-a", time.Now()))
second.addDomain("proxy-b", libvirt.DomainRunning, managedMetadata("uid-b", time.Now()))
p := newProvider(testProvider, cfg, &hostDialer{backends: map[string]backend{"beelink": first, "lab2": second}})
ctx, _ := captureContext()
list, err := p.ListByTag(ctx)
if err != nil {
t.Fatalf("ListByTag() error = %v", err)
}
want := []string{formatProviderID("beelink", "proxy-a"), formatProviderID("beelink", "proxy-z"), formatProviderID("lab2", "proxy-b")}
if got := listIDs(list); strings.Join(got, ",") != strings.Join(want, ",") {
t.Errorf("ListByTag = %v, want %v (config order, then name order)", got, want)
}
if list[0].UID != "uid-a" || list[1].UID != "uid-z" || list[2].UID != "uid-b" {
t.Errorf("uids = %s, %s, %s, want uid-a, uid-z, uid-b", list[0].UID, list[1].UID, list[2].UID)
}
}
func TestListByTag_doesNotReadStateOrAddresses(t *testing.T) {
t.Parallel()
fx := newFixture(t, 2)
id := fx.create(testName)
fx.setAddrs(testName, libvirt.DomainInterfaceAddressesSrcAgent, "10.0.0.5")
fx.fake.addDomain("proxy-off", libvirt.DomainShutoff, managedMetadata("uid-off", time.Now()))
before := len(fx.fake.callLog())
list, err := fx.p.ListByTag(fx.ctx)
if err != nil {
t.Fatalf("ListByTag() error = %v", err)
}
if len(list) != 2 || list[0].ID != id || list[1].ID != formatProviderID(testHost, "proxy-off") {
t.Fatalf("ListByTag = %v, want the two managed domains", listIDs(list))
}
for _, inst := range list {
if inst.State != "" || inst.IP != "" {
t.Errorf("listed %s = state %q ip %q, want both empty: GC does not use them", inst.ID, inst.State, inst.IP)
}
if inst.UID == "" || inst.CreatedAt.IsZero() {
t.Errorf("listed %s lacks uid or createdAt: %+v", inst.ID, inst)
}
}
for _, rpc := range fx.fake.callLog()[before:] {
switch rpc {
case "ConnectListAllDomains", "DomainGetMetadata":
default:
t.Errorf("ListByTag made %s; want only the listing and one metadata read per domain", rpc)
}
}
if got := fx.logs.matching("kvm2 listed instance"); len(got) != 2 || strings.Contains(got[0], `"state"=`) {
t.Errorf("listed instance lines = %v, want two without a state", got)
}
}
func TestListByTag_unreachableHostFailsWhole(t *testing.T) {
t.Parallel()
first := newFakeBackend()
first.addDomain("proxy-a", libvirt.DomainRunning, managedMetadata("uid-a", time.Now()))
d := &hostDialer{
backends: map[string]backend{"beelink": first},
errs: map[string]error{"lab2": errors.New("dial tcp 192.0.2.2:22: i/o timeout")},
}
p := newProvider(testProvider, twoHosts(), d)
ctx, _ := captureContext()
list, err := p.ListByTag(ctx)
if list != nil {
t.Errorf("ListByTag = %v, want nil with the error", list)
}
assertProviderError(t, err, "list", provider.ErrTransient)
}
func TestListByTag_unreadableDomainSkipped(t *testing.T) {
t.Parallel()
tests := []struct {
name string
setup func(fx *fixture)
line string
}{
{
name: "corrupt metadata on one domain",
setup: func(fx *fixture) {
fx.fake.addDomain("proxy-bad", libvirt.DomainRunning, "not an instance")
},
line: "skipping domain with unreadable metadata",
},
{
name: "metadata rpc fails for the first domain only",
setup: func(fx *fixture) {
fx.fake.addDomain("proxy-bad", libvirt.DomainRunning, managedMetadata("uid-bad", time.Now()))
var calls int
fx.fake.mu.Lock()
fx.fake.hooks["DomainGetMetadata"] = func() error {
calls++
if calls == 1 {
return fakeErr(libvirt.ErrInternalError, "internal error: metadata read failed")
}
return nil
}
fx.fake.mu.Unlock()
},
line: "skipping domain with unreadable metadata",
},
{
name: "domain gone between listing and metadata read",
setup: func(fx *fixture) {
fx.fake.addDomain("proxy-bad", libvirt.DomainRunning, managedMetadata("uid-bad", time.Now()))
var calls int
fx.fake.mu.Lock()
fx.fake.hooks["DomainGetMetadata"] = func() error {
calls++
if calls == 1 {
return fakeErr(libvirt.ErrNoDomain, "Domain not found")
}
return nil
}
fx.fake.mu.Unlock()
},
line: "skipping domain with unreadable metadata",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
fx := newFixture(t, 1)
fx.fake.addDomain("proxy-good", libvirt.DomainRunning, managedMetadata("uid-good", time.Now()))
tc.setup(fx)
list, err := fx.p.ListByTag(fx.ctx)
if err != nil {
t.Fatalf("ListByTag() error = %v", err)
}
if got := listIDs(list); len(got) != 1 || got[0] != formatProviderID(testHost, "proxy-good") {
t.Errorf("ListByTag = %v, want only proxy-good", got)
}
if got := fx.logs.matching(tc.line); len(got) != 1 || !strings.Contains(got[0], `"name"="proxy-bad"`) {
t.Errorf("skip line = %v, want one naming proxy-bad", got)
}
})
}
}
func TestListByTag_includesInactiveDomains(t *testing.T) {
t.Parallel()
fx := newFixture(t, 1)
id := fx.create(testName)
fx.withDomain(testName, func(d *fakeDomain) { d.state = libvirt.DomainShutoff })
list, err := fx.p.ListByTag(fx.ctx)
if err != nil {
t.Fatalf("ListByTag() error = %v", err)
}
if len(list) != 1 || list[0].ID != id || list[0].UID != testUID {
t.Errorf("ListByTag = %+v, want the shut-off domain listed with its uid", list)
}
}