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:
2026-09-16 19:23:49 +00:00
co-authored by Claude Fable 5.1
parent 6b3c985b4a
commit 794a2aef3d
14 changed files with 2120 additions and 63 deletions
+222
View File
@@ -0,0 +1,222 @@
package kvm2
import (
"errors"
"slices"
"strings"
"sync"
"testing"
"time"
"github.com/digitalocean/go-libvirt"
"gitea.home.hrajfrisbee.cz/kacerr/egress-proxies-operator/internal/provider"
)
func TestDelete_malformedIDIsPermanent(t *testing.T) {
t.Parallel()
for _, id := range []string{"", "proxy-x", "hosts/beelink/proxy-x", "hosts//domains/proxy-x", "hosts/beelink/domains/"} {
t.Run(id, func(t *testing.T) {
t.Parallel()
fx := newFixture(t, 1)
err := fx.p.Delete(fx.ctx, id)
assertProviderError(t, err, "delete", 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 TestDelete_twiceSucceeds(t *testing.T) {
t.Parallel()
fx := newFixture(t, 1)
id := fx.create(testName)
for i := range 2 {
if err := fx.p.Delete(fx.ctx, id); err != nil {
t.Fatalf("Delete #%d error = %v", i+1, err)
}
}
if fx.hasDomain(testName) {
t.Error("domain still defined after Delete")
}
if got := fx.logs.matching("domain already gone"); len(got) != 1 {
t.Errorf("'already gone' lines = %d, want 1 from the second Delete", len(got))
}
if got := fx.logs.matching("kvm2 domain deleted"); len(got) != 2 {
t.Errorf("'deleted' lines = %d, want 2", len(got))
}
}
func TestDelete_removesOverlayAndSeed(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)
}
for _, name := range []string{overlayVolumeName(testName), seedVolumeName(testName)} {
if fx.fake.vol(name) != nil {
t.Errorf("volume %s still present after Delete", name)
}
}
for _, name := range []string{"base-debian-13.qcow2", "base-debian-13.qcow2.sha256"} {
if fx.fake.vol(name) == nil {
t.Errorf("shared base volume %s removed by Delete", name)
}
}
}
func TestDelete_runningDomainDestroyedFirst(t *testing.T) {
t.Parallel()
fx := newFixture(t, 1)
id := fx.create(testName)
before := len(fx.fake.callLog())
if err := fx.p.Delete(fx.ctx, id); err != nil {
t.Fatalf("Delete() error = %v", err)
}
calls := fx.fake.callLog()[before:]
destroy := slices.Index(calls, "DomainDestroy")
undefine := slices.Index(calls, "DomainUndefineFlags")
volDelete := slices.Index(calls, "StorageVolDelete")
if destroy < 0 || undefine < 0 || volDelete < 0 || !(destroy < undefine && undefine < volDelete) {
t.Errorf("call order = %v, want destroy before undefine before volume deletes", calls)
}
if n := strings.Count(strings.Join(calls, " "), "StorageVolDelete"); n != 2 {
t.Errorf("StorageVolDelete calls = %d, want 2 (overlay and seed)", n)
}
}
func TestDelete_stoppedDomainSkipsDestroy(t *testing.T) {
t.Parallel()
fx := newFixture(t, 1)
id := fx.create(testName)
fx.withDomain(testName, func(d *fakeDomain) { d.state = libvirt.DomainShutoff })
if err := fx.p.Delete(fx.ctx, id); err != nil {
t.Fatalf("Delete() error = %v", err)
}
if fx.hasDomain(testName) {
t.Error("domain still defined after Delete")
}
if got := fx.logs.matching("domain not running, destroy skipped"); len(got) != 1 {
t.Errorf("'destroy skipped' lines = %d, want 1:\n%s", len(got), strings.Join(fx.logs.all(), "\n"))
}
if got := fx.fake.callCount("DomainUndefineFlags"); got != 1 {
t.Errorf("DomainUndefineFlags calls = %d, want 1", got)
}
}
func TestDelete_toleratesMissingVolumesAndPool(t *testing.T) {
t.Parallel()
t.Run("domain without volumes", func(t *testing.T) {
t.Parallel()
fx := newFixture(t, 1)
fx.fake.addDomain(testName, libvirt.DomainRunning, managedMetadata(testUID, time.Now()))
if err := fx.p.Delete(fx.ctx, formatProviderID(testHost, testName)); err != nil {
t.Fatalf("Delete() error = %v", err)
}
if fx.hasDomain(testName) {
t.Error("domain still defined after Delete")
}
if got := fx.fake.callCount("StorageVolDelete"); got != 0 {
t.Errorf("StorageVolDelete calls = %d, want 0 for absent volumes", got)
}
})
t.Run("pool gone", func(t *testing.T) {
t.Parallel()
fx := newFixture(t, 0)
fx.p.cfg.Hosts[0].Pool = "retired"
fx.fake.addDomain(testName, libvirt.DomainShutoff, managedMetadata(testUID, time.Now()))
if err := fx.p.Delete(fx.ctx, formatProviderID(testHost, testName)); err != nil {
t.Fatalf("Delete() error = %v", err)
}
if fx.hasDomain(testName) {
t.Error("domain still defined after Delete")
}
if got := fx.logs.matching("pool not found, no volumes to delete"); len(got) != 1 || !strings.Contains(got[0], `"pool"="retired"`) {
t.Errorf("'pool not found' Info line = %v", got)
}
})
t.Run("nothing at all", func(t *testing.T) {
t.Parallel()
fx := newFixture(t, 1)
if err := fx.p.Delete(fx.ctx, formatProviderID(testHost, testName)); err != nil {
t.Fatalf("Delete() error = %v", err)
}
})
}
func TestDelete_unconfiguredHostIsNoOp(t *testing.T) {
t.Parallel()
fx := newFixture(t, 0)
id := formatProviderID("nas9", testName)
if err := fx.p.Delete(fx.ctx, id); err != nil {
t.Fatalf("Delete() error = %v, want nil", err)
}
if got := fx.dialer.dials.Load(); got != 0 {
t.Errorf("dials = %d, want 0", got)
}
if got := fx.fake.callLog(); len(got) != 0 {
t.Errorf("backend calls = %v, want none", got)
}
if got := fx.logs.matching("host is not configured; delete has nothing to clean up"); len(got) != 1 || !strings.Contains(got[0], `"host"="nas9"`) {
t.Errorf("Info line = %v, want one naming the host", got)
}
}
func TestDelete_concurrentSameNameSafe(t *testing.T) {
t.Parallel()
fx := newFixture(t, 1)
id := fx.create(testName)
const n = 10
errs := make([]error, n)
var wg sync.WaitGroup
for i := range n {
wg.Add(1)
go func() {
defer wg.Done()
errs[i] = fx.p.Delete(fx.ctx, id)
}()
}
wg.Wait()
for i, err := range errs {
if err != nil {
t.Errorf("Delete #%d error = %v, want nil", i, err)
}
}
if fx.hasDomain(testName) {
t.Error("domain still defined after concurrent Deletes")
}
for _, name := range []string{overlayVolumeName(testName), seedVolumeName(testName)} {
if fx.fake.vol(name) != nil {
t.Errorf("volume %s still present", name)
}
}
if got := fx.fake.callCount("DomainUndefineFlags"); got != 1 {
t.Errorf("DomainUndefineFlags calls = %d, want 1 (the lock serialises the rest)", got)
}
}
func TestDelete_undefineUsesNvramFlag(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)
}
fx.fake.mu.Lock()
flags, ok := fx.fake.undefineFlags[testName]
fx.fake.mu.Unlock()
if !ok {
t.Fatal("DomainUndefineFlags was not called")
}
for _, want := range []libvirt.DomainUndefineFlagsValues{libvirt.DomainUndefineNvram, libvirt.DomainUndefineManagedSave, libvirt.DomainUndefineSnapshotsMetadata} {
if flags&want == 0 {
t.Errorf("undefine flags = %d, lack %d", flags, want)
}
}
}