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 "encoding/json"
6 "time"
7
8 "github.com/carlosframework/platform/internal/hostclaim"
9 "github.com/carlosframework/platform/internal/hostwrite"
10 "github.com/carlosframework/platform/internal/provision"
11)
12
13// WakePhase is the K6 execution table. Participant membership alone never starts code.
14func WakePhase(c *hostclaim.Claim, box string, probe bool) error {
15 if c == nil || c.Holder == nil || box == "" || c.Op.Exec != box {
16 return ErrChanged
17 }
18 for _, lost := range c.Lost {
19 if lost == box {
20 return ErrChanged
21 }
22 }
23 switch c.Op.Kind {
24 case hostclaim.KindServing:
25 if !probe && c.Op.ID == "" && c.Op.Phase == "" {
26 return nil
27 }
28 case hostclaim.KindLanding:
29 if c.Op.ID != "" && ((!probe && c.Op.Phase == "committed") || (probe && c.Op.Phase == "probing")) {
30 return nil
31 }
32 case hostclaim.KindReverting, hostclaim.KindRestoring:
33 if c.Op.ID != "" && ((!probe && (c.Op.Phase == "probed" || c.Op.Phase == "opened")) || (probe && c.Op.Phase == "probing")) {
34 return nil
35 }
36 case hostclaim.KindRelocating:
37 // Claim Plan 11 R1 as accepted: op.exec's probe in probing, never
38 // while stuck. No ordinary relocating wake is admitted, the
39 // source's in starting and releasing included.
40 if probe && c.Op.ID != "" && c.Op.Phase == hostclaim.PhaseRelocatingProbing && c.Op.Journal["stuck"] == "" {
41 return nil
42 }
43 }
44 return ErrChanged
45}
46
47// Policy separates ordinary writes from the moves-owned held actions.
48// Moves validates roles, held inputs and phase-specific rights with the same capture.
49// Relocation is region HA's typed owner for a relocating claim (claim Plan
50// 11 R1, R8). It is consulted only after the claim's own relocating gate has
51// passed, so it can refuse and never grant; nil refuses every relocating
52// action. refusals are the append-only checks WithRefusals adds.
53type Policy struct {
54 Inputs func(context.Context, *hostclaim.Claim, Selection, hostwrite.Identity) error
55 Moves func(context.Context, *hostclaim.Claim, Selection, hostwrite.Identity) error
56 Relocation func(context.Context, *hostclaim.Claim, Selection, hostwrite.Identity) error
57 Held func(context.Context, string) (string, error)
58 refusals []PolicyRefusal
59}
60
61// PolicyRefusal is one append-only policy check (claim Plan 11, R8 as
62// accepted). It runs only after every base check of Policy.Check passed, so
63// returning nil changes nothing and an error refuses the selection.
64type PolicyRefusal func(context.Context, *hostclaim.Claim, Selection, hostwrite.Identity) error
65
66// WithRefusals returns a copy of p that also runs refusals, after p's own
67// checks and its earlier refusals. p is unchanged, and no refusal can be
68// removed or replaced: the list is unexported and only ever copied.
69func (p Policy) WithRefusals(refusals ...PolicyRefusal) Policy {
70 p.refusals = append(append([]PolicyRefusal(nil), p.refusals...), refusals...)
71 return p
72}
73
74func (p Policy) Check(ctx context.Context, c *hostclaim.Claim, s Selection, id hostwrite.Identity) error {
75 if err := p.check(ctx, c, s, id); err != nil {
76 return err
77 }
78 for _, refuse := range p.refusals {
79 if refuse == nil {
80 return ErrUnsupported
81 }
82 copy, err := detachedPolicyClaim(c)
83 if err != nil {
84 return err
85 }
86 if err := refuse(ctx, copy, s, id); err != nil {
87 return err
88 }
89 }
90 return nil
91}
92
93func (p Policy) check(ctx context.Context, c *hostclaim.Claim, s Selection, id hostwrite.Identity) error {
94 if c == nil || p.Held == nil {
95 return ErrUnsupported
96 }
97 hold, err := p.Held(ctx, s.Host)
98 if err != nil {
99 return err
100 }
101 if c.Op.Kind == hostclaim.KindServing {
102 if id.Config != "" || id.Release != "" {
103 if p.Inputs == nil {
104 return ErrUnsupported
105 }
106 if err := p.Inputs(ctx, c, s, id); err != nil {
107 return err
108 }
109 }
110 if hold != "" {
111 return ErrChanged
112 }
113 return Serving(c, s)
114 }
115 if c.Op.Kind == hostclaim.KindRelocating {
116 if err := relocationProbe(c, s, hold); err != nil {
117 return err
118 }
119 if p.Relocation == nil {
120 return ErrChanged
121 }
122 copy, err := detachedPolicyClaim(c)
123 if err != nil {
124 return err
125 }
126 return p.Relocation(ctx, copy, s, id)
127 }
128 if p.Moves == nil || s.Op == "" || c.Op.ID != s.Op || c.Op.Exec != s.Box {
129 return ErrChanged
130 }
131 owner, owned := movesHoldOwner(c, s)
132 switch s.Action {
133 case "held-config", "held-release", "held-timers":
134 if !owned || hold != owner {
135 return ErrChanged
136 }
137 case "restore", "spawn", "seed", "writer", "install":
138 if hold != "" {
139 if !owned || hold != owner {
140 return ErrChanged
141 }
142 } else if err := WakePhase(c, s.Box, false); err != nil {
143 return err
144 }
145 default:
146 return ErrChanged
147 }
148 return p.Moves(ctx, c, s, id)
149}
150
151// movesHoldOwner is the op a moves selection's registry hold must name, and
152// false when no hold can be the selection's. A landing, a moving op and a
153// revert hold the row by their op id, the selection's. A rollback keeps the
154// move id as the claim's op id (hostclaim.RollBack) and hands the row to the
155// rollback id (ruling J3), so a restoring op in the source's recovery phases
156// expects the rollback id. That id comes only from the claim's
157// intent.rollback, as RollBack recorded it, never from the selection, the
158// request or a registry field, and it must be a valid rollback id. A
159// restoring op without one, or outside those phases, matches no hold and
160// never falls back to the selection's op (Plan 4 Task 3b fix round 1).
161func movesHoldOwner(c *hostclaim.Claim, s Selection) (string, bool) {
162 if c.Op.Kind != hostclaim.KindRestoring {
163 return s.Op, true
164 }
165 rb := c.Op.Intent["rollback"]
166 if !restoringHoldPhases[c.Op.Phase] || !provision.ValidRollbackID(rb) {
167 return "", false
168 }
169 return rb, true
170}
171
172// restoringHoldPhases are the source's recovery phases of a rollback
173// (hostclaim.RecoveryPhaseOrder(KindRestoring) from claimed on): the source
174// executes, and its registry row is held by the rollback id.
175var restoringHoldPhases = map[string]bool{
176 hostclaim.PhaseClaimed: true, hostclaim.PhaseProbing: true, hostclaim.PhaseProbed: true, hostclaim.PhaseOpened: true,
177}
178
179// relocationProbe is the claim's own gate for a relocating claim (claim Plan
180// 11 R1). The only relocating action is "probe", op.exec's probe spawn,
181// selected as its own action so an ordinary spawn never gets probe
182// authority. It is admitted in probing (WakePhase with probe set), and the
183// same selection keeps working once the passed probe's session serves in the
184// op's serving phase: selected at probing, the holder's writer box is this
185// box, the session is the selected one. A registry hold must be absent or
186// this op's own.
187func relocationProbe(c *hostclaim.Claim, s Selection, hold string) error {
188 if c.Holder == nil || s.Action != "probe" || s.Op == "" || c.Op.ID != s.Op || c.Op.Exec != s.Box || (hold != "" && hold != s.Op) {
189 return ErrChanged
190 }
191 if c.Op.Phase == hostclaim.PhaseRelocatingServing {
192 if s.Phase != hostclaim.PhaseRelocatingProbing || c.Holder.WriterBox != s.Box || c.Holder.Session != s.Session || c.Op.Journal["stuck"] != "" {
193 return ErrChanged
194 }
195 for _, lost := range c.Lost {
196 if lost == s.Box {
197 return ErrChanged
198 }
199 }
200 return nil
201 }
202 return WakePhase(c, s.Box, true)
203}
204
205const RequestLimit = 120 * time.Second
206
207// Each optional check receives its own observation. A callback cannot mutate
208// authority already checked by the base or change a later refusal's inputs.
209func detachedPolicyClaim(c *hostclaim.Claim) (*hostclaim.Claim, error) {
210 raw, err := json.Marshal(c)
211 if err != nil {
212 return nil, ErrChanged
213 }
214 var copy hostclaim.Claim
215 if err = json.Unmarshal(raw, &copy); err != nil {
216 return nil, ErrChanged
217 }
218 return &copy, nil
219}
220