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) } } base := "base-" + testProvider + "-debian-13.qcow2" for _, name := range []string{base, base + ".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, 1) 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' V(1) 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, 1) 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("V(1) line = %v, want one naming the host", got) } } func TestDelete_noOpLinesAreSilentAtV0(t *testing.T) { t.Parallel() fx := newFixture(t, 0) fx.p.cfg.Hosts[0].Pool = "retired" for _, id := range []string{formatProviderID("nas9", testName), formatProviderID(testHost, testName)} { if err := fx.p.Delete(fx.ctx, id); err != nil { t.Fatalf("Delete(%s) error = %v, want nil", id, err) } } if lines := fx.logs.all(); len(lines) != 0 { t.Errorf("Delete logged %d lines at V(0), want none:\n%s", len(lines), strings.Join(lines, "\n")) } } 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) } } }