Files
egress-proxies-operator/internal/provider/kvm2/get_test.go
T

244 lines
8.1 KiB
Go

package kvm2
import (
"context"
"errors"
"fmt"
"strings"
"testing"
"time"
"github.com/digitalocean/go-libvirt"
"gitea.home.hrajfrisbee.cz/kacerr/egress-proxies-operator/internal/provider"
)
func TestGet_afterDeleteReturnsNotFound(t *testing.T) {
t.Parallel()
fx := newFixture(t, 1)
id := fx.create(testName)
if err := fx.p.Delete(fx.ctx, id); err != nil {
t.Fatalf("Delete() error = %v", err)
}
inst, err := fx.p.Get(fx.ctx, id)
if inst != nil {
t.Errorf("Get after Delete = %+v, want nil", inst)
}
assertProviderError(t, err, "get", provider.ErrNotFound)
}
func TestGet_duringDeleteStillReturnsInstance(t *testing.T) {
t.Parallel()
t.Run("destroyed but still defined is stopped", func(t *testing.T) {
t.Parallel()
fx := newFixture(t, 1)
id := fx.create(testName)
fx.withDomain(testName, func(d *fakeDomain) { d.state = libvirt.DomainShutoff })
inst := fx.get(id)
if inst.State != provider.StateStopped || inst.UID != testUID {
t.Errorf("Get = %+v, want Stopped with uid %s", inst, testUID)
}
})
t.Run("undefine in flight still answers", func(t *testing.T) {
t.Parallel()
fx := newFixture(t, 1)
id := fx.create(testName)
release := fx.fake.blockOn("DomainUndefineFlags")
done := make(chan error, 1)
go func() { done <- fx.p.Delete(fx.ctx, id) }()
waitFor(t, "the undefine to block", func() bool { return fx.fake.callCount("DomainUndefineFlags") == 1 })
inst, err := fx.p.Get(fx.ctx, id)
if err != nil {
t.Fatalf("Get during Delete error = %v, want the instance", err)
}
if inst.State != provider.StateStopped || inst.ID != id {
t.Errorf("Get during Delete = %+v, want Stopped %s", inst, id)
}
close(release)
if err := <-done; err != nil {
t.Fatalf("Delete() error = %v", err)
}
_, err = fx.p.Get(fx.ctx, id)
assertProviderError(t, err, "get", provider.ErrNotFound)
})
}
func TestGet_backendBusyIsTransientNeverNotFound(t *testing.T) {
t.Parallel()
tests := []struct {
name string
err error
}{
{name: "operation invalid", err: fakeErr(libvirt.ErrOperationInvalid, "Requested operation is not valid: domain is being migrated")},
{name: "call timed out", err: fmt.Errorf("%w: libvirt call DomainGetState on beelink exceeded its 30s budget: %w", provider.ErrTransient, context.DeadlineExceeded)},
{name: "unknown libvirt code", err: fakeErr(libvirt.ErrInternalError, "internal error: qemu monitor unresponsive")},
{name: "not a libvirt error", err: errors.New("read tcp: connection reset by peer")},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
fx := newFixture(t, 1)
id := fx.create(testName)
fx.fake.failWith("DomainGetState", tc.err)
inst, err := fx.p.Get(fx.ctx, id)
if inst != nil {
t.Errorf("Get = %+v, want nil", inst)
}
assertProviderError(t, err, "get", provider.ErrTransient)
if errors.Is(err, provider.ErrNotFound) {
t.Errorf("Get error = %v, must never be NotFound", err)
}
})
}
}
func TestGet_runningWithoutIPIsProvisioning(t *testing.T) {
t.Parallel()
fx := newFixture(t, 1)
inst := fx.get(fx.create(testName))
if inst.State != provider.StateProvisioning || inst.IP != "" {
t.Errorf("Get = %+v, want Provisioning with no IP", inst)
}
}
func TestGet_runningWithIPIsRunning(t *testing.T) {
t.Parallel()
fx := newFixture(t, 1)
id := fx.create(testName)
fx.setAddrs(testName, libvirt.DomainInterfaceAddressesSrcAgent, "127.0.0.1", "169.254.1.2", "10.0.0.5")
inst := fx.get(id)
if inst.State != provider.StateRunning || inst.IP != "10.0.0.5" {
t.Errorf("Get = %+v, want Running with 10.0.0.5", inst)
}
if got := fx.logs.matching("kvm2 domain fetched"); len(got) != 1 || !strings.Contains(got[0], `"ip"="10.0.0.5"`) || !strings.Contains(got[0], `"state"="Running"`) {
t.Errorf("fetched line = %v, want state and ip", got)
}
}
func TestGet_agentPreferredOverARP(t *testing.T) {
t.Parallel()
fx := newFixture(t, 1)
id := fx.create(testName)
fx.setAddrs(testName, libvirt.DomainInterfaceAddressesSrcAgent, "10.0.0.5")
fx.setAddrs(testName, libvirt.DomainInterfaceAddressesSrcArp, "10.0.0.6")
before := fx.fake.callCount("DomainInterfaceAddresses")
inst := fx.get(id)
if inst.IP != "10.0.0.5" {
t.Errorf("IP = %q, want the agent's 10.0.0.5", inst.IP)
}
if got := fx.fake.callCount("DomainInterfaceAddresses") - before; got != 1 {
t.Errorf("DomainInterfaceAddresses calls = %d, want 1 (ARP not consulted)", got)
}
}
func TestGet_arpFallbackWhenAgentFails(t *testing.T) {
t.Parallel()
fx := newFixture(t, 2)
id := fx.create(testName)
fx.setAddrs(testName, libvirt.DomainInterfaceAddressesSrcArp, "10.0.0.6")
var calls int
fx.fake.mu.Lock()
fx.fake.hooks["DomainInterfaceAddresses"] = func() error {
calls++
if calls%2 == 1 {
return fakeErr(libvirt.ErrAgentUnresponsive, "Guest agent is not responding")
}
return nil
}
fx.fake.mu.Unlock()
inst := fx.get(id)
if inst.State != provider.StateRunning || inst.IP != "10.0.0.6" {
t.Errorf("Get = %+v, want Running with ARP's 10.0.0.6", inst)
}
if got := fx.logs.matching("no addresses from source"); len(got) != 1 || !strings.Contains(got[0], `"source"="agent"`) {
t.Errorf("agent failure line = %v", got)
}
}
func TestGet_addressErrorIsProvisioning(t *testing.T) {
t.Parallel()
fx := newFixture(t, 1)
id := fx.create(testName)
fx.fake.failWith("DomainInterfaceAddresses", fakeErr(libvirt.ErrAgentUnresponsive, "Guest agent is not responding"))
inst := fx.get(id)
if inst.State != provider.StateProvisioning || inst.IP != "" {
t.Errorf("Get = %+v, want Provisioning with no IP", inst)
}
}
func TestGet_readsUIDAndCreatedAtFromBackend(t *testing.T) {
t.Parallel()
fx := newFixture(t, 1)
created := time.Date(2026, 9, 16, 12, 0, 0, 0, time.UTC)
fx.p.now = func() time.Time { return created }
id := fx.create(testName)
inst := fx.get(id)
if inst.UID != testUID || !inst.CreatedAt.Equal(created) {
t.Errorf("Get = %+v, want uid %s created %s", inst, testUID, created)
}
later := created.Add(48 * time.Hour)
fx.withDomain(testName, func(d *fakeDomain) { d.metadata = managedMetadata("rewritten-uid", later) })
inst = fx.get(id)
if inst.UID != "rewritten-uid" || !inst.CreatedAt.Equal(later) {
t.Errorf("Get after metadata change = %+v, want uid rewritten-uid created %s (read back, not remembered)", inst, later)
}
}
func TestGet_unmanagedDomainHasEmptyUID(t *testing.T) {
t.Parallel()
fx := newFixture(t, 1)
fx.fake.addDomain("legacy-vm", libvirt.DomainRunning, "")
inst := fx.get(formatProviderID(testHost, "legacy-vm"))
if inst.UID != "" || !inst.CreatedAt.IsZero() || inst.State != provider.StateProvisioning {
t.Errorf("Get = %+v, want empty uid, zero createdAt, Provisioning", inst)
}
}
func TestGet_malformedIDIsPermanent(t *testing.T) {
t.Parallel()
ids := []string{"", "proxy-x", "beelink/proxy-x", "hosts/beelink/proxy-x", "hosts//domains/proxy-x", "hosts/beelink/domains/", "hosts/beelink/domains/a/b", "nodes/beelink/domains/proxy-x"}
for _, id := range ids {
t.Run(id, func(t *testing.T) {
t.Parallel()
fx := newFixture(t, 1)
inst, err := fx.p.Get(fx.ctx, id)
if inst != nil {
t.Errorf("Get(%q) = %+v, want nil", id, inst)
}
assertProviderError(t, err, "get", provider.ErrPermanent)
var pe *provider.Error
if errors.As(err, &pe) && pe.ID != id {
t.Errorf("error.ID = %q, want %q", pe.ID, id)
}
if got := fx.dialer.dials.Load(); got != 0 {
t.Errorf("dials = %d, want 0", got)
}
})
}
}
func TestGet_unconfiguredHostIsPermanent(t *testing.T) {
t.Parallel()
fx := newFixture(t, 1)
_, err := fx.p.Get(fx.ctx, formatProviderID("nas9", testName))
assertProviderError(t, err, "get", provider.ErrPermanent)
if !strings.Contains(err.Error(), `"nas9"`) {
t.Errorf("error = %v, want the host named", err)
}
if got := fx.dialer.dials.Load(); got != 0 {
t.Errorf("dials = %d, want 0", got)
}
}
func TestGet_unknownStateIsStopped(t *testing.T) {
t.Parallel()
fx := newFixture(t, 1)
fx.fake.addDomain(testName, libvirt.DomainState(42), managedMetadata(testUID, time.Now()))
inst := fx.get(formatProviderID(testHost, testName))
if inst.State != provider.StateStopped {
t.Errorf("State = %s, want Stopped for an unknown backend state", inst.State)
}
}