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 "encoding/base64"
6 "errors"
7 "fmt"
8 "reflect"
9 "strings"
10 "sync"
11 "testing"
12 "time"
13
14 "github.com/carlosframework/platform/internal/activator"
15 "github.com/carlosframework/platform/internal/hostbox"
16 "github.com/carlosframework/platform/internal/hostclaim"
17 "github.com/carlosframework/platform/internal/hostguard"
18 "github.com/carlosframework/platform/internal/hostops"
19 "github.com/carlosframework/platform/internal/hostwrite"
20 "github.com/carlosframework/platform/internal/lease"
21 "github.com/carlosframework/platform/internal/lease/leasetest"
22 "github.com/carlosframework/platform/internal/moves"
23 "github.com/carlosframework/platform/internal/park"
24 "github.com/carlosframework/platform/internal/registry"
25 "github.com/carlosframework/platform/internal/release"
26)
27
28// Plan 5 Task 9b (moves-after-Plan-12 Task 8; rulings Q1, Q12, Q15): the
29// moves wake source is registered right after the parks, with both
30// switches on, and its registration is what opens the one gate
31// (movesWakeSourceRegistered), so the parks arm in the same agent start as
32// the wake owners and never before.
33
34// mwSecretsKey is a CARLOS_SECRETS_KEY for the registered source's env
35// report key and held render: a valid P-256 scalar.
36var mwSecretsKey = base64.StdEncoding.EncodeToString([]byte("0123456789abcdef0123456789abcdef"))
37
38// mwEnv is an environment with the two switches as given.
39func mwEnv(claim, phase1 bool) func(string) string {
40 vals := map[string]string{"CARLOS_SECRETS_KEY": mwSecretsKey}
41 if claim {
42 vals["CARLOS_HOST_CLAIM"] = "1"
43 }
44 if phase1 {
45 vals[moves.SwitchEnv] = "on"
46 }
47 return func(k string) string { return vals[k] }
48}
49
50// mwReadAccess is the claim's move read access as a fixture: the route and
51// launch it answers, and the held works it selects, counted.
52type mwReadAccess struct {
53 route registry.Route
54 launch ClaimServingLaunch
55 report moves.EnvReport
56 works []string
57}
58
59func (a *mwReadAccess) HeldWork(_ context.Context, _, action string, _ hostguard.Captured) (hostwrite.Work, error) {
60 a.works = append(a.works, action)
61 return plan9D5Work{}, nil
62}
63func (a *mwReadAccess) Route(_ context.Context, host string) (*registry.Route, error) {
64 if host != a.route.Host {
65 return nil, hostbox.ErrChanged
66 }
67 rt := a.route
68 return &rt, nil
69}
70func (a *mwReadAccess) Launch(_ context.Context, host string) (ClaimServingLaunch, moves.EnvReport, error) {
71 if host != a.route.Host {
72 return ClaimServingLaunch{}, moves.EnvReport{}, hostbox.ErrChanged
73 }
74 return a.launch, a.report, nil
75}
76
77// mwArm arms a moveCfg for a box whose placement is placement over the
78// claim's engine in engineRegion, with the fleet's home binding home.
79func mwArm(t *testing.T, view func(context.Context, hostclaim.Observed) (park.View, error), engineRegion, home, placement string) (moveCfg, error) {
80 t.Helper()
81 auth := func(context.Context, hostclaim.Observed, hostclaim.Obligation) error { return nil }
82 providers := ClaimProviders{moveParks: &claimMoveParks{
83 local: claimMoveLocalAuthorizer(func(context.Context) error { return nil }, auth),
84 engine: park.Engine{Bucket: "data", Region: engineRegion, HomeRegion: home, Leases: (&renewTable{}).leases(), View: view, Authorize: auth},
85 leases: hostbox.ReplicaParkLeaseSet, check: func(context.Context) error { return nil }}}
86 c := moveCfg{OwnBucket: "data", Region: placement}
87 err := armMovesParks(t.Context(), &c, providers)
88 return c, err
89}
90
91// TestBothFlagsOnRegistersAndArms (Q1, Q15): with CARLOS_HOST_CLAIM and
92// CARLOS_MOVES_PHASE1 both on, the agent's two registrations, the parks and
93// then the wake source, open the gate: the claim's registered move source
94// is this box's moves source, and the moves arms take the claim's engine. A
95// named box arms over its own engine. A home box arms only when the claim
96// grants it the @home alias (claim Plan 13's signed fleet home
97// designation); without it the home box stays unarmed. The parks alone do
98// not open the gate, and a second registration is refused.
99func TestBothFlagsOnRegistersAndArms(t *testing.T) {
100 withMovesWakeSource(t, false)
101 ctx := t.Context()
102 env := mwEnv(true, true)
103 hib := activator.Config{Bucket: "data", Region: "eu-west-1"}
104 store := release.NewDirStore(t.TempDir())
105 roster := func(context.Context) (string, error) { return "ie", nil }
106 parks, err := registerProductionMovesParks(ctx, env, store, hib, roster, newMovesRefusals("ie-box", nil, t.Logf, time.Now))
107 if err != nil {
108 t.Fatal(err)
109 }
110 if parks.View == nil || parks.Evidence == nil || claimMoveSources.Parks == nil {
111 t.Fatalf("the parks are not registered: %+v", parks)
112 }
113 var dep *ClaimDependencyError
114 if err := movesWakeSourceRegistered(); !errors.As(err, &dep) {
115 t.Fatalf("the parks alone opened the gate: %v", err)
116 }
117 if c, err := mwArm(t, parks.View, "ie", "", "ie"); !errors.As(err, &dep) || parkHooksArmed(c) {
118 t.Fatalf("armed on the parks alone: %v", err)
119 }
120
121 if err := registerProductionMovesWakeSource(env, parks, t.TempDir()); err != nil {
122 t.Fatal(err)
123 }
124 if err := movesWakeSourceRegistered(); err != nil {
125 t.Fatalf("the gate is shut after the wake source registered: %v", err)
126 }
127 reg, ok := claimMoveSources.Source.(*movesWakeSourceRegistration)
128 if !ok {
129 t.Fatalf("the claim's move source is %T, not this box's moves source", claimMoveSources.Source)
130 }
131 if _, ok := reg.ClaimProductionMoveSources.(*movesWakeSources); !ok {
132 t.Fatalf("the registered source is %T", reg.ClaimProductionMoveSources)
133 }
134
135 // A named box arms over its own engine.
136 if c, err := mwArm(t, parks.View, "ie", "", "ie"); err != nil || !parkHooksArmed(c) || c.ParkRunHeld == nil || c.ParkLocalAuthorize == nil {
137 t.Fatalf("a named box did not arm: %v", err)
138 }
139 // A home box with the @home grant arms, and keeps the home binding.
140 c, err := mwArm(t, parks.View, "ie", "ie", "")
141 if err != nil || !parkHooksArmed(c) || c.ParkHomeRegion != "ie" {
142 t.Fatalf("a home box the claim grants @home did not arm: %v (%q)", err, c.ParkHomeRegion)
143 }
144 // Without the grant a home box stays unarmed, over its named engine or
145 // over one that names itself @home.
146 for _, engine := range []string{"ie", park.HomeRegion} {
147 if c, err := mwArm(t, parks.View, engine, "", ""); !errors.Is(err, hostbox.ErrChanged) || parkHooksArmed(c) {
148 t.Fatalf("a home box without the grant (engine %q) armed: %v", engine, err)
149 }
150 }
151
152 // One registration per process: a second is refused and the gate stays.
153 if err := registerProductionMovesWakeSource(env, parks, t.TempDir()); !errors.Is(err, hostwrite.ErrUnavailable) {
154 t.Fatalf("a second registration: %v", err)
155 }
156 if err := movesWakeSourceRegistered(); err != nil {
157 t.Fatalf("a refused second registration shut the gate: %v", err)
158 }
159}
160
161// TestEitherFlagOffRegistersNothing (ruling H1, Q1): with either switch
162// off, or both, neither registration does anything, the gate stays shut,
163// the moves arms stay unarmed, and the claim keeps its named refusals.
164func TestEitherFlagOffRegistersNothing(t *testing.T) {
165 hib := activator.Config{Bucket: "data", Region: "eu-west-1"}
166 roster := func(context.Context) (string, error) { return "ie", nil }
167 f := newVwFixture(t)
168 live := movesParkSources{View: f.view("ie"), Evidence: movesRetirementEvidence{Records: f.records(), Now: time.Now}}
169 for name, env := range map[string]func(string) string{
170 "claim off": mwEnv(false, true),
171 "phase 1 off": mwEnv(true, false),
172 "both off": mwEnv(false, false),
173 } {
174 t.Run(name, func(t *testing.T) {
175 withMovesWakeSource(t, false)
176 store := release.NewDirStore(t.TempDir())
177 parks, err := registerProductionMovesParks(t.Context(), env, store, hib, roster, newMovesRefusals("ie-box", nil, t.Logf, time.Now))
178 if err != nil || parks.View != nil || parks.Evidence != nil {
179 t.Fatalf("the parks: %+v %v", parks, err)
180 }
181 // Off, even a complete park source registers no wake source.
182 for _, p := range []movesParkSources{parks, live} {
183 if err := registerProductionMovesWakeSource(env, p, t.TempDir()); err != nil {
184 t.Fatal(err)
185 }
186 }
187 if claimMoveSources.Source != nil || claimMoveSources.Parks != nil {
188 t.Fatalf("registered: source %v parks %v", claimMoveSources.Source, claimMoveSources.Parks)
189 }
190 var dep *ClaimDependencyError
191 if err := movesWakeSourceRegistered(); !errors.As(err, &dep) || !strings.Contains(err.Error(), movesWakeSourceContract) {
192 t.Fatalf("the gate: %v", err)
193 }
194 if c, err := mwArm(t, live.View, "ie", "", "ie"); !errors.As(err, &dep) || parkHooksArmed(c) {
195 t.Fatalf("armed with a switch off: %v", err)
196 }
197 })
198 }
199}
200
201// TestTheClaimsParkViewIsTheMovesView (Q15): the wake source's View and
202// VerifyRetirement are never read by the claim, because the parks 9a
203// registered first stay in place; both delegate to the same park sources,
204// so whichever the claim reads, the answer is the moves view's (and the
205// moves evidence's). The claim's regional view, which every host's park
206// chores and the at-rest sweep read, is that view too.
207func TestTheClaimsParkViewIsTheMovesView(t *testing.T) {
208 withMovesWakeSource(t, false)
209 f := newVwFixture(t)
210 evidence := &countingEvidence{RegionalEvidence: movesRetirementEvidence{Records: f.records(), Now: time.Now}}
211 parks, err := newMovesParkSources(f.records(), f.bucket, "data", fixedRegion("ie"), evidence)
212 if err != nil {
213 t.Fatal(err)
214 }
215 if err := registerMovesParks(parks); err != nil {
216 t.Fatal(err)
217 }
218 registered := claimMoveSources.Parks
219 if err := registerProductionMovesWakeSource(mwEnv(true, true), parks, t.TempDir()); err != nil {
220 t.Fatal(err)
221 }
222 if claimMoveSources.Parks != registered {
223 t.Fatal("the wake source replaced the parks 9a registered")
224 }
225 f.marker(t, "cur", f.inc)
226 obs := vwObs(&hostclaim.Claim{Holder: f.holder("cur", "cur@ie"), Op: hostclaim.Op{Kind: hostclaim.KindServing, Exec: "box"}})
227 direct, err := movesParkView(f.records(), f.bucket, "data", "ie")(t.Context(), obs)
228 if err != nil || direct.AuthoritativeRegion != "ie" || !direct.MarkerKnown || direct.MarkerGeneration != "cur" {
229 t.Fatalf("the moves view: %+v %v", direct, err)
230 }
231 src := claimMoveSources.Source
232 for name, view := range map[string]func(context.Context, hostclaim.Observed) (park.View, error){
233 "the claim's park view": claimMoveSources.Parks.View,
234 "the wake source's View": src.View,
235 "the registered parks": parks.View,
236 "the claim's regional view": productionClaimRegional{Identity: handoverRecoveryIdentity(t, hostbox.Actor{Box: "box-1", Enrollment: "enrollment-1", Revision: 7, Binding: "7", Region: "ie"}), Parks: claimMoveSources.Parks}.View,
237 } {
238 got, err := view(t.Context(), obs)
239 if err != nil {
240 t.Fatalf("%s: %v", name, err)
241 }
242 got.GenerationOf, direct.GenerationOf = nil, nil
243 got.ArchivedIncarnations, got.RefusedLocations = nil, nil
244 if !reflect.DeepEqual(got, direct) {
245 t.Fatalf("%s is not the moves view:\n got %+v\nwant %+v", name, got, direct)
246 }
247 }
248 retired := vwObs(&hostclaim.Claim{})
249 if _, _, err := src.VerifyRetirement(t.Context(), retired, nil); evidence.calls != 1 {
250 t.Fatalf("the wake source's VerifyRetirement did not delegate to the moves evidence: %d calls (%v)", evidence.calls, err)
251 }
252 if _, _, err := claimMoveSources.Parks.Evidence.VerifyRetirement(t.Context(), retired, nil); evidence.calls != 2 {
253 t.Fatalf("the claim's evidence is not the moves evidence: %d calls (%v)", evidence.calls, err)
254 }
255}
256
257// countingEvidence counts VerifyRetirement calls.
258type countingEvidence struct {
259 hostbox.RegionalEvidence
260 calls int
261}
262
263func (e *countingEvidence) VerifyRetirement(ctx context.Context, o hostclaim.Observed, v []lease.ObjectVersion) (string, time.Time, error) {
264 e.calls++
265 return e.RegionalEvidence.VerifyRetirement(ctx, o, v)
266}
267
268// TestRegisteredSourceOpensTheMovesOwnersWithTheClaimsFiles (Task 8's
269// controller overrides): the registered source's Open returns the moves
270// owner set the claim binds (the landing, committed and recovery owners,
271// with Operations, LossRecovery and LossSources), over the claim's read
272// access, the actor's bucket and region, and Task 7b's held release adapter;
273// it opens no files provider of its own. The C8 provider the recovery
274// owners establish through is the one Task 6's registration binds for the
275// claim's serving owners: the same value. A second provider is a wiring
276// error.
277func TestRegisteredSourceOpensTheMovesOwnersWithTheClaimsFiles(t *testing.T) {
278 withMovesWakeSource(t, false)
279 ctx := t.Context()
280 f := newVwFixture(t)
281 parks, err := newMovesParkSources(f.records(), f.bucket, "data", fixedRegion("ie"), movesRetirementEvidence{Records: f.records(), Now: time.Now})
282 if err != nil {
283 t.Fatal(err)
284 }
285 if err := registerMovesParks(parks); err != nil {
286 t.Fatal(err)
287 }
288 env := mwEnv(true, true)
289 dataDir := t.TempDir()
290 if err := registerProductionMovesWakeSource(env, parks, dataDir); err != nil {
291 t.Fatal(err)
292 }
293 src := claimMoveSources.Source.(*movesWakeSourceRegistration).ClaimProductionMoveSources.(*movesWakeSources)
294 src.Getenv = env
295 src.markers = func(context.Context, string, string) (ClaimMarkerStore, error) { return plan9D5Markers{f.bucket}, nil }
296 actor := hostbox.Actor{Box: "ie-box", Region: "ie", Bucket: "data", Enrollment: "enr", Cloud: hostbox.CloudIdentity{Region: "eu-west-1"}}
297 store := release.NewDirStore(t.TempDir())
298 data := leasetest.NewVersionedBucket()
299 access := &mwReadAccess{route: registry.Route{Host: vwHost}}
300 out := ClaimOwnerBindings{Data: data}
301 // OpenClaimOwners hands Open its move read access; here the fixture's.
302 owners, files, err := claimMoveSources.Source.Open(ctx, store, data, actor, access)
303 if err != nil || files != nil {
304 t.Fatalf("open: files %v, %v", files, err)
305 }
306 if ok, err := claimMovesC1(env, owners); !ok || err != nil {
307 t.Fatalf("the owner set is not complete: %v %v", ok, err)
308 }
309 if owners.LossRecovery == nil || owners.LossSources == nil || owners.Operations == nil || owners.ProbeReady != nil {
310 t.Fatalf("owners %+v", owners)
311 }
312 if reflect.TypeOf(owners.LossRecovery) != reflect.TypeOf(hostops.MoveLossRecoveryOwner()) {
313 t.Fatalf("the loss owner is %T", owners.LossRecovery)
314 }
315 m, ok := owners.Inputs.(*movesProbeOwners)
316 if !ok {
317 t.Fatalf("the inputs are %T", owners.Inputs)
318 }
319 if !m.valid() || m.Box != "ie-box" || m.Region != "ie" || m.Bucket != "data" || m.Data != data || m.Claims != store || m.DataDir != movesDataDir(dataDir) || m.Files != nil || m.Release == nil || m.Page == nil {
320 t.Fatalf("the probe owners: %+v", m)
321 }
322 ops, ok := owners.Operations.(*movesProbeAttempts)
323 if !ok || ops.Claims != store || ops.DataDir != m.DataDir || ops.Admitted == nil || ops.RecoveryAttempt == nil {
324 t.Fatalf("the attempts: %#v", owners.Operations)
325 }
326 // The read access is the claim's: route, launch and held works.
327 if rt, err := m.Route(vwHost); err != nil || rt.Host != vwHost {
328 t.Fatalf("route %v %v", rt, err)
329 }
330 if _, err := m.Works(ctx, vwHost, movesActionHeldConfig, hostguard.Captured{}); err != nil || !reflect.DeepEqual(access.works, []string{movesActionHeldConfig}) {
331 t.Fatalf("held work %v %v", access.works, err)
332 }
333
334 // The claim's C8 provider binds the serving owners and the recovery
335 // owners alike: one value.
336 out.Moves = owners
337 provider := &movesFiles{}
338 closeFiles, err := openClaimFilesManifest(ctx, env, &fakeFilesSource{files: provider}, store, data, actor, &out, func(context.Context) error { return nil })
339 if err != nil || closeFiles == nil {
340 t.Fatalf("files: %v", err)
341 }
342 if out.Serving.Files != ClaimFilesManifest(provider) || m.Files != ClaimFilesManifest(provider) {
343 t.Fatalf("the recovery owners' files %v, the serving owners' %v", m.Files, out.Serving.Files)
344 }
345 // Another provider for the same owners is refused, never swapped in.
346 again := ClaimOwnerBindings{Moves: owners}
347 if _, err := openClaimFilesManifest(ctx, env, &fakeFilesSource{files: &movesFiles{}}, store, data, actor, &again, func(context.Context) error { return nil }); !errors.Is(err, hostwrite.ErrUnavailable) || m.Files != ClaimFilesManifest(provider) {
348 t.Fatalf("a second provider: %v, files %v", err, m.Files)
349 }
350 // Without the read access, or the secrets key, Open refuses: the wake
351 // is then refused by name, never half-wired.
352 if _, _, err := src.Open(ctx, store, data, actor, nil); !errors.Is(err, hostwrite.ErrUnavailable) {
353 t.Fatalf("no read access: %v", err)
354 }
355 src.Getenv = mwEnv(true, true)
356 noKey := *src
357 noKey.Getenv = func(k string) string {
358 if k == "CARLOS_SECRETS_KEY" {
359 return ""
360 }
361 return env(k)
362 }
363 if _, _, err := noKey.Open(ctx, store, data, actor, access); err == nil {
364 t.Fatal("opened without the env report key")
365 }
366 if _, _, err := src.Open(ctx, store, data, hostbox.Actor{Box: "ie-box"}, access); !errors.Is(err, hostwrite.ErrUnavailable) {
367 t.Fatalf("a bare actor: %v", err)
368 }
369}
370
371// mwDataSource is a moves source that records the bucket the claim hands
372// its Open.
373type mwDataSource struct {
374 parkRegistrationWake
375 data lease.VersionedStore
376}
377
378func (s *mwDataSource) Open(_ context.Context, _ release.Store, data lease.VersionedStore, _ hostbox.Actor, _ ClaimMoveReadAccess) (ClaimMovesOwners, ClaimFilesManifest, error) {
379 s.data = data
380 return ClaimMovesOwners{}, nil, nil
381}
382
383// mwBucket is the claim's data bucket, recording each listing's context.
384// When started is set, a listing closes it once it has reached the bucket
385// and then holds until its context ends. The test sets started before the
386// listing's goroutine starts, and reads the counters only after that
387// goroutine's result has been received, so neither side races.
388type mwBucket struct {
389 *leasetest.VersionedBucket
390 lists int
391 deadlines []time.Time
392 started chan struct{}
393}
394
395func (b *mwBucket) ListVersions(ctx context.Context, bucket, prefix string) ([]lease.ObjectVersion, error) {
396 b.lists++
397 d, _ := ctx.Deadline()
398 b.deadlines = append(b.deadlines, d)
399 if b.started != nil {
400 close(b.started)
401 <-ctx.Done()
402 return nil, ctx.Err()
403 }
404 return b.VersionedBucket.ListVersions(ctx, bucket, prefix)
405}
406
407// TestMovesOwnersReadTheBucketThroughTheRequestStore (Task 8 fix round 1,
408// Astra and platform-1e): the moves owners get the claim's data bucket as
409// the serving owners do, through hostbox.RequestStore: every request is
410// bounded by the claim's request limit, the box's identity is checked
411// before each one (a refused identity issues no later bucket call), and
412// the operation's own context still cancels a request in flight.
413func TestMovesOwnersReadTheBucketThroughTheRequestStore(t *testing.T) {
414 inner := &mwBucket{VersionedBucket: leasetest.NewVersionedBucket()}
415 inner.AddBucket("data")
416 var refuse error
417 checks := 0
418 check := func(context.Context) error { checks++; return refuse }
419 src := &mwDataSource{}
420 out := ClaimOwnerBindings{Data: inner}
421 openProductionClaimMoveWake(t.Context(), release.NewDirStore(t.TempDir()), hostbox.Actor{Box: "ie-box"}, src, &out, check)
422 if out.wakeUnavailable != nil || src.data == nil {
423 t.Fatalf("open: %v", out.wakeUnavailable)
424 }
425 if _, ok := src.data.(hostbox.RequestStore); !ok {
426 t.Fatalf("the moves owners' bucket is %T, not the claim's request store", src.data)
427 }
428 before := time.Now()
429 if _, err := src.data.ListVersions(t.Context(), "data", "instances/"); err != nil {
430 t.Fatal(err)
431 }
432 if inner.lists != 1 || checks != 1 || inner.deadlines[0].IsZero() || inner.deadlines[0].After(time.Now().Add(hostbox.RequestLimit)) || inner.deadlines[0].Before(before.Add(hostbox.RequestLimit-time.Minute)) {
433 t.Fatalf("the request's deadline %v (lists %d, checks %d), want the request limit", inner.deadlines, inner.lists, checks)
434 }
435 refuse = hostbox.ErrChanged
436 if _, err := src.data.ListVersions(t.Context(), "data", "instances/"); !errors.Is(err, hostbox.ErrChanged) || inner.lists != 1 {
437 t.Fatalf("a refused identity: %v, %d bucket calls", err, inner.lists)
438 }
439 if _, err := src.data.GetVersion(t.Context(), "data", "k", "v"); !errors.Is(err, hostbox.ErrChanged) {
440 t.Fatalf("a refused identity's read: %v", err)
441 }
442 refuse = nil
443 // A request in flight: wait (bounded) until it has reached the
444 // bucket, or has ended early, then cancel the operation.
445 started := make(chan struct{})
446 inner.started = started
447 ctx, cancel := context.WithCancel(t.Context())
448 defer cancel()
449 done := make(chan error, 1)
450 go func() { _, err := src.data.ListVersions(ctx, "data", "instances/"); done <- err }()
451 select {
452 case <-started:
453 case err := <-done:
454 t.Fatalf("the request ended before it reached the bucket: %v", err)
455 case <-time.After(10 * time.Second):
456 t.Fatal("the request never reached the bucket")
457 }
458 cancel()
459 select {
460 case err := <-done:
461 if !errors.Is(err, context.Canceled) {
462 t.Fatalf("a cancelled operation's request: %v", err)
463 }
464 case <-time.After(10 * time.Second):
465 t.Fatal("the operation's cancellation did not reach the request")
466 }
467 if inner.lists != 2 {
468 t.Fatalf("%d bucket calls, want the first and the cancelled one", inner.lists)
469 }
470}
471
472// TestMovesSourceOpenRefusalIsLoggedOnce (Task 8 fix round 1,
473// platform-1e): a moves source that does not open keeps every wake on the
474// box refused (fail closed), and the cause is logged once per agent start,
475// so an operator can see why the box serves nothing.
476func TestMovesSourceOpenRefusalIsLoggedOnce(t *testing.T) {
477 var lines []string
478 oldLogf, oldOnce := claimMoveWakeLogf, claimMoveWakeLogOnce
479 claimMoveWakeLogf = func(format string, args ...any) { lines = append(lines, fmt.Sprintf(format, args...)) }
480 claimMoveWakeLogOnce = new(sync.Once)
481 t.Cleanup(func() { claimMoveWakeLogf, claimMoveWakeLogOnce = oldLogf, oldOnce })
482 cause := errors.New("moves: secrets key: not a 32-byte key")
483 for i := 0; i < 3; i++ {
484 out := ClaimOwnerBindings{}
485 openProductionClaimMoveWake(t.Context(), nil, hostbox.Actor{}, &parkRegistrationWake{failure: cause}, &out, func(context.Context) error { return nil })
486 if !errors.Is(out.wakeUnavailable, cause) {
487 t.Fatalf("open %d: the refusal is not kept: %v", i, out.wakeUnavailable)
488 }
489 }
490 if len(lines) != 1 || !strings.Contains(lines[0], cause.Error()) || !strings.Contains(lines[0], "every wake") {
491 t.Fatalf("logged %q, want the cause once", lines)
492 }
493}
494
495// TestBindClaimMovesFilesRefusesASecondProvider (Task 8 fix round 1,
496// platform-1e): the claim's C8 provider binds the moves owners once; the
497// same provider again changes nothing, and another is refused and never
498// swapped in. A binding without moves owners, or without a provider, does
499// nothing.
500func TestBindClaimMovesFilesRefusesASecondProvider(t *testing.T) {
501 m := &movesProbeOwners{}
502 first, second := &movesFiles{Box: "first"}, &movesFiles{Box: "second"}
503 out := ClaimOwnerBindings{Moves: m.MovesOwners(&movesProbeAttempts{})}
504 out.Serving.Files = first
505 if err := bindClaimMovesFiles(&out); err != nil || m.Files != ClaimFilesManifest(first) {
506 t.Fatalf("the first provider: %v, files %v", err, m.Files)
507 }
508 if err := bindClaimMovesFiles(&out); err != nil || m.Files != ClaimFilesManifest(first) {
509 t.Fatalf("the same provider again: %v", err)
510 }
511 out.Serving.Files = second
512 if err := bindClaimMovesFiles(&out); !errors.Is(err, hostwrite.ErrUnavailable) || m.Files != ClaimFilesManifest(first) {
513 t.Fatalf("a second provider: %v, files %v", err, m.Files)
514 }
515 if err := bindClaimMovesFiles(&ClaimOwnerBindings{Serving: ClaimServingSources{Files: second}}); err != nil {
516 t.Fatalf("no moves owners: %v", err)
517 }
518 if err := bindClaimMovesFiles(&ClaimOwnerBindings{Moves: out.Moves}); err != nil || m.Files != ClaimFilesManifest(first) {
519 t.Fatalf("no provider: %v", err)
520 }
521}
522