carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1//go:build unix
2
3package hostbox
4
5import (
6 "crypto/sha256"
7 "encoding/hex"
8 "errors"
9 "fmt"
10 "os"
11 "path/filepath"
12 "testing"
13 "time"
14
15 "github.com/carlosframework/platform/internal/hostguard"
16 "github.com/carlosframework/platform/internal/provision"
17 "github.com/carlosframework/platform/internal/registry"
18)
19
20// N4: a relocation target's held row is minted through the same typed
21// inventory as reconcile's own (ownedRow): on an empty installation the
22// census initialises, the route entry is recorded and materialized, and
23// the row is held by the op with its ClaimOwnership bound to the captured
24// stamp. A second call is idempotent. Another incarnation and a flat route
25// refuse with ErrChanged; a host-named file under the data root refuses
26// with ErrTypedInventory before any row is written.
27func TestMintHeldBindsTheRowToTheInstallStamp(t *testing.T) {
28 type mint struct {
29 f admissionFixture
30 reg *registry.Registry
31 inv *StopInventoryDB
32 adapter ReconcileAdapter
33 rec provision.ClaimRecord
34 captured hostguard.Captured
35 }
36 setup := func(t *testing.T) mint {
37 t.Helper()
38 f := newAdmissionFixture(t, false)
39 reg, err := registry.Open(filepath.Join(t.TempDir(), "registry.db"))
40 if err != nil {
41 t.Fatal(err)
42 }
43 t.Cleanup(func() { reg.Close() })
44 inv, err := OpenStopInventoryDB(filepath.Join(t.TempDir(), "inventory.db"))
45 if err != nil {
46 t.Fatal(err)
47 }
48 t.Cleanup(func() { inv.Close() })
49 launches, err := OpenLaunchJournal(filepath.Join(t.TempDir(), "launches.db"))
50 if err != nil {
51 t.Fatal(err)
52 }
53 t.Cleanup(func() { launches.Close() })
54 census := &EmptyInstallationCensus{Inventory: inv, Registry: reg, Launches: launches, Box: "box", Enrollment: "en", DirectoryRevision: "1",
55 Roots: map[string]string{"data": f.data, "opt": t.TempDir(), "units": t.TempDir()}, SharedPaths: map[string][]string{"data": {"hostguard"}}}
56 adapter := ReconcileAdapter{Registry: reg, Inventory: inv, Census: census, Box: "box", Enrollment: "en", DirectoryRevision: "1",
57 Reconciler: Reconciler{Claims: f.store, Box: "box", Region: "ie", Guard: hostguard.Guard{DataDir: f.data, LockDir: t.TempDir(), Claims: hostguard.StoreReader{Store: f.store}}}}
58 record := provision.Record{Schema: 1, Host: f.host, Slug: "shop", App: "app", Account: f.account, Region: "ie", Channel: "stable", CreatedAt: time.Date(2026, 10, 7, 0, 0, 0, 0, time.UTC)}
59 rec := provision.ClaimRecord{Host: f.host, Key: f.key, Family: "instance", Account: f.account, App: "app", CreatedAt: record.CreatedAt, Value: record,
60 Route: provision.RouteFor(record, f.data, "", true, nil)}
61 return mint{f: f, reg: reg, inv: inv, adapter: adapter, rec: rec, captured: hostguard.Captured{Incarnation: f.holder.Incarnation, Stamp: f.holder.Stamp}}
62 }
63 ctx := t.Context()
64 t.Run("from an empty installation", func(t *testing.T) {
65 m := setup(t)
66 if err := m.adapter.MintHeld(ctx, m.rec, m.captured, "relocating", "op-1"); err != nil {
67 t.Fatal(err)
68 }
69 row, err := m.reg.Get(m.f.host)
70 if err != nil || row == nil {
71 t.Fatalf("row: %+v %v", row, err)
72 }
73 sum := sha256.Sum256([]byte(fmt.Sprintf("%s\x00%s\x00%d\x00%s", m.f.host, m.captured.Incarnation, m.captured.Stamp, "box")))
74 if row.Status != "relocating" || row.HoldOp != "op-1" || row.ClaimOwnership.ClaimStamp != m.captured.Stamp || row.ClaimOwnership.ClaimOwner != hex.EncodeToString(sum[:]) ||
75 row.ClaimOwnership.ClaimIncarnation != m.captured.Incarnation || !provision.NestedPaths(m.f.data, *row) {
76 t.Fatalf("row %+v", row)
77 }
78 in, err := m.inv.ReadInstallation(ctx, m.f.host, m.captured.Incarnation, m.captured.Stamp)
79 if err != nil || !in.Complete {
80 t.Fatalf("inventory: %+v %v", in, err)
81 }
82 materialized := 0
83 for _, e := range in.Entries {
84 if e.Kind == "route" && e.RelativePath == m.f.host && e.Materialization == "materialized" && e.RowIdentity == row.ClaimOwnership.ClaimRowID {
85 materialized++
86 }
87 }
88 if materialized != 1 {
89 t.Fatalf("route entries: %+v", in.Entries)
90 }
91 if err := m.adapter.MintHeld(ctx, m.rec, m.captured, "relocating", "op-1"); err != nil {
92 t.Fatalf("a second mint: %v", err)
93 }
94 again, err := m.reg.Get(m.f.host)
95 if err != nil || *again != *row {
96 t.Fatalf("the second mint changed the row: %+v %v", again, err)
97 }
98 if err := m.adapter.MintHeld(ctx, m.rec, m.captured, "relocating", "op-2"); !errors.Is(err, registry.ErrClaimOwnership) {
99 t.Fatalf("another op's mint over a held row: %v", err)
100 }
101 })
102 t.Run("another incarnation", func(t *testing.T) {
103 m := setup(t)
104 m.captured.Incarnation = "other"
105 if err := m.adapter.MintHeld(ctx, m.rec, m.captured, "relocating", "op-1"); !errors.Is(err, ErrChanged) {
106 t.Fatalf("got %v", err)
107 }
108 })
109 t.Run("a flat route", func(t *testing.T) {
110 m := setup(t)
111 m.rec.Route = provision.RouteFor(m.rec.Value.(provision.Record), m.f.data, "", false, nil)
112 if err := m.adapter.MintHeld(ctx, m.rec, m.captured, "relocating", "op-1"); !errors.Is(err, ErrChanged) {
113 t.Fatalf("got %v", err)
114 }
115 })
116 t.Run("not a hold status", func(t *testing.T) {
117 m := setup(t)
118 if err := m.adapter.MintHeld(ctx, m.rec, m.captured, "ready", "op-1"); !errors.Is(err, ErrUnsupported) {
119 t.Fatalf("got %v", err)
120 }
121 })
122 t.Run("a host-named file under the data root", func(t *testing.T) {
123 m := setup(t)
124 if err := os.WriteFile(filepath.Join(m.f.data, m.f.host+".env"), []byte("X=1\n"), 0o600); err != nil {
125 t.Fatal(err)
126 }
127 if err := m.adapter.MintHeld(ctx, m.rec, m.captured, "relocating", "op-1"); !errors.Is(err, ErrTypedInventory) {
128 t.Fatalf("got %v", err)
129 }
130 if row, err := m.reg.Get(m.f.host); err != nil || row != nil {
131 t.Fatalf("a row was written: %+v %v", row, err)
132 }
133 })
134}
135