carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package hostbox
2
3import (
4 "context"
5 "errors"
6 "github.com/carlosframework/platform/internal/genselect"
7 "github.com/carlosframework/platform/internal/hostclaim"
8 "github.com/carlosframework/platform/internal/hostwrite"
9 "github.com/carlosframework/platform/internal/release"
10 "testing"
11)
12
13func TestWakePhaseK6(t *testing.T) {
14 for _, tc := range []struct {
15 kind, phase string
16 probe, want bool
17 }{
18 {"serving", "", false, true}, {"serving", "", true, false},
19 {"landing", "committed", false, true}, {"landing", "restored", false, false},
20 {"landing", "probing", true, true}, {"reverting", "probed", false, true},
21 {"restoring", "opened", false, true}, {"moving", "flipped", false, false},
22 } {
23 c := &hostclaim.Claim{Holder: &hostclaim.Holder{}, Op: hostclaim.Op{Kind: tc.kind, Phase: tc.phase, ID: "op", Exec: "box"}}
24 if tc.kind == "serving" {
25 c.Op.ID = ""
26 }
27 if got := WakePhase(c, "box", tc.probe) == nil; got != tc.want {
28 t.Fatalf("%+v: %v", tc, got)
29 }
30 if WakePhase(c, "other", tc.probe) == nil {
31 t.Fatal("foreign executor")
32 }
33 }
34}
35func TestHeldTimerPolicyOnlyOwningOperation(t *testing.T) {
36 c := &hostclaim.Claim{Holder: &hostclaim.Holder{}, Op: hostclaim.Op{Kind: "landing", ID: "landing", Exec: "box"}}
37 p := Policy{Held: func(context.Context, string) (string, error) { return "landing", nil }, Moves: func(context.Context, *hostclaim.Claim, Selection, hostwrite.Identity) error { return nil }}
38 s := Selection{Host: "x.example", Box: "box", Op: "landing", Action: "held-timers"}
39 if err := p.Check(context.Background(), c, s, hostwrite.Identity{}); err != nil {
40 t.Fatal(err)
41 }
42 for _, action := range []string{"timer-delivery", "config", "release"} {
43 s.Action = action
44 if p.Check(context.Background(), c, s, hostwrite.Identity{}) == nil {
45 t.Fatal(action)
46 }
47 }
48 s.Action = "held-timers"
49 s.Op = "previous"
50 if p.Check(context.Background(), c, s, hostwrite.Identity{}) == nil {
51 t.Fatal("stale op")
52 }
53}
54func TestWriterColdOnlyEvenWithCertificate(t *testing.T) {
55 evidenceCalls := 0
56 w := Writer{Claims: release.NewDirStore(t.TempDir()), Box: "box", Region: "ie", AdmissionEvidence: func(context.Context, hostclaim.Observed, hostclaim.Admission, *genselect.Plan) error {
57 evidenceCalls++
58 return nil
59 }}
60 obs := hostclaim.Observed{Host: "x.example", ETag: "one", Claim: &hostclaim.Claim{Holder: &hostclaim.Holder{}}}
61 for _, in := range []hostclaim.Admission{{Box: "box", Region: "ie", Mode: "warm", PredecessorCertified: true}, {Box: "box", Region: "ie", Mode: "cold", NewGeneration: "next", PredecessorCertified: true}} {
62 if _, err := w.Admit(context.Background(), obs, in, nil); !errors.Is(err, ErrUnsupported) {
63 t.Fatal(err)
64 }
65 }
66 if evidenceCalls != 0 {
67 t.Fatal("certificate provider consulted", evidenceCalls)
68 }
69}
70
71func TestPolicyOrdinaryWakeAfterReleaseHold(t *testing.T) {
72 for _, phase := range []struct{ kind, phase string }{{"landing", "committed"}, {"reverting", "probed"}, {"reverting", "opened"}, {"restoring", "probed"}, {"restoring", "opened"}} {
73 c := &hostclaim.Claim{Holder: &hostclaim.Holder{}, Op: hostclaim.Op{Kind: phase.kind, Phase: phase.phase, ID: "op", Exec: "box"}}
74 p := Policy{Held: func(context.Context, string) (string, error) { return "", nil }, Moves: func(context.Context, *hostclaim.Claim, Selection, hostwrite.Identity) error { return nil }}
75 s := Selection{Host: "x.example", Box: "box", Op: "op"}
76 for _, action := range []string{"restore", "spawn", "seed", "writer", "install"} {
77 s.Action = action
78 if err := p.Check(t.Context(), c, s, hostwrite.Identity{}); err != nil {
79 t.Fatalf("%+v %s: %v", phase, action, err)
80 }
81 }
82 for _, action := range []string{"held-config", "held-release", "held-timers", "config", "timers"} {
83 s.Action = action
84 if p.Check(t.Context(), c, s, hostwrite.Identity{}) == nil {
85 t.Fatalf("unheld %s accepted", action)
86 }
87 }
88 s.Action = "spawn"
89 c.Op.Phase = "probing"
90 if p.Check(t.Context(), c, s, hostwrite.Identity{}) == nil {
91 t.Fatal("ordinary wake in probing")
92 }
93 c.Op.Phase = phase.phase
94 s.Op = "other"
95 if p.Check(t.Context(), c, s, hostwrite.Identity{}) == nil {
96 t.Fatal("stale op")
97 }
98 }
99}
100
101// A rollback keeps the move id as the claim's op id (hostclaim.RollBack) and
102// hands the registry row to the rollback id (ruling J3), so under restoring,
103// in the source's recovery phases, the hold Policy.Check expects is the
104// claim's intent.rollback, a valid rollback id (Plan 4 Task 3b fix round 1).
105// Landing, moving and reverting keep the selection's op. A restoring op with
106// no valid rollback id, or outside those phases, matches no hold and never
107// falls back to the op.
108func TestPolicyHoldOwnerUnderRollback(t *testing.T) {
109 const mv, rb, otherRB = "mv_move", "rb_abcdefghijklmnopqrstuvwxyz", "rb_zzzzzzzzzzzzzzzzzzzzzzzzzz"
110 check := func(kind, phase, hold string, intent map[string]string, action string) error {
111 c := &hostclaim.Claim{Holder: &hostclaim.Holder{}, Op: hostclaim.Op{Kind: kind, Phase: phase, ID: mv, Exec: "box", Intent: intent}}
112 p := Policy{Held: func(context.Context, string) (string, error) { return hold, nil }, Moves: func(context.Context, *hostclaim.Claim, Selection, hostwrite.Identity) error { return nil }}
113 return p.Check(t.Context(), c, Selection{Host: "x.example", Box: "box", Op: mv, Action: action}, hostwrite.Identity{})
114 }
115 unheldWake := map[string]bool{"restore": true, "spawn": true, "seed": true, "writer": true, "install": true}
116 restoring := map[string]string{"rollback": rb, "to": "uk"}
117 actions := []string{"held-config", "held-release", "held-timers", "restore", "spawn", "seed", "writer", "install"}
118 sourcePhases := []string{hostclaim.PhaseClaimed, hostclaim.PhaseProbing, hostclaim.PhaseProbed, hostclaim.PhaseOpened}
119 for _, action := range actions {
120 for _, phase := range sourcePhases {
121 if err := check(hostclaim.KindRestoring, phase, rb, restoring, action); err != nil {
122 t.Errorf("restoring/%s, held by its rollback, %s: %v", phase, action, err)
123 }
124 for name, hold := range map[string]string{"the move id": mv, "another rollback": otherRB, "an unrelated op": "op"} {
125 if err := check(hostclaim.KindRestoring, phase, hold, restoring, action); !errors.Is(err, ErrChanged) {
126 t.Errorf("restoring/%s, held by %s, %s: %v", phase, name, action, err)
127 }
128 }
129 }
130 for name, intent := range map[string]map[string]string{
131 "no intent": {},
132 "no rollback": {"to": "uk"},
133 "an empty rollback": {"rollback": ""},
134 "the move id as rollback": {"rollback": mv},
135 "a short rollback id": {"rollback": "rb_abc"},
136 "an upper-case rollback": {"rollback": "RB_ABCDEFGHIJKLMNOPQRSTUVWXYZ"},
137 "a non-base32 rollback id": {"rollback": "rb_abcdefghijklmnopqrstuvwx01"},
138 "a padded rollback id": {"rollback": rb + " "},
139 } {
140 for _, hold := range []string{mv, rb, intent["rollback"], ""} {
141 if hold == "" && unheldWake[action] {
142 continue // unheld: WakePhase decides, unchanged
143 }
144 if err := check(hostclaim.KindRestoring, hostclaim.PhaseClaimed, hold, intent, action); !errors.Is(err, ErrChanged) {
145 t.Errorf("restoring with %s, held by %q, %s: %v", name, hold, action, err)
146 }
147 }
148 }
149 for _, phase := range []string{hostclaim.PhaseReleasing, hostclaim.PhaseReleased, "", hostclaim.PhaseSealed, hostclaim.PhaseCommitted} {
150 for _, hold := range []string{rb, mv} {
151 if err := check(hostclaim.KindRestoring, phase, hold, restoring, action); !errors.Is(err, ErrChanged) {
152 t.Errorf("restoring outside the source's recovery phases (%q), held by %q, %s: %v", phase, hold, action, err)
153 }
154 }
155 }
156 for _, kind := range []string{hostclaim.KindLanding, hostclaim.KindMoving, hostclaim.KindReverting} {
157 if err := check(kind, hostclaim.PhaseProbing, rb, restoring, action); !errors.Is(err, ErrChanged) {
158 t.Errorf("%s held by a rollback id, %s: %v", kind, action, err)
159 }
160 if err := check(kind, hostclaim.PhaseProbing, mv, restoring, action); err != nil {
161 t.Errorf("%s held by its op, %s: %v", kind, action, err)
162 }
163 }
164 }
165 // The rollback id comes from the claim alone: a selection naming the
166 // rollback id as its op does not match the claim's op.
167 c := &hostclaim.Claim{Holder: &hostclaim.Holder{}, Op: hostclaim.Op{Kind: hostclaim.KindRestoring, Phase: hostclaim.PhaseClaimed, ID: mv, Exec: "box", Intent: restoring}}
168 p := Policy{Held: func(context.Context, string) (string, error) { return rb, nil }, Moves: func(context.Context, *hostclaim.Claim, Selection, hostwrite.Identity) error { return nil }}
169 if err := p.Check(t.Context(), c, Selection{Host: "x.example", Box: "box", Op: rb, Action: "held-timers"}, hostwrite.Identity{}); !errors.Is(err, ErrChanged) {
170 t.Fatalf("a selection naming the rollback: %v", err)
171 }
172 // The source's recovery phases are hostclaim's restoring order from
173 // claimed on.
174 order := hostclaim.RecoveryPhaseOrder(hostclaim.KindRestoring)
175 if len(order) != 6 || order[2] != hostclaim.PhaseClaimed || len(restoringHoldPhases) != 4 {
176 t.Fatalf("restoring order %v, hold phases %v", order, restoringHoldPhases)
177 }
178 for _, phase := range order[2:] {
179 if !restoringHoldPhases[phase] {
180 t.Fatalf("%s is not a hold phase", phase)
181 }
182 }
183}
184