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 "testing"
7
8 "github.com/carlosframework/platform/internal/hostclaim"
9 "github.com/carlosframework/platform/internal/hostwrite"
10)
11
12func plan11Relocating(phase string) *hostclaim.Claim {
13 exec := "target"
14 switch phase {
15 case "starting", "releasing":
16 exec = "source"
17 case "pinning":
18 exec = "none"
19 }
20 return &hostclaim.Claim{Holder: &hostclaim.Holder{Session: 7, WriterBox: "target"},
21 Op: hostclaim.Op{Kind: hostclaim.KindRelocating, ID: "r1", Phase: phase, Exec: exec, Participants: []string{"source", "target"},
22 Intent: map[string]string{"region": "uk", "source_box": "source", "target_box": "target"}}}
23}
24
25// WakePhase admits a relocation's probe at probing on op.exec only. No
26// ordinary relocating wake is admitted at any phase: the source's ordinary
27// wake in starting and releasing is not part of the accepted R1.
28func TestPlan11WakePhaseAdmitsOnlyTheRelocationProbe(t *testing.T) {
29 for _, tc := range []struct {
30 phase, box string
31 probe, want bool
32 }{
33 {"probing", "target", true, true},
34 {"probing", "target", false, false},
35 {"probing", "source", true, false},
36 {"starting", "source", false, false},
37 {"releasing", "source", false, false},
38 {"starting", "source", true, false},
39 {"installed", "target", true, false},
40 {"installing", "target", true, false},
41 {"probe-failed", "target", true, false},
42 {"serving", "target", false, false},
43 {"serving", "target", true, false},
44 } {
45 if got := WakePhase(plan11Relocating(tc.phase), tc.box, tc.probe) == nil; got != tc.want {
46 t.Errorf("%+v: admitted=%v", tc, got)
47 }
48 }
49 c := plan11Relocating("probing")
50 c.Lost = []string{"target"}
51 if WakePhase(c, "target", true) == nil {
52 t.Fatal("a lost target probes")
53 }
54 c = plan11Relocating("probing")
55 c.Op.Journal = map[string]string{"stuck": "deadline"}
56 if WakePhase(c, "target", true) == nil {
57 t.Fatal("a stuck relocation probes")
58 }
59}
60
61type plan11Owner struct{ calls []string }
62
63func (o *plan11Owner) check(name string, err error) func(context.Context, *hostclaim.Claim, Selection, hostwrite.Identity) error {
64 return func(_ context.Context, _ *hostclaim.Claim, s Selection, _ hostwrite.Identity) error {
65 o.calls = append(o.calls, name+":"+s.Action)
66 return err
67 }
68}
69
70func plan11Policy(o *plan11Owner, hold string) Policy {
71 return Policy{Held: func(context.Context, string) (string, error) { return hold, nil },
72 Moves: o.check("moves", nil), Relocation: o.check("relocation", nil)}
73}
74
75// Policy.Check under relocating: the claim's own gate runs first and only
76// the "probe" action of op.exec at probing (or its session carried into
77// serving) reaches the typed relocation owner. Every other action, a stale
78// op, a foreign hold, and a nil owner refuse without consulting it. The
79// moves owner never sees a relocating claim.
80func TestPlan11PolicyGivesProbeAuthorityOnlyToTheRelocationProbe(t *testing.T) {
81 ctx := context.Background()
82 o := &plan11Owner{}
83 p := plan11Policy(o, "")
84 probe := Selection{Host: "x.example", Box: "target", Op: "r1", Action: "probe", Phase: "probing", Session: 7}
85 if err := p.Check(ctx, plan11Relocating("probing"), probe, hostwrite.Identity{}); err != nil {
86 t.Fatal(err)
87 }
88 if len(o.calls) != 1 || o.calls[0] != "relocation:probe" {
89 t.Fatalf("owner calls %v", o.calls)
90 }
91 // The admitted probe session carried into relocating/serving.
92 o.calls = nil
93 if err := p.Check(ctx, plan11Relocating("serving"), probe, hostwrite.Identity{}); err != nil {
94 t.Fatalf("carried session: %v", err)
95 }
96 // A probe hold by this op is the op's own; another op's hold refuses.
97 if err := plan11Policy(o, "r1").Check(ctx, plan11Relocating("probing"), probe, hostwrite.Identity{}); err != nil {
98 t.Fatalf("own hold: %v", err)
99 }
100 o.calls = nil
101 refused := map[string]struct {
102 p Policy
103 c *hostclaim.Claim
104 s Selection
105 }{
106 "spawn": {plan11Policy(o, ""), plan11Relocating("probing"), func() Selection { s := probe; s.Action = "spawn"; return s }()},
107 "seed": {plan11Policy(o, ""), plan11Relocating("probing"), func() Selection { s := probe; s.Action = "seed"; return s }()},
108 "held-config": {plan11Policy(o, "r1"), plan11Relocating("probing"), func() Selection { s := probe; s.Action = "held-config"; return s }()},
109 "install": {plan11Policy(o, ""), plan11Relocating("installing"), func() Selection { s := probe; s.Action = "install"; return s }()},
110 "reconcile": {plan11Policy(o, ""), plan11Relocating("probing"), func() Selection { s := probe; s.Action = "reconcile"; return s }()},
111 "probe installed": {plan11Policy(o, ""), plan11Relocating("installed"), probe},
112 "probe probe-failed": {plan11Policy(o, ""), plan11Relocating("probe-failed"), probe},
113 "source probe": {plan11Policy(o, ""), plan11Relocating("starting"), func() Selection { s := probe; s.Box = "source"; return s }()},
114 "source spawn": {plan11Policy(o, ""), plan11Relocating("releasing"), func() Selection { s := probe; s.Box, s.Action = "source", "spawn"; return s }()},
115 "stale op": {plan11Policy(o, ""), plan11Relocating("probing"), func() Selection { s := probe; s.Op = "r0"; return s }()},
116 "foreign hold": {plan11Policy(o, "m9"), plan11Relocating("probing"), probe},
117 "serving, selected at serving": {plan11Policy(o, ""), plan11Relocating("serving"), func() Selection { s := probe; s.Phase = "serving"; return s }()},
118 "serving, other writer": {plan11Policy(o, ""), func() *hostclaim.Claim { c := plan11Relocating("serving"); c.Holder.WriterBox = "source"; return c }(), probe},
119 "no owner": {Policy{Held: func(context.Context, string) (string, error) { return "", nil }, Moves: o.check("moves", nil)}, plan11Relocating("probing"), probe},
120 }
121 for name, tc := range refused {
122 if err := tc.p.Check(ctx, tc.c, tc.s, hostwrite.Identity{}); err == nil {
123 t.Errorf("%s: admitted", name)
124 }
125 }
126 if len(o.calls) != 0 {
127 t.Fatalf("owners consulted on refused selections: %v", o.calls)
128 }
129 // The owner can refuse what the claim's gate allows.
130 deny := errors.New("region HA refuses")
131 p.Relocation = o.check("relocation", deny)
132 if err := p.Check(ctx, plan11Relocating("probing"), probe, hostwrite.Identity{}); !errors.Is(err, deny) {
133 t.Fatalf("owner refusal: %v", err)
134 }
135 // "probe" is never an action of any other kind.
136 landing := &hostclaim.Claim{Holder: &hostclaim.Holder{}, Op: hostclaim.Op{Kind: hostclaim.KindLanding, ID: "m1", Phase: "probing", Exec: "target"}}
137 o.calls = nil
138 if err := plan11Policy(o, "m1").Check(ctx, landing, Selection{Host: "x.example", Box: "target", Op: "m1", Action: "probe"}, hostwrite.Identity{}); err == nil || len(o.calls) != 0 {
139 t.Fatalf("landing probe action: %v %v", err, o.calls)
140 }
141}
142
143// R8 as accepted: an append-only policy check. Refusals run only after the
144// whole base check passed, so they can add a refusal and never remove one;
145// WithRefusals copies, so an earlier Policy value is unchanged.
146func TestPlan11PolicyRefusalsAreAppendOnly(t *testing.T) {
147 ctx := context.Background()
148 serving := &hostclaim.Claim{Holder: &hostclaim.Holder{}, Op: hostclaim.Op{Kind: hostclaim.KindServing, Exec: "box"}}
149 s := Selection{Host: "x.example", Box: "box", Action: "spawn"}
150 base := Policy{Held: func(context.Context, string) (string, error) { return "", nil }}
151 if err := base.Check(ctx, serving, s, hostwrite.Identity{}); err != nil {
152 t.Fatal(err)
153 }
154 var seen []string
155 deny := errors.New("refused by region HA")
156 pass := func(name string) PolicyRefusal {
157 return func(context.Context, *hostclaim.Claim, Selection, hostwrite.Identity) error {
158 seen = append(seen, name)
159 return nil
160 }
161 }
162 refuse := func(context.Context, *hostclaim.Claim, Selection, hostwrite.Identity) error {
163 seen = append(seen, "refuse")
164 return deny
165 }
166 one := base.WithRefusals(pass("a"))
167 two := one.WithRefusals(refuse)
168 if err := one.Check(ctx, serving, s, hostwrite.Identity{}); err != nil || len(seen) != 1 {
169 t.Fatalf("passing refusal: %v %v", err, seen)
170 }
171 seen = nil
172 if err := two.Check(ctx, serving, s, hostwrite.Identity{}); !errors.Is(err, deny) || len(seen) != 2 || seen[0] != "a" {
173 t.Fatalf("added refusal: %v %v", err, seen)
174 }
175 seen = nil
176 if err := one.Check(ctx, serving, s, hostwrite.Identity{}); err != nil || len(seen) != 1 {
177 t.Fatalf("WithRefusals changed an earlier policy: %v %v", err, seen)
178 }
179 if err := base.Check(ctx, serving, s, hostwrite.Identity{}); err != nil {
180 t.Fatalf("base changed: %v", err)
181 }
182 // A refused base never reaches the refusals.
183 seen = nil
184 foreign := s
185 foreign.Box = "other"
186 if err := two.Check(ctx, serving, foreign, hostwrite.Identity{}); !errors.Is(err, ErrChanged) || len(seen) != 0 {
187 t.Fatalf("refusals consulted after a base refusal: %v %v", err, seen)
188 }
189 // A nil refusal fails closed.
190 if err := base.WithRefusals(nil).Check(ctx, serving, s, hostwrite.Identity{}); !errors.Is(err, ErrUnsupported) {
191 t.Fatalf("nil refusal: %v", err)
192 }
193 // Aliasing: appending to two copies of the same parent keeps each its own.
194 x, y := one.WithRefusals(refuse), one.WithRefusals(pass("y"))
195 seen = nil
196 if err := y.Check(ctx, serving, s, hostwrite.Identity{}); err != nil || len(seen) != 2 || seen[1] != "y" {
197 t.Fatalf("sibling copies share refusals: %v %v", err, seen)
198 }
199 if err := x.Check(ctx, serving, s, hostwrite.Identity{}); !errors.Is(err, deny) {
200 t.Fatalf("sibling refusal lost: %v", err)
201 }
202}
203