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 "context"
7 "os"
8 "path/filepath"
9 "strconv"
10 "testing"
11 "time"
12
13 "github.com/carlosframework/platform/internal/hostclaim"
14 "github.com/carlosframework/platform/internal/hostguard"
15 "github.com/carlosframework/platform/internal/hostwrite"
16 "github.com/carlosframework/platform/internal/park"
17 "github.com/carlosframework/platform/internal/provision"
18 "github.com/carlosframework/platform/internal/registry"
19 "github.com/carlosframework/platform/internal/release"
20)
21
22// relocationReconcile is the box "box" as a relocation target: the
23// production adapter with the real census and park.Local, region HA's
24// relocation-aware reconciler (FenceClosed and HoldRow set), and the
25// target's row minted held by MintHeld at the install stamp.
26type relocationReconcile struct {
27 f admissionFixture
28 reg *registry.Registry
29 adapter ReconcileAdapter
30 cfg provision.ReconcileConfig
31 captured hostguard.Captured
32 holds []string
33}
34
35func newRelocationReconcile(t *testing.T) *relocationReconcile {
36 t.Helper()
37 ctx := t.Context()
38 f := newAdmissionFixture(t, false)
39 r := &relocationReconcile{f: f}
40 var err error
41 if r.reg, err = registry.Open(filepath.Join(t.TempDir(), "registry.db")); err != nil {
42 t.Fatal(err)
43 }
44 t.Cleanup(func() { r.reg.Close() })
45 inv, err := OpenStopInventoryDB(filepath.Join(t.TempDir(), "inventory.db"))
46 if err != nil {
47 t.Fatal(err)
48 }
49 t.Cleanup(func() { inv.Close() })
50 launches, err := OpenLaunchJournal(filepath.Join(t.TempDir(), "launches.db"))
51 if err != nil {
52 t.Fatal(err)
53 }
54 t.Cleanup(func() { launches.Close() })
55 census := &EmptyInstallationCensus{Inventory: inv, Registry: r.reg, Launches: launches, Box: "box", Enrollment: "en", DirectoryRevision: "1",
56 Roots: map[string]string{"data": f.data, "opt": t.TempDir(), "units": t.TempDir()}, SharedPaths: map[string][]string{"data": {"hostguard"}}}
57 guard := hostguard.Guard{DataDir: f.data, LockDir: t.TempDir(), Claims: hostguard.StoreReader{Store: f.store}}
58 source := WorkSource{Guard: guard, Box: "box", Region: "ie", Now: time.Now,
59 Policy: func(context.Context, *hostclaim.Claim, Selection, hostwrite.Identity) error { return nil },
60 Local: func(context.Context, *hostclaim.Claim, Selection) error { return nil }}
61 r.adapter = ReconcileAdapter{Registry: r.reg, Inventory: inv, Census: census, Box: "box", Enrollment: "en", DirectoryRevision: "1",
62 Reconciler: Reconciler{Claims: f.store, Box: "box", Region: "ie", Guard: guard, Writes: hostwrite.Config{Enabled: true, Selector: source},
63 Local: park.Local{Guard: guard, Claims: f.store, DataDir: f.data, Box: "box"},
64 Now: func() time.Time { return time.Unix(100, 0) }, CheckRecord: func(context.Context, RecordSelection) error { return nil }, CheckLocal: census.CheckLocal,
65 FenceClosed: func(context.Context, hostclaim.Observed) (bool, error) { return false, nil },
66 HoldRow: func(_ context.Context, host, op string) error {
67 r.holds = append(r.holds, op)
68 row, err := r.reg.Get(host)
69 if err != nil || row == nil {
70 return ErrChanged
71 }
72 if row.HoldOp == op {
73 return nil
74 }
75 return r.reg.SetHold(host, "relocating", op)
76 }}}
77 r.cfg = provision.ReconcileConfig{DataDir: f.data, Region: "ie"}
78 r.cfg.Layout = func(context.Context, release.Store) (string, error) { return release.LayoutModeNested, nil }
79 r.adapter.Configure(&r.cfg)
80 // The installing CAS: this box executes rel-1 at the install stamp.
81 o, err := hostclaim.Read(ctx, f.store, f.host)
82 if err != nil {
83 t.Fatal(err)
84 }
85 if o, err = hostclaim.Update(ctx, f.store, o, func(c *hostclaim.Claim) error {
86 c.Holder.Stamp = c.Gen + 1
87 c.Op = hostclaim.Op{Kind: hostclaim.KindRelocating, ID: "rel-1", Phase: hostclaim.PhaseInstalling, Exec: "box", Participants: []string{"src", "box"},
88 Intent: map[string]string{"region": "ie", "source_box": "src", "target_box": "box"}, Journal: map[string]string{"install_stamp": strconv.FormatUint(c.Holder.Stamp, 10)}}
89 return nil
90 }); err != nil {
91 t.Fatal(err)
92 }
93 if err := guard.Activate(ctx, f.host, "box", nil); err != nil {
94 t.Fatal(err)
95 }
96 r.captured = hostguard.Captured{Incarnation: o.Claim.Holder.Incarnation, Stamp: o.Claim.Holder.Stamp}
97 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)}
98 rec := provision.ClaimRecord{Host: f.host, Key: f.key, Family: "instance", Account: f.account, App: "app", CreatedAt: record.CreatedAt, Value: record, Route: provision.RouteFor(record, f.data, "", true, nil)}
99 if err := r.adapter.MintHeld(ctx, rec, r.captured, "relocating", "rel-1"); err != nil {
100 t.Fatal(err)
101 }
102 return r
103}
104
105func (r *relocationReconcile) phase(t *testing.T, tr func(*hostclaim.Claim)) hostclaim.Observed {
106 t.Helper()
107 o, err := hostclaim.Read(t.Context(), r.f.store, r.f.host)
108 if err != nil {
109 t.Fatal(err)
110 }
111 if o, err = hostclaim.Update(t.Context(), r.f.store, o, func(c *hostclaim.Claim) error { tr(c); return nil }); err != nil {
112 t.Fatal(err)
113 }
114 return o
115}
116
117func targetGuardFiles(t *testing.T, data string) map[string]string {
118 t.Helper()
119 out := map[string]string{}
120 entries, err := os.ReadDir(filepath.Join(data, "hostguard"))
121 if err != nil {
122 t.Fatal(err)
123 }
124 for _, e := range entries {
125 raw, err := os.ReadFile(filepath.Join(data, "hostguard", e.Name()))
126 if err != nil {
127 t.Fatal(err)
128 }
129 out[e.Name()] = string(raw)
130 }
131 return out
132}
133
134// Ruling 31: under a relocating claim reconcile never activates the
135// target. The target's held row (minted by its install) is left to the
136// install at installing, installed and probing: stage A's check refuses
137// the executor's reconcile before any write (claim Plan 11 admits no
138// reconcile action under relocating), so the row, the guard record and
139// the claim are unchanged and no hold is written.
140func TestReconcileLeavesARelocatingTargetsRowToItsInstall(t *testing.T) {
141 ctx := t.Context()
142 r := newRelocationReconcile(t)
143 before, err := r.reg.Get(r.f.host)
144 if err != nil || before == nil {
145 t.Fatal(err)
146 }
147 for _, phase := range []string{hostclaim.PhaseInstalling, hostclaim.PhaseInstalled, hostclaim.PhaseRelocatingProbing} {
148 o := r.phase(t, func(c *hostclaim.Claim) { c.Op.Phase = phase })
149 guard := targetGuardFiles(t, r.f.data)
150 r.holds = nil
151 if err := r.f.pass(ctx, r.f.store, r.reg, r.cfg); err != nil {
152 t.Fatalf("%s: %v", phase, err)
153 }
154 row, err := r.reg.Get(r.f.host)
155 if err != nil || row == nil || *row != *before {
156 t.Fatalf("%s: the row changed: %+v %v", phase, row, err)
157 }
158 if len(r.holds) != 0 {
159 t.Fatalf("%s: a hold was written: %v", phase, r.holds)
160 }
161 if got := targetGuardFiles(t, r.f.data); len(got) != len(guard) || got[r.f.host+".json"] != guard[r.f.host+".json"] {
162 t.Fatalf("%s: the guard record changed", phase)
163 }
164 after, err := hostclaim.Read(ctx, r.f.store, r.f.host)
165 if err != nil || after.ETag != o.ETag {
166 t.Fatalf("%s: the claim was written: %v", phase, err)
167 }
168 }
169}
170
171// Rev 1 finding 5: after completion the target serves at the install
172// stamp; relocationReleaseHold releases the row's hold (registry
173// ReleaseHold, under the op); reconcile's real adapter then finds the
174// installation's inventory coherent and keeps the row, ready and owned by
175// the same installation.
176func TestReconcileAdapterClearsTheHoldAfterCompletion(t *testing.T) {
177 ctx := t.Context()
178 r := newRelocationReconcile(t)
179 minted, err := r.reg.Get(r.f.host)
180 if err != nil || minted == nil {
181 t.Fatal(err)
182 }
183 r.phase(t, func(c *hostclaim.Claim) {
184 c.Op = hostclaim.Op{Kind: hostclaim.KindServing, Exec: "box", Participants: []string{"box"}}
185 c.Holder.WriterBox = "box"
186 })
187 if changed, err := r.reg.ReleaseHold(r.f.host, "rel-1"); err != nil || !changed {
188 t.Fatalf("release hold: %v %v", changed, err)
189 }
190 if err := r.f.pass(ctx, r.f.store, r.reg, r.cfg); err != nil {
191 t.Fatalf("the serving reconcile: %v", err)
192 }
193 row, err := r.reg.Get(r.f.host)
194 if err != nil || row == nil || row.Status == "relocating" || row.HoldOp != "" || row.ClaimOwnership != minted.ClaimOwnership || !provision.NestedPaths(r.f.data, *row) {
195 t.Fatalf("after completion: %+v %v (minted %+v)", row, err, minted.ClaimOwnership)
196 }
197 if len(r.holds) != 0 {
198 t.Fatalf("a serving claim took the hold-only path: %v", r.holds)
199 }
200}
201