kvm2: Create, Get, Delete and ListByTag with the contract tests
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,243 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user