//go:build linux package host import ( "encoding/json" "errors" "os" "path/filepath" "slices" "strings" "testing" "github.com/carlosframework/platform/internal/hostbox" "github.com/carlosframework/platform/internal/hostops" "github.com/carlosframework/platform/internal/provision" "github.com/carlosframework/platform/internal/registry" ) // step8MovedAside is B2: claimtrial's and wakecheck's instance directories, // program files and live links, moved out of leo's census roots. Their // certificates stay (the certificate cache is box state). func step8MovedAside(r leoRow) bool { for _, h := range []string{leoClaimtrial, leoWakecheck} { switch r.Root { case "/data": for _, dir := range []string{"instances/" + h, ".instances/" + h} { if r.Path == dir || strings.HasPrefix(r.Path, dir+"/") { return true } } if r.Path == "programs/"+h+".json" { return true } case "/opt/carlos": if r.Path == "live/"+h { return true } } } return false } // newEmptyLeoCensusTree is leo's tree for the empty-leo flow (Option B): // claimtrial's row gone (B1), the records keep left in the bucket, and, when // movedAside, claimtrial's and wakecheck's leftovers moved out (B2). Leo // reports zero installations. func newEmptyLeoCensusTree(t *testing.T, keep []string, movedAside bool) *leoCensusTree { t.Helper() base := t.TempDir() roots := map[string]string{"data": filepath.Join(base, "data"), "opt": filepath.Join(base, "opt"), "units": filepath.Join(base, "units")} for _, dir := range roots { if err := os.Mkdir(dir, 0o755); err != nil { t.Fatal(err) } } buildLeoTree(t, leoTreeRows(t, true), leoTreeOptions{ Roots: map[string]string{"/data": roots["data"], "/opt/carlos": roots["opt"], "/etc/systemd/system": roots["units"]}, Skip: func(r leoRow) bool { if movedAside && step8MovedAside(r) { return true } return r.Root == "/etc/systemd/system" && r.Type == "l" && (strings.HasPrefix(r.Target, "/usr/lib/") || strings.HasPrefix(r.Target, "/lib/")) }, }) reg, err := registry.Open(filepath.Join(t.TempDir(), "registry.db")) if err != nil { t.Fatal(err) } t.Cleanup(func() { reg.Close() }) c := &leoCensusTree{roots: roots, r: ClaimRuntimeResources{Mode: "instance-control", Registry: reg, DataDir: roots["data"], OptDir: roots["opt"], UnitsDir: roots["units"]}} c.snap.Report.Installations = []hostops.CensusInstallation{} for _, r := range leoRecords(t) { if !slices.Contains(keep, r.Holder.Host) { continue } var rec provision.Record if err := json.Unmarshal(r.Body, &rec); err != nil { t.Fatal(err) } c.snap.Records = append(c.snap.Records, rec) } return c } // The empty-leo flow's census on leo's real tree, under the window's masks: // with B1 and B2 done it accepts, whether the bucket keeps no record (the // trial's ruling) or rollcheck's and wakecheck's. Without B2, and with // wakecheck's record deleted, wakecheck's leftovers refuse, naming them; so // do claimtrial's, whose record is gone whatever B1 kept. func TestMigrationCensusClassifiesEmptyLeo(t *testing.T) { for name, keep := range map[string][]string{"zero hosts": nil, "two hosts": {leoRollcheck, leoWakecheck}} { t.Run(name, func(t *testing.T) { c := newEmptyLeoCensusTree(t, keep, true) if err := c.check(t, allWindowUnits()); err != nil { t.Fatalf("empty leo under the window's masks: %v", err) } if err := c.check(t, nil); !errors.Is(err, hostbox.ErrTypedInventory) || !strings.Contains(err.Error(), "units entry") { t.Fatalf("masks without --window = %v", err) } c = newEmptyLeoCensusTree(t, keep, false) err := c.check(t, allWindowUnits()) if !errors.Is(err, hostbox.ErrTypedInventory) { t.Fatalf("leftovers not moved aside = %v, want a refusal", err) } if !strings.Contains(err.Error(), "claimtrial") && !strings.Contains(err.Error(), "wakecheck") { t.Fatalf("leftovers refusal does not name them: %v", err) } t.Logf("without B2: %v", err) }) } }