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 "fmt"
7 "net/http"
8 "strings"
9 "testing"
10 "time"
11
12 "github.com/carlosframework/platform/internal/provision"
13 "github.com/carlosframework/platform/internal/registry"
14 "github.com/carlosframework/platform/internal/release"
15 "github.com/carlosframework/platform/internal/rolloutwords"
16 "github.com/carlosframework/platform/internal/status"
17)
18
19// writeFleetHeldBox registers fleet to owner and writes, under the fleet's
20// own prefix, a box whose report holds host back on build for this
21// fixture's account and app. It returns the box's label.
22func writeFleetHeldBox(t *testing.T, fx *consoleFixture, fleet, owner, host, app, build string) string {
23 t.Helper()
24 reg, err := registry.Open(fx.regPath)
25 if err != nil {
26 t.Fatal(err)
27 }
28 defer reg.Close()
29 if err := reg.UpsertFleet(registry.Fleet{Name: fleet, Account: owner, Bucket: fleet + "-bucket"}); err != nil {
30 t.Fatal(err)
31 }
32 box := status.CollectBox(nil, status.Probes{MachineID: fleet + "-box"}, time.Now())
33 box.Metrics = &status.BoxMetrics{Rollout: &status.RolloutReport{Hosts: map[string]status.HostRollout{
34 host: {Account: fx.sqid, App: app, Installed: build, Serving: "v1", Good: "v1", Awake: true, HeldBy: build, Reason: status.ReasonHealth,
35 HeldEpoch: heldEpoch, HeldBook: heldBook, RetryV: status.HostRetryV},
36 }}}
37 if err := status.WriteBox(context.Background(), release.NewPrefixStore(fx.rel, "fleets/"+fleet+"/"), box); err != nil {
38 t.Fatal(err)
39 }
40 return box.Label
41}
42
43// listRollout is the list's answer for host: its rollout view, and whether
44// the answer said rollout_incomplete.
45func listRollout(t *testing.T, fx *consoleFixture, token, app, host string) (*rolloutView, bool) {
46 t.Helper()
47 code, _, out := rawReq(t, http.MethodGet, fx.ts.URL+"/api/cli/apps/"+fx.sqid+"/"+app+"/instances", token, "")
48 if code != http.StatusOK {
49 t.Fatalf("list = %d %s", code, out)
50 }
51 var list struct {
52 Instances []struct {
53 Host string `json:"host"`
54 Rollout *rolloutView `json:"rollout"`
55 } `json:"instances"`
56 RolloutIncomplete bool `json:"rollout_incomplete"`
57 }
58 if err := json.Unmarshal([]byte(out), &list); err != nil {
59 t.Fatal(err)
60 }
61 for _, r := range list.Instances {
62 if r.Host == host {
63 return r.Rollout, list.RolloutIncomplete
64 }
65 }
66 t.Fatalf("list lacks %s: %s", host, out)
67 return nil, false
68}
69
70// placedRecord writes app's instance record for host, placed at region
71// ("ie", or "fleet/<name>" for a customer fleet).
72func placedRecord(t *testing.T, fx *consoleFixture, app, host, region string) {
73 t.Helper()
74 if err := provision.CreateRecord(context.Background(), fx.rel, provision.Record{
75 Schema: 1, Host: host, Slug: strings.SplitN(host, ".", 2)[0],
76 App: app, Account: release.OpsAccount, Region: region, Channel: "stable",
77 CreatedAt: time.Now().UTC(), CreatedBy: "paul@keymail.dev",
78 }); err != nil {
79 t.Fatal(err)
80 }
81}
82
83// Astra plan 6 R1 #6: a customer-fleet box's report counts. Its held host
84// shows held on the list and on the instance page, and the retry door takes
85// the retry, through the one loader all three share. The record is placed on
86// the fleet, so an unreadable snapshot there makes the list incomplete.
87func TestFleetBoxHoldShowsOnTheListPageAndRetryDoor(t *testing.T) {
88 fx := newConsoleFixture(t)
89 fx.addRoute(t, registry.Route{Host: heldHost, App: "kass", Account: fx.sqid, Kind: "instance", Status: "ready", Version: "v2", Channel: "stable"})
90 placedRecord(t, fx, "kass", heldHost, registry.PlacementTarget("pi"))
91 label := writeFleetHeldBox(t, fx, "pi", fx.sqid, heldHost, "kass", "v2")
92 token := cliToken(t, fx, "paul@keymail.dev", "code-fleet-held")
93
94 ro, incomplete := listRollout(t, fx, token, "kass", heldHost)
95 if ro == nil || ro.Box != label || ro.HeldBy != "v2" || incomplete {
96 t.Fatalf("list rollout = %+v, incomplete %v: want the fleet box's hold, complete", ro, incomplete)
97 }
98 c := signInPaul(t, fx)
99 if code, _, body := get(t, c, fx.u("/apps/kass/instances/"+heldHost)); code != http.StatusOK ||
100 !strings.Contains(body, fmt.Sprintf(swapCopy.HoldText, "v2", rolloutwords.Reason(status.ReasonHealth), "v1")) {
101 t.Fatalf("page = %d, lacks the fleet box's hold", code)
102 }
103 code, _, body := rawReq(t, http.MethodPost, fx.ts.URL+"/api/cli/apps/"+fx.sqid+"/kass/instances/"+heldHost+"/retry", token, `{}`)
104 if code != http.StatusOK || !strings.Contains(body, `"version":"v2"`) {
105 t.Fatalf("retry = %d %s, want 200 for v2", code, body)
106 }
107
108 // A snapshot that cannot be read on the fleet the record is placed on
109 // makes the join incomplete.
110 if _, err := fx.rel.Put(context.Background(), "fleets/pi/status/boxes/broken.json", []byte("{not json"), ""); err != nil {
111 t.Fatal(err)
112 }
113 if _, incomplete := listRollout(t, fx, token, "kass", heldHost); !incomplete {
114 t.Fatal("an unreadable fleet box snapshot left the join complete")
115 }
116}
117
118// Astra plan 6 R1 #6: only this account's fleets are read. Another
119// account's fleet box that names this account's host is never joined,
120// whatever account its report claims: no hold on the list or the page, and
121// the retry door refuses.
122func TestAnotherAccountsFleetBoxIsNeverJoined(t *testing.T) {
123 fx := newConsoleFixture(t)
124 fx.addRoute(t, registry.Route{Host: heldHost, App: "kass", Account: fx.sqid, Kind: "instance", Status: "ready", Version: "v2", Channel: "stable"})
125 sibling(t, fx, "kass", heldHost, "stable")
126 writeFleetHeldBox(t, fx, "stranger", "zzz", heldHost, "kass", "v2")
127 token := cliToken(t, fx, "paul@keymail.dev", "code-fleet-stranger")
128
129 if ro, _ := listRollout(t, fx, token, "kass", heldHost); ro != nil {
130 t.Fatalf("list joined another account's fleet box: %+v", ro)
131 }
132 c := signInPaul(t, fx)
133 if _, _, body := get(t, c, fx.u("/apps/kass/instances/"+heldHost)); strings.Contains(body, swapCopy.Installed) {
134 t.Fatal("page joined another account's fleet box")
135 }
136 code, _, body := rawReq(t, http.MethodPost, fx.ts.URL+"/api/cli/apps/"+fx.sqid+"/kass/instances/"+heldHost+"/retry", token, `{}`)
137 if code != http.StatusConflict || !strings.Contains(body, fmt.Sprintf(apiRetryNotHeld, heldHost)) {
138 t.Fatalf("retry = %d %s, want 409", code, body)
139 }
140}
141
142// A platform box's report wins over a fleet box's for the same host: the
143// operator's boxes are the ones the console trusts most.
144func TestJoinFleetRolloutsNeverOverrideAPlatformBox(t *testing.T) {
145 now := time.Now()
146 view := func(label, build string) status.Box {
147 return status.Box{GeneratedAt: now, Label: label, Metrics: &status.BoxMetrics{Rollout: &status.RolloutReport{
148 Hosts: map[string]status.HostRollout{"h": {Account: "bab", App: "shop", Installed: build}},
149 }}}
150 }
151 j := joinBoxes([]status.Box{view("platform", "v1")}, now)
152 j.joinFleetRollouts([]status.Box{view("fleet", "v9")}, now)
153 if got := j.rollout("h", "bab", "shop"); got == nil || got.Box != "platform" || got.Installed != "v1" {
154 t.Fatalf("h = %+v, want the platform box's report", got)
155 }
156 var empty boxJoin
157 empty.joinFleetRollouts([]status.Box{view("fleet", "v9")}, now)
158 if got := empty.rollout("h", "bab", "shop"); got == nil || got.Box != "fleet" {
159 t.Fatalf("h on an empty join = %+v, want the fleet box's report", got)
160 }
161}
162
163// Astra plan 6 R2 (new Medium): completeness covers only the sources that
164// could hold this app's reports. The account has a fleet whose snapshot is
165// corrupt, but none of the app's instances is placed there; the app runs on
166// healthy platform boxes with no reports (flag off). The list must not say
167// rollout_incomplete, so the deploy watch falls back to today's URL watch.
168func TestAnUnrelatedUnreadableFleetDoesNotMakeTheListIncomplete(t *testing.T) {
169 fx := newConsoleFixture(t)
170 placedRecord(t, fx, "kass", heldHost, "ie")
171 reg, err := registry.Open(fx.regPath)
172 if err != nil {
173 t.Fatal(err)
174 }
175 defer reg.Close()
176 if err := reg.UpsertFleet(registry.Fleet{Name: "spare", Account: fx.sqid, Bucket: "spare-bucket"}); err != nil {
177 t.Fatal(err)
178 }
179 if _, err := fx.rel.Put(context.Background(), "fleets/spare/status/boxes/broken.json", []byte("{not json"), ""); err != nil {
180 t.Fatal(err)
181 }
182 box := status.CollectBox(nil, status.Probes{MachineID: "box1"}, time.Now())
183 box.Metrics = &status.BoxMetrics{RouteVersions: map[string]string{heldHost: "v1"}}
184 if err := status.WriteBox(context.Background(), fx.rel, box); err != nil {
185 t.Fatal(err)
186 }
187 token := cliToken(t, fx, "paul@keymail.dev", "code-fleet-unrelated")
188 ro, incomplete := listRollout(t, fx, token, "kass", heldHost)
189 if ro != nil || incomplete {
190 t.Fatalf("rollout %+v, incomplete %v: an unrelated fleet's unreadable snapshot blocked the legacy watch", ro, incomplete)
191 }
192}
193
194// Astra plan 6 R3: an unreadable record must never drop its fleet from the
195// completeness check. The record's host sits on fleet/pi, whose only
196// snapshot is unreadable too. The readable record is on the platform, so
197// without the fix no fleet is relevant and the list would claim to be
198// complete, sending the deploy watch to poll URLs it must not poll.
199func TestAnUnreadableRecordKeepsEveryFleetInTheCompletenessCheck(t *testing.T) {
200 fx := newConsoleFixture(t)
201 const piHost = "pi-host.example.test"
202 placedRecord(t, fx, "kass", heldHost, "ie")
203 placedRecord(t, fx, "kass", piHost, registry.PlacementTarget("pi"))
204 reg, err := registry.Open(fx.regPath)
205 if err != nil {
206 t.Fatal(err)
207 }
208 defer reg.Close()
209 if err := reg.UpsertFleet(registry.Fleet{Name: "pi", Account: fx.sqid, Bucket: "pi-bucket"}); err != nil {
210 t.Fatal(err)
211 }
212 box := status.CollectBox(nil, status.Probes{MachineID: "box1"}, time.Now())
213 box.Metrics = &status.BoxMetrics{RouteVersions: map[string]string{heldHost: "v1"}}
214 if err := status.WriteBox(context.Background(), fx.rel, box); err != nil {
215 t.Fatal(err)
216 }
217 token := cliToken(t, fx, "paul@keymail.dev", "code-unreadable-record")
218
219 // Both readable and complete first: the platform record's fleet set is
220 // empty, but the pi record is placed on pi, whose snapshot is fine.
221 if _, incomplete := listRollout(t, fx, token, "kass", heldHost); incomplete {
222 t.Fatal("a healthy app was reported incomplete before anything was broken")
223 }
224
225 // Now the pi record and the pi snapshot both go unreadable.
226 key := provision.RecordKey(release.OpsAccount, "kass", piHost)
227 _, etag, err := fx.rel.Get(context.Background(), key)
228 if err != nil {
229 t.Fatal(err)
230 }
231 if _, err := fx.rel.Put(context.Background(), key, []byte("{not json"), etag); err != nil {
232 t.Fatal(err)
233 }
234 if _, err := fx.rel.Put(context.Background(), "fleets/pi/status/boxes/broken.json", []byte("{not json"), ""); err != nil {
235 t.Fatal(err)
236 }
237 if _, incomplete := listRollout(t, fx, token, "kass", heldHost); !incomplete {
238 t.Fatal("an unreadable record dropped its fleet from the completeness check")
239 }
240}
241
242// The fleets that could hold an app's reports: where its records are placed,
243// and both ends of a move in flight. A platform region names none.
244func TestRecordFleetsNamesPlacementsAndMoveEnds(t *testing.T) {
245 got := recordFleets([]provision.Record{
246 {Host: "a", Region: "ie"},
247 {Host: "b", Region: registry.PlacementTarget("pi")},
248 {Host: "c", Region: "ie", Move: &provision.MoveState{From: "ie", To: registry.PlacementTarget("zero")}},
249 })
250 if len(got) != 2 || !got["pi"] || !got["zero"] {
251 t.Fatalf("fleets = %v, want pi and zero", got)
252 }
253 if got := recordFleets(nil); got == nil || len(got) != 0 {
254 t.Fatalf("no records = %v, want an empty, non-nil set", got)
255 }
256}
257