carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package console
2
3import (
4 "context"
5 "encoding/json"
6 "net/http"
7 "testing"
8
9 "github.com/carlosframework/platform/internal/hostclaim"
10 "github.com/carlosframework/platform/internal/hostops"
11 "github.com/carlosframework/platform/internal/provision"
12)
13
14// J17 at the door: on a host-claim console with its runtime configured, the
15// cut, abort and retry doors run the typed ControlWriter transition through
16// Config.ClaimAuthority, bound to the claim and record they read.
17
18// j17StartedMove starts a move through the host-claim request door and
19// returns its id. The record is preparing, the claim moving/"".
20func j17StartedMove(t *testing.T, code string) (*consoleFixture, string, string, string) {
21 t.Helper()
22 fx, token, moveURL := moveDoorWithClaim(t, code)
23 armClaimMoves(t, fx, true, true, nil)
24 if c, _, out := rawReq(t, http.MethodPost, moveURL, token, `{"region":"uk","cut":"manual"}`); c != http.StatusAccepted {
25 t.Fatalf("request = %d %s", c, out)
26 }
27 rec, err := provision.GetRecord(context.Background(), fx.rel, "bab", "titogo", "acme.go.tito.io")
28 if err != nil {
29 t.Fatal(err)
30 }
31 return fx, token, moveURL, rec.Move.ID
32}
33
34// j17SetRecord writes the record's move journal under the raw bucket, as a
35// box's own worker would.
36func j17SetRecord(t *testing.T, fx *consoleFixture, mut func(*provision.Record)) {
37 t.Helper()
38 if _, err := provision.SetRecordMove(provision.WithRecordSequence(context.Background()), fx.rel, "bab", "titogo", "acme.go.tito.io", func(r *provision.Record) error {
39 mut(r)
40 return nil
41 }); err != nil {
42 t.Fatal(err)
43 }
44}
45
46func j17Ready(r *provision.Record) {
47 r.Move.State = provision.MoveReady
48 r.Move.Inputs = &provision.MoveInputs{Release: "r1", BundleETags: map[string]string{"b": "e"}}
49}
50
51func j17MoveState(t *testing.T, fx *consoleFixture) *provision.MoveState {
52 t.Helper()
53 rec, err := provision.GetRecord(context.Background(), fx.rel, "bab", "titogo", "acme.go.tito.io")
54 if err != nil {
55 t.Fatal(err)
56 }
57 return rec.Move
58}
59
60func TestJ17DoorCutUnderHostClaim(t *testing.T) {
61 fx, token, moveURL, _ := j17StartedMove(t, "code-j17-cut")
62 j17SetRecord(t, fx, j17Ready)
63 claim := claimBytes(t, fx, "acme.go.tito.io")
64 code, _, out := rawReq(t, http.MethodPost, moveURL+"/cut", token, "")
65 if code != http.StatusAccepted {
66 t.Fatalf("cut = %d %s", code, out)
67 }
68 if m := j17MoveState(t, fx); m.State != provision.MoveCutting || m.CutPhase != provision.CutRequested {
69 t.Fatalf("record move = %+v", m)
70 }
71 var view map[string]any
72 if json.Unmarshal([]byte(out), &view) != nil || view["state"] != provision.MoveCutting {
73 t.Fatalf("answer = %s", out)
74 }
75 if claimBytes(t, fx, "acme.go.tito.io") != claim {
76 t.Fatal("the cut door wrote the claim")
77 }
78}
79
80func TestJ17DoorAbortUnderHostClaim(t *testing.T) {
81 fx, token, moveURL, _ := j17StartedMove(t, "code-j17-abort")
82 claim := claimBytes(t, fx, "acme.go.tito.io")
83 if code, _, out := rawReq(t, http.MethodDelete, moveURL, token, ""); code != http.StatusAccepted {
84 t.Fatalf("abort = %d %s", code, out)
85 }
86 if m := j17MoveState(t, fx); m.State != provision.MoveAborting {
87 t.Fatalf("record move = %+v", m)
88 }
89 if claimBytes(t, fx, "acme.go.tito.io") != claim {
90 t.Fatal("the abort door wrote the claim")
91 }
92}
93
94func TestJ17DoorRetryUnderHostClaim(t *testing.T) {
95 ctx := context.Background()
96 fx, token, moveURL, id := j17StartedMove(t, "code-j17-retry")
97 obs, err := hostclaim.Read(ctx, fx.rel, "acme.go.tito.io")
98 if err != nil {
99 t.Fatal(err)
100 }
101 rb := "rb_abcdefghijklmnopqrstuvwxyz"
102 if _, err := hostclaim.Update(ctx, fx.rel, obs, func(c *hostclaim.Claim) error {
103 c.Op.Kind, c.Op.Phase = hostclaim.KindRestoring, hostclaim.PhaseProbing
104 c.Op.Intent["rollback"] = rb
105 return nil
106 }); err != nil {
107 t.Fatal(err)
108 }
109 j17SetRecord(t, fx, func(r *provision.Record) {
110 r.Move.State, r.Move.StuckReason = provision.MoveStuck, provision.StuckRecoveryProbeAttempts
111 r.Move.Rollback = &provision.MoveRollback{ID: rb, TargetPhase: provision.RbTargetReleased, SourcePhase: provision.RbSourceProbing, StuckAt: provision.RbSourceProbing}
112 })
113 claim := claimBytes(t, fx, "acme.go.tito.io")
114 code, _, out := rawReq(t, http.MethodPost, moveURL+"/resolve", token, `{"retry":true}`)
115 if code != http.StatusAccepted {
116 t.Fatalf("retry = %d %s", code, out)
117 }
118 if m := j17MoveState(t, fx); m.ID != id || m.State != provision.MoveRollingBack || m.StuckReason != "" || m.Rollback.SourcePhase != provision.RbSourceProbing {
119 t.Fatalf("record move = %+v", m)
120 }
121 if claimBytes(t, fx, "acme.go.tito.io") != claim {
122 t.Fatal("the retry door wrote the claim")
123 }
124}
125
126// A refusal the transition records is its recorded sentence; nothing is
127// written.
128func TestJ17DoorRecordedRefusal(t *testing.T) {
129 fx, token, moveURL, _ := j17StartedMove(t, "code-j17-refusal")
130 before := j17MoveState(t, fx)
131 code, _, out := rawReq(t, http.MethodPost, moveURL+"/cut", token, "")
132 if code != http.StatusConflict || errorText(out) != "cut is only legal from ready" {
133 t.Fatalf("cut of a preparing move = %d %s", code, out)
134 }
135 if after := j17MoveState(t, fx); after.State != before.State || !after.UpdatedAt.Equal(before.UpdatedAt) {
136 t.Fatal("a refused cut wrote the record")
137 }
138 code, _, out = rawReq(t, http.MethodPost, moveURL+"/resolve", token, `{"retry":true}`)
139 if code != http.StatusConflict || errorText(out) != moveChangedSentence {
140 t.Fatalf("retry of a moving claim = %d %s", code, out)
141 }
142}
143
144// Without the move's registration nothing authorizes the record write: the
145// door answers the fixed sentence and writes nothing.
146func TestJ17DoorRefusesWithoutRegistration(t *testing.T) {
147 ctx := context.Background()
148 fx, token, moveURL, id := j17StartedMove(t, "code-j17-noreg")
149 j17SetRecord(t, fx, j17Ready)
150 a, err := hostops.ReadAdmission(ctx, fx.rel, "bab", "titogo", claimMoveAppID)
151 if err != nil {
152 t.Fatal(err)
153 }
154 delete(a.Value.Registrations, id)
155 b, _ := json.Marshal(a.Value)
156 if _, err := fx.rel.Put(ctx, a.Key, b, a.ETag); err != nil {
157 t.Fatal(err)
158 }
159 before := j17MoveState(t, fx)
160 code, _, out := rawReq(t, http.MethodPost, moveURL+"/cut", token, "")
161 if code != http.StatusConflict || errorText(out) != moveChangedSentence {
162 t.Fatalf("cut without registration = %d %s", code, out)
163 }
164 if after := j17MoveState(t, fx); after.State != before.State {
165 t.Fatal("refused cut wrote the record")
166 }
167}
168
169// The plain (flag-on, claim-off) doors are untouched: they still write
170// through SetRecordMove (covered by the Phase 1 door tests), and a host-claim
171// console without its runtime still refuses with HC5-N1
172// (TestMoveDoorPhase1RefusedUnderHostClaim, TestResolveRetryRefusedUnderHostClaim).
173
174// Final review I5 (preflight L5, claim-on half): on a host-claim console,
175// once the cut has started (cutting past requested, landing, committed,
176// rolling back, reverting) the abort door answers M-C13 before the typed
177// grant, which after the seal would refuse with "the move changed". Nothing
178// is written. A stuck move keeps the grant's own answer, and an abort from
179// cutting/requested before the seal is still the typed abort.
180func TestJ17DoorAbortAfterTheCutStartedSaysSo(t *testing.T) {
181 ctx := context.Background()
182 fx, token, moveURL, _ := j17StartedMove(t, "code-j17-mc13")
183 setPhase := func(phase string) {
184 t.Helper()
185 obs, err := hostclaim.Read(ctx, fx.rel, "acme.go.tito.io")
186 if err != nil {
187 t.Fatal(err)
188 }
189 if _, err := hostclaim.Update(ctx, fx.rel, obs, func(c *hostclaim.Claim) error {
190 c.Op.Phase = phase
191 return nil
192 }); err != nil {
193 t.Fatal(err)
194 }
195 }
196 setPhase(hostclaim.PhaseSealed)
197 for _, c := range []struct {
198 name string
199 mut func(*provision.MoveState)
200 }{
201 {"cutting/admitted", func(m *provision.MoveState) { m.State, m.CutPhase = provision.MoveCutting, provision.CutAdmitted }},
202 {"cutting/staged", func(m *provision.MoveState) { m.State, m.CutPhase = provision.MoveCutting, provision.CutStaged }},
203 {"landing", func(m *provision.MoveState) { m.State = provision.MoveLanding }},
204 {"committed", func(m *provision.MoveState) { m.State = provision.MoveCommitted }},
205 {"rolling back", func(m *provision.MoveState) { m.State = provision.MoveRollingBack }},
206 {"reverting", func(m *provision.MoveState) { m.State = provision.MoveReverting }},
207 } {
208 j17SetRecord(t, fx, func(r *provision.Record) { c.mut(r.Move) })
209 claim, before := claimBytes(t, fx, "acme.go.tito.io"), j17MoveState(t, fx)
210 if code, _, out := rawReq(t, http.MethodDelete, moveURL, token, ""); code != http.StatusConflict || errorText(out) != moveCutStartedSentence {
211 t.Fatalf("abort in %s = %d %s, want M-C13", c.name, code, out)
212 }
213 if after := j17MoveState(t, fx); after.State != before.State || !after.UpdatedAt.Equal(before.UpdatedAt) || claimBytes(t, fx, "acme.go.tito.io") != claim {
214 t.Fatalf("a refused abort in %s wrote", c.name)
215 }
216 }
217 j17SetRecord(t, fx, func(r *provision.Record) {
218 r.Move.State, r.Move.CutPhase, r.Move.StuckReason = provision.MoveStuck, provision.CutArchived, provision.StuckBaselineMismatch
219 })
220 if code, _, out := rawReq(t, http.MethodDelete, moveURL, token, ""); code != http.StatusConflict || errorText(out) == moveCutStartedSentence {
221 t.Fatalf("abort of a stuck move = %d %s, want the grant's own refusal", code, out)
222 }
223 setPhase(hostclaim.PhaseMoving)
224 j17SetRecord(t, fx, func(r *provision.Record) {
225 r.Move.State, r.Move.CutPhase, r.Move.StuckReason = provision.MoveCutting, provision.CutRequested, ""
226 })
227 if code, _, out := rawReq(t, http.MethodDelete, moveURL, token, ""); code != http.StatusAccepted {
228 t.Fatalf("abort from cutting/requested = %d %s", code, out)
229 }
230 if m := j17MoveState(t, fx); m.State != provision.MoveAborting {
231 t.Fatalf("record move = %+v", m)
232 }
233}
234