| 1 | //go:build unix |
| 2 | |
| 3 | package host |
| 4 | |
| 5 | import ( |
| 6 | "errors" |
| 7 | "os" |
| 8 | "path/filepath" |
| 9 | "testing" |
| 10 | "time" |
| 11 | |
| 12 | "github.com/carlosframework/platform/internal/hostbox" |
| 13 | "github.com/carlosframework/platform/internal/hostclaim" |
| 14 | "github.com/carlosframework/platform/internal/provision" |
| 15 | "github.com/carlosframework/platform/internal/registry" |
| 16 | "github.com/carlosframework/platform/internal/release" |
| 17 | ) |
| 18 | |
| 19 | // Astra round 2 finding 8, ruling 61: an empty installation initialises with |
| 20 | // a previous relocation's journals present under relocate/ (N9 classifies |
| 21 | // the directory as the claim classifies its own journals); without the |
| 22 | // classification the same tree refuses, which is the defect. |
| 23 | func TestCensusInitializesWithOldRelocationJournals(t *testing.T) { |
| 24 | t.Parallel() |
| 25 | censusWithSharedFiles(t, "relocate", map[string]string{ |
| 26 | "relocate/ops/op-old/install.json": `{"v":1,"host":"old.example","op":"op-old"}`, |
| 27 | "relocate/ops/op-old/attempts.json": `{"v":1,"host":"old.example","op":"op-old"}`, |
| 28 | "relocate/release/op-old.json": `{"v":1,"request":{"Host":"old.example"}}`, |
| 29 | "relocate/build/op-old/old.example.db": "scratch", |
| 30 | }) |
| 31 | } |
| 32 | |
| 33 | // censusWithSharedFiles plants files under a production box's data root |
| 34 | // and checks an empty installation of another host initialises with them, |
| 35 | // and refuses once the shared entry named classified is taken out of |
| 36 | // claimGlobalSharedPaths. |
| 37 | func censusWithSharedFiles(t *testing.T, classified string, files map[string]string) { |
| 38 | t.Helper() |
| 39 | ctx := t.Context() |
| 40 | r := claimCensusBox(t) |
| 41 | reg, err := registry.Open(filepath.Join(r.DataDir, "registry.db")) |
| 42 | if err != nil { |
| 43 | t.Fatal(err) |
| 44 | } |
| 45 | defer reg.Close() |
| 46 | inventory, err := openLegacyInventory(t, filepath.Join(r.DataDir, ".hostclaims", "inventory.db")) |
| 47 | if err != nil { |
| 48 | t.Fatal(err) |
| 49 | } |
| 50 | defer inventory.Close() |
| 51 | launches, err := hostbox.OpenLaunchJournal(filepath.Join(r.DataDir, ".hostclaims", "launches.db")) |
| 52 | if err != nil { |
| 53 | t.Fatal(err) |
| 54 | } |
| 55 | defer launches.Close() |
| 56 | for path, body := range files { |
| 57 | full := filepath.Join(r.DataDir, filepath.FromSlash(path)) |
| 58 | if err := os.MkdirAll(filepath.Dir(full), 0o700); err != nil { |
| 59 | t.Fatal(err) |
| 60 | } |
| 61 | if err := os.WriteFile(full, []byte(body), 0o600); err != nil { |
| 62 | t.Fatal(err) |
| 63 | } |
| 64 | } |
| 65 | store := release.NewDirStore(t.TempDir()) |
| 66 | created := time.Date(2026, 10, 7, 0, 0, 0, 0, time.UTC) |
| 67 | record := provision.Record{Schema: 1, Host: "shop.example", Slug: "shop", App: "app", Account: "acct", Region: "ie", Channel: "stable", CreatedAt: created} |
| 68 | if err := provision.CreateRecord(ctx, store, record); err != nil { |
| 69 | t.Fatal(err) |
| 70 | } |
| 71 | key := provision.RecordKey(record.Account, record.App, record.Host) |
| 72 | obs, err := hostclaim.Read(ctx, store, record.Host) |
| 73 | if err != nil { |
| 74 | t.Fatal(err) |
| 75 | } |
| 76 | obs, err = hostclaim.Update(ctx, store, obs, hostclaim.Adopt(hostclaim.NewHolder{Family: "instance", Key: key, Account: record.Account, App: record.App, Host: record.Host, Region: record.Region, CreatedAt: created}, "box", nil, nil, "adopt", func() string { return "id" })) |
| 77 | if err != nil { |
| 78 | t.Fatal(err) |
| 79 | } |
| 80 | selected := hostbox.RecordSelection{Host: record.Host, Incarnation: obs.Claim.Holder.Incarnation} |
| 81 | census := hostbox.EmptyInstallationCensus{Inventory: inventory, Registry: reg, Launches: launches, Box: "box", Enrollment: "enrollment", DirectoryRevision: "1", |
| 82 | Roots: map[string]string{"data": r.DataDir, "opt": r.OptDir, "units": r.UnitsDir}, SharedPaths: claimGlobalSharedPaths(r)} |
| 83 | unclassified := census |
| 84 | unclassified.SharedPaths = map[string][]string{} |
| 85 | for root, entries := range claimGlobalSharedPaths(r) { |
| 86 | for _, e := range entries { |
| 87 | if e != classified { |
| 88 | unclassified.SharedPaths[root] = append(unclassified.SharedPaths[root], e) |
| 89 | } |
| 90 | } |
| 91 | } |
| 92 | if err := unclassified.Initialize(ctx, obs, selected); !errors.Is(err, hostbox.ErrTypedInventory) { |
| 93 | t.Fatalf("without %s classified the files were accepted: %v", classified, err) |
| 94 | } |
| 95 | if err := census.Initialize(ctx, obs, selected); err != nil { |
| 96 | t.Fatalf("initialisation with %s present: %v", classified, err) |
| 97 | } |
| 98 | } |
| 99 | |