carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package host
2
3import (
4 "context"
5 "crypto/sha256"
6 "encoding/hex"
7 "encoding/json"
8 "errors"
9 "os/exec"
10 "path/filepath"
11 "strconv"
12 "strings"
13 "testing"
14 "time"
15
16 "github.com/carlosframework/platform/internal/genselect"
17 "github.com/carlosframework/platform/internal/hostbox"
18 "github.com/carlosframework/platform/internal/hostclaim"
19 "github.com/carlosframework/platform/internal/hostfiles"
20 "github.com/carlosframework/platform/internal/hostwrite"
21 "github.com/carlosframework/platform/internal/lease/leasetest"
22 "github.com/carlosframework/platform/internal/moves"
23 "github.com/carlosframework/platform/internal/registry"
24 "github.com/carlosframework/platform/internal/release"
25)
26
27// relocationProbeRig is P's first probe on box-1 through the claim's own
28// runtime (the recovery fixture's controller), the relocation owners bound
29// as claimGroupDWiring binds them, the probe attempt counted and pending.
30type relocationProbeRig struct {
31 f *recoveryFixture
32 bucket *leasetest.VersionedBucket
33 files *release.DirStore
34 c8 *roFiles
35 inputs *RelocationWakeInputs
36 c *recoveryController
37 h *hostclaim.Holder
38 o hostclaim.Observed
39 im moves.InstallManifest
40 auth hostclaim.PreparedAuthority
41 route registry.Route
42}
43
44// roProbeLaunch is the serving launch source's result for the rig: the
45// live link hashed as build v1, staged under v1.
46func roProbeLaunch() ClaimServingLaunch {
47 return ClaimServingLaunch{Identity: hostwrite.Identity{Config: "sha256:config", Release: "sha256:release"}, Executable: "/usr/bin/true", ExecutableSHA256: strings.Repeat("a", 64), Version: "v1",
48 Staged: release.StagedIdentity{Version: "v1", StagedPath: "/opt/carlos/versions/v1/bab/jam", Digest: strings.Repeat("a", 64)}}
49}
50
51func newRelocationProbeRig(t *testing.T, launch ClaimServingLaunch) *relocationProbeRig {
52 t.Helper()
53 ctx := t.Context()
54 f := newRecoveryFixture(t)
55 bucket := leasetest.NewVersionedBucket()
56 bucket.AddBucket("data")
57 files := release.NewDirStore(t.TempDir())
58 o := f.read()
59 h := o.Claim.Holder
60 prefix := moves.GenerationPrefix(h.Location, roP)
61 snap, anchor := moves.SnapshotKey(prefix, 42), moves.AnchorKey(prefix, 42)
62 im := moves.InstallManifest{V: 1, Op: roOp, Host: recoveryHost, N: 42, SnapshotKey: snap, SnapshotVersion: bucket.Put("data", snap, ltxHeader(t, 1, 42)), SnapshotCRC64: 7,
63 AnchorKey: anchor, AnchorVersion: bucket.Put("data", anchor, ltxHeader(t, 42, 42)), IntegrityCheck: "ok", DBSHA256: strings.Repeat("d", 64)}
64 imRaw := mustJSON(t, im)
65 bucket.Put("data", moves.InstallManifestKey(prefix), imRaw)
66 pm := mustJSON(t, hostfiles.Manifest{V: hostfiles.ManifestVersion, Entries: map[string]hostfiles.Entry{}, Platform: map[string]hostfiles.Entry{}})
67 if _, err := files.Put(ctx, hostfiles.ManifestKey(moves.GenerationFilesPrefix(h.Location, roP)), pm, ""); err != nil {
68 t.Fatal(err)
69 }
70 imSum, pmSum := sha256.Sum256(imRaw), sha256.Sum256(pm)
71 auth := hostclaim.PreparedAuthority{Generation: roP, InstallManifest: moves.InstallManifestKey(prefix), SHA256: hex.EncodeToString(imSum[:]), SnapshotTXID: 42, SourceFilesSeq: 5, FilesManifestSHA256: hex.EncodeToString(pmSum[:])}
72 ref := hostclaim.RecoveryPin{Key: "control/relocations/" + recoveryHost + "/" + roOp + "/pin-1.json", SHA256: strings.Repeat("e", 64), Generation: h.Generation, MaxTXID: 42, FilesSeq: 5, Release: "v1"}
73 o, err := hostclaim.Update(ctx, f.claims, o, func(c *hostclaim.Claim) error {
74 c.Holder.Stamp = c.Gen + 1
75 auth.InstallStamp = c.Holder.Stamp
76 c.Op = hostclaim.Op{Kind: hostclaim.KindRelocating, ID: roOp, Phase: hostclaim.PhaseRelocatingProbing, Exec: "box-1", Participants: []string{"box-0", "box-1"},
77 Intent: map[string]string{"region": "ie", "source_box": "box-0", "target_box": "box-1", "target_enrollment": "enrollment-1"},
78 Journal: map[string]string{"pin": string(mustJSON(t, ref)), "prepared": roP, "install_stamp": strconv.FormatUint(c.Holder.Stamp, 10), "install_release": "v1",
79 "prepared_authority": string(mustJSON(t, auth)), "probing_since": f.now.Format(time.RFC3339Nano), "exec_ack.box-0": `{"op":"rel-1"}`, "exec_ack.box-1": `{"op":"rel-1"}`,
80 "local_install": string(mustJSON(t, hostclaim.LocalInstall{Box: "box-1", Enrollment: "enrollment-1", InstallStamp: c.Holder.Stamp, JournalSHA256: strings.Repeat("c", 64)}))}}
81 return nil
82 })
83 if err != nil {
84 t.Fatal(err)
85 }
86 if o, err = hostclaim.Update(ctx, f.claims, o, hostclaim.BeginProbeAttempt(roOp, "box-1")); err != nil {
87 t.Fatal(err)
88 }
89 c8 := &roFiles{}
90 inputs := &RelocationWakeInputs{
91 Launch: func(context.Context, registry.Route) (ClaimServingLaunch, error) { return launch, nil },
92 FilesManifest: c8, Markers: plan9D5Markers{bucket}, Claims: f.claims, FilesStore: files, Data: bucket, Bucket: "data", Region: "ie", Box: "box-1",
93 NewID: f.newID, Now: func() time.Time { return f.now }, Releases: &roReleases{}}
94 attempts := &relocationProbeAttempts{Claims: f.claims, DataDir: t.TempDir(), Box: "box-1", NewID: f.newID, Now: func() time.Time { return f.now }}
95 c := f.controller("boot-1", "controller-1", 4101, "start-1")
96 c.wake.Inputs = ClaimOwnerDispatch{Relocation: &ClaimKindOwners{Inputs: inputs, AdmissionEvidence: inputs.AdmissionEvidence, PublicationEvidence: inputs.PublicationEvidence}}
97 c.wake.Continue = hostclaim.SessionContinues
98 // As claimGroupDWiring routes them once the relocation owners bind.
99 ready := func(context.Context, *exec.Cmd, registry.Route, ClaimLaunchInputs, string) error { return nil }
100 c.wake.Operations = claimOperationRouter{hostclaim.KindRelocating: {Attempts: attempts, ProbeReady: ready}}
101 live, cancel := context.WithCancel(context.Background())
102 t.Cleanup(cancel)
103 done := make(chan struct{})
104 go func() { <-live.Done(); close(done) }()
105 c.wake.mu.Lock()
106 c.wake.pending[recoveryHost] = &claimWakeAttempt{observed: o, held: recoveryHeld{}, ctx: live, cancel: cancel, done: done}
107 c.wake.mu.Unlock()
108 route := registry.Route{Host: recoveryHost, Kind: "instance", App: "jam", Account: "bab", Status: "relocating", HoldOp: roOp, Socket: filepath.Join(f.guard.DataDir, "app.sock"), DBPath: filepath.Join(f.guard.DataDir, "state", "data.db")}
109 return &relocationProbeRig{f: f, bucket: bucket, files: files, c8: c8, inputs: inputs, c: c, h: h, o: o, im: im, auth: auth, route: route}
110}
111
112// Finding 6, ruling 47: P's first probe publishes through the shared
113// assembly. The claim's own runtime (the recovery fixture's controller)
114// admits P's opening through the relocation dispatch, binds region HA's
115// attempt journal to the probe start, and Materialize, under the admitted
116// spawn selection, runs moves' C8 Establish and writes P's marker: P as
117// the marker's op, the install manifest's pin, prepared_authority's
118// digest. Publish (the pre-replication call) finds that marker and
119// records the receipt; PublicationEvidence then passes. Publish without
120// Materialize's fence refuses.
121func TestRelocationFirstProbePublishesThroughTheAssembly(t *testing.T) {
122 r := newRelocationProbeRig(t, roProbeLaunch())
123 ctx := t.Context()
124 f, bucket, files, c8, inputs, c, h, o, im, auth, route := r.f, r.bucket, r.files, r.c8, r.inputs, r.c, r.h, r.o, r.im, r.auth, r.route
125 admitted, err := c.runtime.Admit(ctx, route, o, roProbe)
126 if err != nil {
127 t.Fatal(err)
128 }
129 ah := admitted.Claim.Holder
130 if ah.Generation != roP || ah.Stamp != o.Claim.Holder.Stamp || ah.WriterBox != "box-1" || !hostclaim.PreparedOpened(admitted.Claim) {
131 t.Fatalf("P's opening: %+v", ah)
132 }
133 s, err := c.runtime.Prepare(ctx, route, admitted, roProbe)
134 if err != nil {
135 t.Fatal(err)
136 }
137 if s.OpenProbe == nil {
138 t.Fatal("the probe start carries no attempt journal")
139 }
140 if _, err := s.OpenProbe(ctx, f.now); err != nil {
141 t.Fatal(err)
142 }
143 if err := s.Reserve(ctx, 0); err != nil {
144 t.Fatal(err)
145 }
146 if a := s.CurrentAttempt(); a.ID == "" || a.Session != ah.Session || a.ControllerID != "controller-1" {
147 t.Fatalf("the reservation: %+v", a)
148 }
149 if err := s.Materialize(ctx); err != nil {
150 t.Fatal(err)
151 }
152 if len(c8.established) != 1 {
153 t.Fatalf("C8 established %v", c8.established)
154 }
155 markerKey := strings.TrimSuffix(h.Location, "/") + "/generation.json"
156 body, ok := bucket.Current("data", markerKey)
157 var m genselect.Marker
158 if !ok || json.Unmarshal(body, &m) != nil || m.Op != roP || m.Box != "box-1" || m.Host != recoveryHost || m.ManifestSHA256 != auth.SHA256 || m.Pin != manifestPin(im) {
159 t.Fatalf("P's marker: %s", body)
160 }
161 written := bucket.LatestID("data", markerKey)
162 // The pre-replication publish (ClaimWakeAssembly s.Replicate's first
163 // call for a prepared admission) finds the marker and records it.
164 in, err := inputs.Select(ctx, route, o, roProbe)
165 if err != nil {
166 t.Fatal(err)
167 }
168 if err := inputs.Publish(ctx, admitted, in); err != nil {
169 t.Fatal(err)
170 }
171 if bucket.LatestID("data", markerKey) != written {
172 t.Fatal("Publish rewrote P's marker")
173 }
174 if err := inputs.PublicationEvidence(ctx, admitted); err != nil {
175 t.Fatalf("publication evidence: %v", err)
176 }
177 // A fresh wake inputs (another process) has no fence for the session.
178 other := &RelocationWakeInputs{Launch: inputs.Launch, FilesManifest: c8, Markers: inputs.Markers, Claims: f.claims, FilesStore: files, Data: bucket, Bucket: "data", Region: "ie", Box: "box-1",
179 NewID: f.newID, Now: inputs.Now, Releases: inputs.Releases}
180 if err := other.Publish(ctx, admitted, in); !errors.Is(err, hostbox.ErrChanged) {
181 t.Fatalf("Publish without Materialize's fence: %v", err)
182 }
183}
184
185// L18 joint round, final 4, item 1 (ruling R-EXEC-CLAIMVERSION): the
186// relocation probe's start names the file its launch source hashed. With
187// the production BeforeExec hook set, the activator refuses any claimed
188// start whose Staged.Version is not its Version (Service.claimedBuild,
189// run on the start the runtime's Prepare returns), so a probe selection
190// that drops Staged never starts. The start Prepare hands the activator
191// for P's prepared opening carries the launch's Staged, and so does every
192// selection: a later cold attempt's and an empty P's.
193func TestTheRelocationProbeStartNamesTheBuildItHashed(t *testing.T) {
194 ctx := t.Context()
195 launch := roProbeLaunch()
196 r := newRelocationProbeRig(t, launch)
197 admitted, err := r.c.runtime.Admit(ctx, r.route, r.o, roProbe)
198 if err != nil {
199 t.Fatal(err)
200 }
201 s, err := r.c.runtime.Prepare(ctx, r.route, admitted, roProbe)
202 if err != nil {
203 t.Fatal(err)
204 }
205 // claimedBuild's refusal, as the activator applies it with BeforeExec set.
206 if s.Staged != launch.Staged || s.Version != launch.Version || s.Staged.Version != s.Version {
207 t.Fatalf("the probe start names build %q but staged %+v; the launch hashed %+v", s.Version, s.Staged, launch.Staged)
208 }
209
210 for _, o := range []roOptions{{}, {empty: true}} {
211 f := newROFixture(t, o)
212 source := f.inputs.Launch
213 staged := release.StagedIdentity{Version: "v1", StagedPath: "/opt/carlos/versions/v1/acct/app", Digest: strings.Repeat("b", 64)}
214 f.inputs.Launch = func(ctx context.Context, rt registry.Route) (ClaimServingLaunch, error) {
215 l, err := source(ctx, rt)
216 l.Staged = staged
217 return l, err
218 }
219 first, err := f.inputs.Select(ctx, f.route, f.read(), roProbe)
220 if err != nil {
221 t.Fatal(err)
222 }
223 if first.Staged != staged || first.Staged.Version != first.Version {
224 t.Fatalf("empty=%v: the first attempt names build %q but staged %+v", o.empty, first.Version, first.Staged)
225 }
226 if o.empty {
227 continue
228 }
229 again, err := f.inputs.Select(ctx, f.route, f.admit(f.read(), first), roProbe)
230 if err != nil {
231 t.Fatal(err)
232 }
233 if again.Prepared || again.Staged != staged || again.Staged.Version != again.Version {
234 t.Fatalf("a later attempt (prepared %v) names build %q but staged %+v", again.Prepared, again.Version, again.Staged)
235 }
236 }
237}
238
239// Ruling 47: a later attempt's fresh generation, and an empty P's opening,
240// publish through main's fenced writer only after their first root.
241func TestRelocationLaterAttemptsPublishFenced(t *testing.T) {
242 ctx := t.Context()
243 f := newROFixture(t, roOptions{})
244 first, err := f.inputs.Select(ctx, f.route, f.read(), roProbe)
245 if err != nil {
246 t.Fatal(err)
247 }
248 opened := f.admit(f.read(), first)
249 again, err := f.inputs.Select(ctx, f.route, opened, roProbe)
250 if err != nil {
251 t.Fatal(err)
252 }
253 later := f.admit(opened, again)
254 gen := later.Claim.Holder.Generation
255 synced := 0
256 pub := ClaimPublication{Sync: func(context.Context) error { synced++; return nil }, Check: func(context.Context) error { return nil }}
257 if err := f.inputs.Publish(ctx, later, again); !errors.Is(err, hostbox.ErrChanged) {
258 t.Fatalf("an unfenced marker for a fresh generation: %v", err)
259 }
260 if _, err := f.inputs.PublishFenced(ctx, later, again, pub); err == nil || synced != 1 {
261 t.Fatalf("a marker before the root: %v synced=%d", err, synced)
262 }
263 root := moves.SnapshotKey(moves.GenerationPrefix(later.Claim.Holder.Location, gen), 44)
264 f.bucket.Put("data", root, ltxHeader(t, 1, 44))
265 receipt, err := f.inputs.PublishFenced(ctx, later, again, pub)
266 if err != nil || receipt.Root.Key != root || receipt.MarkerVersion == "" {
267 t.Fatalf("publish: %+v %v", receipt, err)
268 }
269 if err := f.inputs.PublicationEvidence(ctx, later); err != nil {
270 t.Fatalf("fresh evidence: %v", err)
271 }
272 prepared := again
273 prepared.Prepared = true
274 if _, err := f.inputs.PublishFenced(ctx, later, prepared, pub); !errors.Is(err, hostbox.ErrChanged) {
275 t.Fatalf("a prepared admission through the fenced writer: %v", err)
276 }
277
278 // An empty P: no snapshot, no root until the first transaction.
279 e := newROFixture(t, roOptions{empty: true})
280 ein, err := e.inputs.Select(ctx, e.route, e.read(), roProbe)
281 if err != nil {
282 t.Fatal(err)
283 }
284 eopened := e.admit(e.read(), ein)
285 if _, err := e.inputs.PublishFenced(ctx, eopened, ein, pub); err == nil {
286 t.Fatal("an empty P published a marker with no root")
287 }
288}
289