package hostbox import ( "context" "errors" "github.com/carlosframework/platform/internal/genselect" "github.com/carlosframework/platform/internal/hostclaim" "github.com/carlosframework/platform/internal/hostwrite" "github.com/carlosframework/platform/internal/release" "testing" ) func TestWakePhaseK6(t *testing.T) { for _, tc := range []struct { kind, phase string probe, want bool }{ {"serving", "", false, true}, {"serving", "", true, false}, {"landing", "committed", false, true}, {"landing", "restored", false, false}, {"landing", "probing", true, true}, {"reverting", "probed", false, true}, {"restoring", "opened", false, true}, {"moving", "flipped", false, false}, } { c := &hostclaim.Claim{Holder: &hostclaim.Holder{}, Op: hostclaim.Op{Kind: tc.kind, Phase: tc.phase, ID: "op", Exec: "box"}} if tc.kind == "serving" { c.Op.ID = "" } if got := WakePhase(c, "box", tc.probe) == nil; got != tc.want { t.Fatalf("%+v: %v", tc, got) } if WakePhase(c, "other", tc.probe) == nil { t.Fatal("foreign executor") } } } func TestHeldTimerPolicyOnlyOwningOperation(t *testing.T) { c := &hostclaim.Claim{Holder: &hostclaim.Holder{}, Op: hostclaim.Op{Kind: "landing", ID: "landing", Exec: "box"}} p := Policy{Held: func(context.Context, string) (string, error) { return "landing", nil }, Moves: func(context.Context, *hostclaim.Claim, Selection, hostwrite.Identity) error { return nil }} s := Selection{Host: "x.example", Box: "box", Op: "landing", Action: "held-timers"} if err := p.Check(context.Background(), c, s, hostwrite.Identity{}); err != nil { t.Fatal(err) } for _, action := range []string{"timer-delivery", "config", "release"} { s.Action = action if p.Check(context.Background(), c, s, hostwrite.Identity{}) == nil { t.Fatal(action) } } s.Action = "held-timers" s.Op = "previous" if p.Check(context.Background(), c, s, hostwrite.Identity{}) == nil { t.Fatal("stale op") } } func TestWriterColdOnlyEvenWithCertificate(t *testing.T) { evidenceCalls := 0 w := Writer{Claims: release.NewDirStore(t.TempDir()), Box: "box", Region: "ie", AdmissionEvidence: func(context.Context, hostclaim.Observed, hostclaim.Admission, *genselect.Plan) error { evidenceCalls++ return nil }} obs := hostclaim.Observed{Host: "x.example", ETag: "one", Claim: &hostclaim.Claim{Holder: &hostclaim.Holder{}}} for _, in := range []hostclaim.Admission{{Box: "box", Region: "ie", Mode: "warm", PredecessorCertified: true}, {Box: "box", Region: "ie", Mode: "cold", NewGeneration: "next", PredecessorCertified: true}} { if _, err := w.Admit(context.Background(), obs, in, nil); !errors.Is(err, ErrUnsupported) { t.Fatal(err) } } if evidenceCalls != 0 { t.Fatal("certificate provider consulted", evidenceCalls) } } func TestPolicyOrdinaryWakeAfterReleaseHold(t *testing.T) { for _, phase := range []struct{ kind, phase string }{{"landing", "committed"}, {"reverting", "probed"}, {"reverting", "opened"}, {"restoring", "probed"}, {"restoring", "opened"}} { c := &hostclaim.Claim{Holder: &hostclaim.Holder{}, Op: hostclaim.Op{Kind: phase.kind, Phase: phase.phase, ID: "op", Exec: "box"}} p := Policy{Held: func(context.Context, string) (string, error) { return "", nil }, Moves: func(context.Context, *hostclaim.Claim, Selection, hostwrite.Identity) error { return nil }} s := Selection{Host: "x.example", Box: "box", Op: "op"} for _, action := range []string{"restore", "spawn", "seed", "writer", "install"} { s.Action = action if err := p.Check(t.Context(), c, s, hostwrite.Identity{}); err != nil { t.Fatalf("%+v %s: %v", phase, action, err) } } for _, action := range []string{"held-config", "held-release", "held-timers", "config", "timers"} { s.Action = action if p.Check(t.Context(), c, s, hostwrite.Identity{}) == nil { t.Fatalf("unheld %s accepted", action) } } s.Action = "spawn" c.Op.Phase = "probing" if p.Check(t.Context(), c, s, hostwrite.Identity{}) == nil { t.Fatal("ordinary wake in probing") } c.Op.Phase = phase.phase s.Op = "other" if p.Check(t.Context(), c, s, hostwrite.Identity{}) == nil { t.Fatal("stale op") } } } // A rollback keeps the move id as the claim's op id (hostclaim.RollBack) and // hands the registry row to the rollback id (ruling J3), so under restoring, // in the source's recovery phases, the hold Policy.Check expects is the // claim's intent.rollback, a valid rollback id (Plan 4 Task 3b fix round 1). // Landing, moving and reverting keep the selection's op. A restoring op with // no valid rollback id, or outside those phases, matches no hold and never // falls back to the op. func TestPolicyHoldOwnerUnderRollback(t *testing.T) { const mv, rb, otherRB = "mv_move", "rb_abcdefghijklmnopqrstuvwxyz", "rb_zzzzzzzzzzzzzzzzzzzzzzzzzz" check := func(kind, phase, hold string, intent map[string]string, action string) error { c := &hostclaim.Claim{Holder: &hostclaim.Holder{}, Op: hostclaim.Op{Kind: kind, Phase: phase, ID: mv, Exec: "box", Intent: intent}} p := Policy{Held: func(context.Context, string) (string, error) { return hold, nil }, Moves: func(context.Context, *hostclaim.Claim, Selection, hostwrite.Identity) error { return nil }} return p.Check(t.Context(), c, Selection{Host: "x.example", Box: "box", Op: mv, Action: action}, hostwrite.Identity{}) } unheldWake := map[string]bool{"restore": true, "spawn": true, "seed": true, "writer": true, "install": true} restoring := map[string]string{"rollback": rb, "to": "uk"} actions := []string{"held-config", "held-release", "held-timers", "restore", "spawn", "seed", "writer", "install"} sourcePhases := []string{hostclaim.PhaseClaimed, hostclaim.PhaseProbing, hostclaim.PhaseProbed, hostclaim.PhaseOpened} for _, action := range actions { for _, phase := range sourcePhases { if err := check(hostclaim.KindRestoring, phase, rb, restoring, action); err != nil { t.Errorf("restoring/%s, held by its rollback, %s: %v", phase, action, err) } for name, hold := range map[string]string{"the move id": mv, "another rollback": otherRB, "an unrelated op": "op"} { if err := check(hostclaim.KindRestoring, phase, hold, restoring, action); !errors.Is(err, ErrChanged) { t.Errorf("restoring/%s, held by %s, %s: %v", phase, name, action, err) } } } for name, intent := range map[string]map[string]string{ "no intent": {}, "no rollback": {"to": "uk"}, "an empty rollback": {"rollback": ""}, "the move id as rollback": {"rollback": mv}, "a short rollback id": {"rollback": "rb_abc"}, "an upper-case rollback": {"rollback": "RB_ABCDEFGHIJKLMNOPQRSTUVWXYZ"}, "a non-base32 rollback id": {"rollback": "rb_abcdefghijklmnopqrstuvwx01"}, "a padded rollback id": {"rollback": rb + " "}, } { for _, hold := range []string{mv, rb, intent["rollback"], ""} { if hold == "" && unheldWake[action] { continue // unheld: WakePhase decides, unchanged } if err := check(hostclaim.KindRestoring, hostclaim.PhaseClaimed, hold, intent, action); !errors.Is(err, ErrChanged) { t.Errorf("restoring with %s, held by %q, %s: %v", name, hold, action, err) } } } for _, phase := range []string{hostclaim.PhaseReleasing, hostclaim.PhaseReleased, "", hostclaim.PhaseSealed, hostclaim.PhaseCommitted} { for _, hold := range []string{rb, mv} { if err := check(hostclaim.KindRestoring, phase, hold, restoring, action); !errors.Is(err, ErrChanged) { t.Errorf("restoring outside the source's recovery phases (%q), held by %q, %s: %v", phase, hold, action, err) } } } for _, kind := range []string{hostclaim.KindLanding, hostclaim.KindMoving, hostclaim.KindReverting} { if err := check(kind, hostclaim.PhaseProbing, rb, restoring, action); !errors.Is(err, ErrChanged) { t.Errorf("%s held by a rollback id, %s: %v", kind, action, err) } if err := check(kind, hostclaim.PhaseProbing, mv, restoring, action); err != nil { t.Errorf("%s held by its op, %s: %v", kind, action, err) } } } // The rollback id comes from the claim alone: a selection naming the // rollback id as its op does not match the claim's op. c := &hostclaim.Claim{Holder: &hostclaim.Holder{}, Op: hostclaim.Op{Kind: hostclaim.KindRestoring, Phase: hostclaim.PhaseClaimed, ID: mv, Exec: "box", Intent: restoring}} p := Policy{Held: func(context.Context, string) (string, error) { return rb, nil }, Moves: func(context.Context, *hostclaim.Claim, Selection, hostwrite.Identity) error { return nil }} if err := p.Check(t.Context(), c, Selection{Host: "x.example", Box: "box", Op: rb, Action: "held-timers"}, hostwrite.Identity{}); !errors.Is(err, ErrChanged) { t.Fatalf("a selection naming the rollback: %v", err) } // The source's recovery phases are hostclaim's restoring order from // claimed on. order := hostclaim.RecoveryPhaseOrder(hostclaim.KindRestoring) if len(order) != 6 || order[2] != hostclaim.PhaseClaimed || len(restoringHoldPhases) != 4 { t.Fatalf("restoring order %v, hold phases %v", order, restoringHoldPhases) } for _, phase := range order[2:] { if !restoringHoldPhases[phase] { t.Fatalf("%s is not a hold phase", phase) } } }