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/status"
14)
15
16func updatesBox(label string, enforce bool, hosts map[string]status.HostUpdates, routes map[string]string) status.Box {
17 m := &status.BoxMetrics{Updates: hosts, RouteVersions: routes}
18 if enforce {
19 m.UpdateSettings = status.UpdateSettingsProtocol
20 }
21 return status.Box{Label: label, GeneratedAt: time.Now(), Metrics: m}
22}
23
24func TestUpdatesJoinCarriesTheReason(t *testing.T) {
25 two := 2
26 j := joinBoxes([]status.Box{updatesBox("ie-1", true,
27 map[string]status.HostUpdates{"a.shop.example.com": {Desired: "v1", Reason: "major", Detail: "v2", Baking: "v4", Major: &two,
28 Pending: 7, Completed: 6, CompletedFailed: true, Account: "bab", App: "shop"}},
29 map[string]string{"a.shop.example.com": "v1"})}, time.Now())
30 u := j.update("a.shop.example.com", "bab", "shop")
31 if u == nil || !u.Enforced || u.Desired != "v1" || u.Reason != "major" || u.Detail != "v2" || u.Baking != "v4" ||
32 u.Major == nil || *u.Major != 2 || u.Pending != 7 || u.Completed != 6 || !u.CompletedFailed {
33 t.Fatalf("join = %+v", u)
34 }
35}
36
37// enforcedOnly says u is the join's answer for a report it cannot attribute:
38// whether the box enforces settings, and nothing else.
39func enforcedOnly(u *updatesJoinView, enforced bool) bool {
40 return u != nil && *u == updatesJoinView{Enforced: enforced}
41}
42
43// Astra r1 (R18): a report that names no tenant, on a box with no rollout
44// report for the host, may be another tenant's: the hostname may have been
45// retired and given to this one while the snapshot is still fresh. A
46// platform box's report is no exception. The join answers only whether the
47// box enforces settings, so an old box still reads "not enforced".
48func TestATenantlessReportJoinsOnlyWhetherTheBoxEnforces(t *testing.T) {
49 now := time.Now()
50 secret := map[string]status.HostUpdates{"h": {Desired: "v-a-desired", Reason: "pin", Detail: "v-a-pin", Baking: "v-a-baking", Pending: 4, Completed: 3, CompletedFailed: true}}
51 for _, enforce := range []bool{true, false} {
52 if u := joinBoxes([]status.Box{updatesBox("platform", enforce, secret, map[string]string{"h": "v1"})}, now).update("h", "bab", "shop"); !enforcedOnly(u, enforce) {
53 t.Fatalf("platform box (enforces %v): tenantless report joined as %+v", enforce, u)
54 }
55 var j boxJoin
56 j.joinFleetRollouts([]status.Box{updatesBox("fleet", enforce, secret, map[string]string{"h": "v1"})}, now)
57 if u := j.update("h", "bab", "shop"); !enforcedOnly(u, enforce) {
58 t.Fatalf("fleet box (enforces %v): tenantless report joined as %+v", enforce, u)
59 }
60 }
61}
62
63// R18: an entry that names its own tenant needs no rollout report, and its
64// own word wins over the rollout report's.
65func TestAnAttributedReportNeedsNoRolloutReport(t *testing.T) {
66 now := time.Now()
67 b := updatesBox("platform", true, map[string]status.HostUpdates{"h": {Reason: "manual", Detail: "v1", Account: "bab", App: "shop"}}, map[string]string{"h": "v1"})
68 j := joinBoxes([]status.Box{b}, now)
69 if u := j.update("h", "bab", "shop"); u == nil || u.Reason != "manual" || u.Detail != "v1" || !u.Enforced {
70 t.Fatalf("the named tenant = %+v", u)
71 }
72 for _, other := range [][2]string{{"cdf", "shop"}, {"bab", "blog"}} {
73 if u := j.update("h", other[0], other[1]); u != nil {
74 t.Fatalf("%s/%s joined bab/shop's report: %+v", other[0], other[1], u)
75 }
76 }
77 b.Metrics.Rollout = &status.RolloutReport{Hosts: map[string]status.HostRollout{"h": {Account: "cdf", App: "blog", Installed: "v1"}}}
78 j = joinBoxes([]status.Box{b}, now)
79 if u := j.update("h", "cdf", "blog"); u != nil {
80 t.Fatalf("the rollout report's tenant outranked the entry's own: %+v", u)
81 }
82 if u := j.update("h", "bab", "shop"); u == nil || u.Reason != "manual" {
83 t.Fatalf("the entry's own tenant = %+v", u)
84 }
85}
86
87// Review Focus 3: a box's report about a host never surfaces on another
88// tenant's list. The box's own rollout report names the host's tenant.
89func TestUpdatesJoinRefusesAnotherTenantsReport(t *testing.T) {
90 b := updatesBox("ie-1", true,
91 map[string]status.HostUpdates{"a.shop.example.com": {Reason: "manual"}}, nil)
92 b.Metrics.Rollout = &status.RolloutReport{Hosts: map[string]status.HostRollout{
93 "a.shop.example.com": {Account: "cdf", App: "blog", Installed: "v1"}}}
94 j := joinBoxes([]status.Box{b}, time.Now())
95 if u := j.update("a.shop.example.com", "bab", "shop"); u != nil {
96 t.Fatalf("another tenant's report was joined: %+v", u)
97 }
98 if u := j.update("a.shop.example.com", "bab", "blog"); u != nil {
99 t.Fatalf("another account's report was joined on a matching app: %+v", u)
100 }
101 if u := j.update("a.shop.example.com", "cdf", "blog"); u == nil || u.Reason != "manual" {
102 t.Fatalf("the named tenant lost its own report: %+v", u)
103 }
104}
105
106func TestAnOldBoxIsNotEnforced(t *testing.T) {
107 j := joinBoxes([]status.Box{updatesBox("ie-1", false, nil, map[string]string{"a.shop.example.com": "v1"})}, time.Now())
108 if u := j.update("a.shop.example.com", "bab", "shop"); u == nil || u.Enforced {
109 t.Fatalf("an old box's host = %+v; want a view saying not enforced", u)
110 }
111}
112
113// A fleet box's report names no tenant unless its rollout report does, and a
114// customer's box is not trusted to speak for a host on its own say-so: all
115// that joins is whether the box enforces settings (R18).
116func TestAFleetBoxReportWithoutATenantIsRefused(t *testing.T) {
117 now := time.Now()
118 fleet := updatesBox("fleet", true, map[string]status.HostUpdates{"h": {Reason: "manual"}}, map[string]string{"h": "v1"})
119 var j boxJoin
120 j.joinFleetRollouts([]status.Box{fleet}, now)
121 if u := j.update("h", "bab", "shop"); !enforcedOnly(u, true) {
122 t.Fatalf("a tenantless fleet report was joined: %+v", u)
123 }
124 // With the tenant from its own rollout report it is this tenant's, and
125 // only this tenant's.
126 fleet.Metrics.Rollout = &status.RolloutReport{Hosts: map[string]status.HostRollout{"h": {Account: "bab", App: "shop", Installed: "v1"}}}
127 j = boxJoin{}
128 j.joinFleetRollouts([]status.Box{fleet}, now)
129 if u := j.update("h", "bab", "shop"); u == nil || u.Reason != "manual" {
130 t.Fatalf("a fleet report naming this tenant = %+v", u)
131 }
132 if u := j.update("h", "cdf", "shop"); u != nil {
133 t.Fatalf("a fleet report naming bab was joined for cdf: %+v", u)
134 }
135}
136
137// A platform box's word wins over a fleet box's for the same host, as the
138// rollout join's does.
139func TestAFleetBoxNeverOverridesAPlatformBoxUpdates(t *testing.T) {
140 now := time.Now()
141 view := func(label, reason string) status.Box {
142 b := updatesBox(label, true, map[string]status.HostUpdates{"h": {Reason: reason}}, map[string]string{"h": "v1"})
143 b.Metrics.Rollout = &status.RolloutReport{Hosts: map[string]status.HostRollout{"h": {Account: "bab", App: "shop", Installed: "v1"}}}
144 return b
145 }
146 j := joinBoxes([]status.Box{view("platform", "manual")}, now)
147 j.joinFleetRollouts([]status.Box{view("fleet", "pin")}, now)
148 if u := j.update("h", "bab", "shop"); u == nil || u.Reason != "manual" {
149 t.Fatalf("h = %+v, want the platform box's report", u)
150 }
151 var empty boxJoin
152 empty.joinFleetRollouts([]status.Box{view("fleet", "pin")}, now)
153 if u := empty.update("h", "bab", "shop"); u == nil || u.Reason != "pin" {
154 t.Fatalf("h on an empty join = %+v, want the fleet box's report", u)
155 }
156}
157
158func TestAStaleBoxReportsNothing(t *testing.T) {
159 b := updatesBox("ie-1", true, map[string]status.HostUpdates{"h": {Reason: "manual"}}, map[string]string{"h": "v1"})
160 b.GeneratedAt = time.Now().Add(-24 * time.Hour)
161 if u := joinBoxes([]status.Box{b}, time.Now()).update("h", "bab", "shop"); u != nil {
162 t.Fatalf("a stale box's report was joined: %+v", u)
163 }
164}
165
166// The list door: update_report rides beside the record's own updates key,
167// the other tenant's host carries none, and settings_default tells an
168// untouched record from one that asked for a hold.
169func TestAPIInstanceListCarriesTheUpdateReport(t *testing.T) {
170 fx, token := instanceFixture(t, "code-inst-updates")
171 enableInstances(t, fx, "titogo", titoManifest())
172 u := instancesURL(fx, "bab", "titogo")
173 for _, h := range []string{"acme.go.tito.io", "beta.go.tito.io"} {
174 if code, _, out := rawReq(t, http.MethodPost, u, token, fmt.Sprintf(`{"host":%q,"region":"ie"}`, h)); code != http.StatusCreated {
175 t.Fatalf("create %s = %d %s", h, code, out)
176 }
177 }
178 box := status.CollectBox(nil, status.Probes{MachineID: "box1"}, time.Now())
179 box.Metrics = &status.BoxMetrics{
180 UpdateSettings: status.UpdateSettingsProtocol,
181 RouteVersions: map[string]string{"acme.go.tito.io": "v1", "beta.go.tito.io": "v9"},
182 Updates: map[string]status.HostUpdates{
183 "acme.go.tito.io": {Desired: "v1", Reason: "manual", Detail: "v1"},
184 "beta.go.tito.io": {Reason: "pin", Detail: "v9"},
185 },
186 Rollout: &status.RolloutReport{Hosts: map[string]status.HostRollout{
187 "acme.go.tito.io": {Account: "bab", App: "titogo", Installed: "v1"},
188 // A report naming another app under this host: never joined.
189 "beta.go.tito.io": {Account: "bab", App: "other", Installed: "v9"},
190 }},
191 }
192 if err := status.WriteBox(context.Background(), fx.rel, box); err != nil {
193 t.Fatal(err)
194 }
195 type listed struct {
196 Host string `json:"host"`
197 Updates string `json:"updates"`
198 SettingsDefault bool `json:"settings_default"`
199 UpdateReport *updatesJoinView `json:"update_report"`
200 }
201 list := func() []listed {
202 code, _, out := rawReq(t, http.MethodGet, u, token, "")
203 if code != http.StatusOK {
204 t.Fatalf("list = %d %s", code, out)
205 }
206 var l struct {
207 Instances []listed `json:"instances"`
208 }
209 if err := json.Unmarshal([]byte(out), &l); err != nil {
210 t.Fatal(err)
211 }
212 return l.Instances
213 }
214 for _, r := range list() {
215 switch r.Host {
216 case "acme.go.tito.io":
217 if r.UpdateReport == nil || r.UpdateReport.Reason != "manual" || !r.UpdateReport.Enforced {
218 t.Errorf("acme update_report = %+v", r.UpdateReport)
219 }
220 case "beta.go.tito.io":
221 if r.UpdateReport != nil {
222 t.Errorf("beta joined another app's report: %+v", r.UpdateReport)
223 }
224 if !r.SettingsDefault || r.Updates != "" {
225 t.Errorf("beta never asked for settings: default=%v updates=%q", r.SettingsDefault, r.Updates)
226 }
227 }
228 }
229}
230
231// The app's policy reaches settings_default: a manual default makes every
232// record a non-default one without the record saying so itself.
233func TestAPIInstanceListSettingsDefaultFollowsThePolicy(t *testing.T) {
234 fx, token := instanceFixture(t, "code-inst-updates-default")
235 m := titoManifest()
236 m.Updates = &provision.UpdatePolicy{DefaultUpdates: "manual"}
237 enableInstances(t, fx, "titogo", m)
238 u := instancesURL(fx, "bab", "titogo")
239 if code, _, out := rawReq(t, http.MethodPost, u, token, `{"host":"acme.go.tito.io","region":"ie"}`); code != http.StatusCreated {
240 t.Fatalf("create = %d %s", code, out)
241 }
242 code, _, out := rawReq(t, http.MethodGet, u, token, "")
243 if code != http.StatusOK {
244 t.Fatalf("list = %d %s", code, out)
245 }
246 var l struct {
247 Instances []struct {
248 SettingsDefault bool `json:"settings_default"`
249 } `json:"instances"`
250 }
251 if err := json.Unmarshal([]byte(out), &l); err != nil || len(l.Instances) != 1 {
252 t.Fatalf("decode %v: %s", err, out)
253 }
254 if l.Instances[0].SettingsDefault {
255 t.Fatalf("a manual app default read as default: %s", out)
256 }
257}
258
259// Astra r1 (R18), through the list door: box1 (a platform box with no
260// rollout report, as with CARLOS_SWAP off) still reports the update state
261// it last had for acme under its previous tenant. The hostname now belongs
262// to bab/titogo. The list carries whether the box enforces settings and
263// nothing of the report, and the CLI's REASON cell is empty.
264func TestAReassignedHostNeverListsThePreviousTenantsReport(t *testing.T) {
265 fx, token := instanceFixture(t, "code-inst-updates-reassigned")
266 enableInstances(t, fx, "titogo", titoManifest())
267 u := instancesURL(fx, "bab", "titogo")
268 if code, _, out := rawReq(t, http.MethodPost, u, token, `{"host":"acme.go.tito.io","region":"ie"}`); code != http.StatusCreated {
269 t.Fatalf("create = %d %s", code, out)
270 }
271 box := status.CollectBox(nil, status.Probes{MachineID: "box1"}, time.Now())
272 box.Metrics = &status.BoxMetrics{
273 UpdateSettings: status.UpdateSettingsProtocol,
274 RouteVersions: map[string]string{"acme.go.tito.io": "v1"},
275 Updates: map[string]status.HostUpdates{
276 "acme.go.tito.io": {Desired: "v-prev-desired", Reason: "pin", Detail: "v-prev-pin", Baking: "v-prev-baking", Pending: 9, Completed: 8},
277 },
278 }
279 if err := status.WriteBox(context.Background(), fx.rel, box); err != nil {
280 t.Fatal(err)
281 }
282 code, _, out := rawReq(t, http.MethodGet, u, token, "")
283 if code != http.StatusOK {
284 t.Fatalf("list = %d %s", code, out)
285 }
286 for _, leak := range []string{"v-prev-desired", "v-prev-pin", "v-prev-baking", `"reason":"pin"`, `"pending"`, `"completed"`} {
287 if strings.Contains(out, leak) {
288 t.Errorf("the list leaked %s: %s", leak, out)
289 }
290 }
291 var l struct {
292 Instances []struct {
293 Host string `json:"host"`
294 UpdateReport *updatesJoinView `json:"update_report"`
295 } `json:"instances"`
296 }
297 if err := json.Unmarshal([]byte(out), &l); err != nil || len(l.Instances) != 1 {
298 t.Fatalf("decode %v: %s", err, out)
299 }
300 if r := l.Instances[0].UpdateReport; !enforcedOnly(r, true) {
301 t.Fatalf("acme update_report = %+v, want enforced and nothing else", r)
302 }
303}
304