a73x

internal/server/syncsvc/volumes_test.go

Ref:   Size: 6.4 KiB   History

package syncsvc

import (
	"testing"

	"github.com/a73x/eitri/internal/pb"
	"github.com/a73x/eitri/internal/server/registry"
	"github.com/a73x/eitri/internal/server/release"
	"github.com/a73x/eitri/internal/server/store"
	"github.com/stretchr/testify/require"
)

// claimedVM gives the host one VM holding one 4GiB claim, and returns the
// claim as the store now reads it — bound to a volume, held by the VM.
func claimedVM(t *testing.T, st *store.Store, hostID, vmID string) store.VolumeClaim {
	t.Helper()
	c, err := st.CreateVolumeClaim(testTenant, vmID+"-data", 4)
	require.NoError(t, err)
	require.NoError(t, st.CreateVM(store.VM{ID: vmID, HostID: hostID, Name: vmID, ImageURL: "u", ImageSHA256: "s",
		VCPUs: 1, MemMB: 1, DiskGB: 1, PowerState: "running", VolumeClaimIDs: []string{c.ID}}))
	c, err = st.GetVolumeClaim(c.ID)
	require.NoError(t, err)
	return c
}

func TestSnapshotCarriesVolumesAndFloor(t *testing.T) {
	f := setup(t)
	c := claimedVM(t, f.st, f.host.ID, "vm1")
	snap, err := f.svc.buildSnapshot(f.host.ID)
	require.NoError(t, err)
	require.Len(t, snap.Volumes, 1)
	require.Equal(t, c.BoundVolumeID, snap.Volumes[0].VolumeId)
	require.EqualValues(t, 4, snap.Volumes[0].SizeGb)
	require.False(t, snap.Volumes[0].Tombstoned)
	require.Equal(t, []string{c.BoundVolumeID}, snap.Vms[0].VolumeIds)
	require.Equal(t, release.Volumes.Since, snap.MinAgentVersion)
}

func TestSnapshotWithoutVolumesHasNoFloor(t *testing.T) {
	f := setup(t)
	snap, err := f.svc.buildSnapshot(f.host.ID)
	require.NoError(t, err)
	require.Equal(t, "", snap.MinAgentVersion, "a pre-volumes agent must keep taking ordinary snapshots")
}

// tombstoneFreeClaim gets the claim to the only state a reap may act on: the
// VM that held it destroyed, the claim itself tombstoned.
func tombstoneFreeClaim(t *testing.T, st *store.Store, c store.VolumeClaim) {
	t.Helper()
	require.NoError(t, st.TombstoneVM(c.VMID))
	require.NoError(t, st.HardDeleteVM(c.VMID, c.HostID))
	require.NoError(t, st.TombstoneVolumeClaim(c.ID))
}

func TestTombstonedVolumeIsReapedWhenHostReportsItGone(t *testing.T) {
	f := setup(t)
	f.reg.SetAgentVersion(f.host.ID, release.Volumes.Since)
	c := claimedVM(t, f.st, f.host.ID, "vm1")
	tombstoneFreeClaim(t, f.st, c)

	f.svc.applyReport(f.host.ID, &pb.Report{Volumes: []*pb.VolumeStatus{{VolumeId: c.BoundVolumeID, Present: true, SizeGb: 4}}})
	vols, err := f.st.ListVolumesForHost(f.host.ID)
	require.NoError(t, err)
	require.Len(t, vols, 1, "the host still has the file")

	f.svc.applyReport(f.host.ID, &pb.Report{Volumes: []*pb.VolumeStatus{{VolumeId: c.BoundVolumeID, Present: false}}})
	vols, err = f.st.ListVolumesForHost(f.host.ID)
	require.NoError(t, err)
	require.Empty(t, vols, "reported gone: row reaped")

	// The claim goes with it, so its name is free again.
	_, err = f.st.GetVolumeClaim(c.ID)
	require.Error(t, err)
}

// A live volume the host does not list is NOT reaped: the row is the truth
// the agent converges toward, and absence on one tick is a file not yet made.
func TestLiveVolumeSurvivesAnEmptyReport(t *testing.T) {
	f := setup(t)
	f.reg.SetAgentVersion(f.host.ID, release.Volumes.Since)
	claimedVM(t, f.st, f.host.ID, "vm1")
	f.svc.applyReport(f.host.ID, &pb.Report{})
	vols, err := f.st.ListVolumesForHost(f.host.ID)
	require.NoError(t, err)
	require.Len(t, vols, 1)
}

// Even told outright the file is gone, a live volume stays: the tenant still
// holds the claim, and the agent's next converge is what makes the file again.
func TestLiveVolumeSurvivesAPresentFalseReport(t *testing.T) {
	f := setup(t)
	f.reg.SetAgentVersion(f.host.ID, release.Volumes.Since)
	c := claimedVM(t, f.st, f.host.ID, "vm1")
	f.svc.applyReport(f.host.ID, &pb.Report{Volumes: []*pb.VolumeStatus{{VolumeId: c.BoundVolumeID, Present: false}}})
	vols, err := f.st.ListVolumesForHost(f.host.ID)
	require.NoError(t, err)
	require.Len(t, vols, 1)
}

// A pre-volumes agent never reports volumes, so its silence proves nothing
// about the file; the tombstoned row waits for an agent that can answer.
func TestTombstonedVolumeIsNotReapedOnAPreVolumesAgentReport(t *testing.T) {
	f := setup(t)
	f.reg.SetAgentVersion(f.host.ID, "v0.0.6")
	c := claimedVM(t, f.st, f.host.ID, "vm1")
	tombstoneFreeClaim(t, f.st, c)
	f.svc.applyReport(f.host.ID, &pb.Report{})
	vols, err := f.st.ListVolumesForHost(f.host.ID)
	require.NoError(t, err)
	require.Len(t, vols, 1)
}

// A fenced report is the agent refusing a stale snapshot: it acted on nothing,
// so nothing in it answers what the snapshot asked for. Reaping off it would
// delete every tombstoned row on the host for a report that never looked.
func TestTombstonedVolumeIsNotReapedOnAFencedReport(t *testing.T) {
	f := setup(t)
	f.reg.SetAgentVersion(f.host.ID, release.Volumes.Since)
	c := claimedVM(t, f.st, f.host.ID, "vm1")
	tombstoneFreeClaim(t, f.st, c)

	f.svc.applyReport(f.host.ID, &pb.Report{FenceViolation: true})
	vols, err := f.st.ListVolumesForHost(f.host.ID)
	require.NoError(t, err)
	require.Len(t, vols, 1, "an omission in a fenced report is not a report that the file is gone")

	// The same omission from an accepted report still reaps: the guard is the
	// fence, not a new reluctance.
	f.svc.applyReport(f.host.ID, &pb.Report{})
	vols, err = f.st.ListVolumesForHost(f.host.ID)
	require.NoError(t, err)
	require.Empty(t, vols)
}

// The reap is a tenant event, not a system one: the claim is gone by the time
// it lands, so the tenant is read from the tombstoned row.
func TestReapIsAuditedUnderTheClaimsTenant(t *testing.T) {
	f := setup(t)
	f.reg.SetAgentVersion(f.host.ID, release.Volumes.Since)
	c := claimedVM(t, f.st, f.host.ID, "vm1")
	tombstoneFreeClaim(t, f.st, c)
	f.svc.applyReport(f.host.ID, &pb.Report{Volumes: []*pb.VolumeStatus{{VolumeId: c.BoundVolumeID, Present: false}}})

	events, err := f.st.ListAudit(testTenant, 50)
	require.NoError(t, err)
	var found bool
	for _, e := range events {
		if e.Action == "volume.reap" {
			found = true
			require.Contains(t, e.Detail, c.BoundVolumeID)
		}
	}
	require.True(t, found, "volume.reap audited under %s", testTenant)
}

// The report reaches the registry, so the console can say what the host found.
func TestReportedVolumesReachTheRegistry(t *testing.T) {
	f := setup(t)
	c := claimedVM(t, f.st, f.host.ID, "vm1")
	f.svc.applyReport(f.host.ID, &pb.Report{Volumes: []*pb.VolumeStatus{{VolumeId: c.BoundVolumeID, Present: true, SizeGb: 4}}})
	hs, ok := f.reg.Get(f.host.ID)
	require.True(t, ok)
	require.Equal(t, []registry.VolumeStatus{{VolumeID: c.BoundVolumeID, Present: true, SizeGB: 4}}, hs.Volumes)
}