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 "net/http"
6 "testing"
7 "time"
8
9 "github.com/carlosframework/platform/internal/objstore"
10 "github.com/carlosframework/platform/internal/provision"
11 "github.com/carlosframework/platform/internal/seeds"
12)
13
14const bindHost = "other.shop.example.com"
15
16// plainInstance writes a plain record of shop on host in env.
17func (f fromFixture) plainInstance(t *testing.T, host, env string) {
18 t.Helper()
19 if err := provision.CreateRecord(context.Background(), f.fx.rel, provision.Record{Schema: 1, Host: host, Slug: "other",
20 App: fromApp, Account: "bab", Channel: "stable", Environment: env, CreatedAt: time.Now().UTC()}); err != nil {
21 t.Fatal(err)
22 }
23}
24
25// bindEnvironment patches host's environment through the update door.
26func (f fromFixture) bindEnvironment(t *testing.T, host, env string) (int, string) {
27 t.Helper()
28 code, _, out := rawReq(t, http.MethodPatch, f.url+"/"+host, f.token, `{"environment":"`+env+`"}`)
29 return code, out
30}
31
32// Final review F3: the finish's first look passes (the store is the
33// seed's, and no other instance names canary), then another instance binds
34// canary through the update door, then the finish goes on to its teardown.
35// The binding took the store from the seed under the store's lock, so the
36// teardown's locked re-read keeps it: the store and its bucket survive,
37// with the mark cleared, and the record still goes.
38func TestFinishSeedSparesAStoreBoundAfterItsFirstLook(t *testing.T) {
39 f := finishFixture(t)
40 id := newSeedID(t)
41 f.seedRecord(t, fromHost, "canary", id, seeds.KindImport, seeds.StateRolledBack)
42 f.markedStore(t, id)
43 f.plainInstance(t, bindHost, "staging")
44 bucket := storeBucketName(f.canaryStore(t))
45 if f.fake.Buckets[bucket] == nil {
46 f.fake.Buckets[bucket] = map[string][]byte{}
47 }
48 bound := false
49 finishSeedCheckedHook = func(host string) {
50 if host != fromHost || bound {
51 return
52 }
53 bound = true
54 if code, out := f.bindEnvironment(t, bindHost, "canary"); code != http.StatusOK {
55 t.Errorf("bind canary = %d %s", code, out)
56 }
57 }
58 t.Cleanup(func() { finishSeedCheckedHook = nil })
59
60 if err := f.fx.srv.finishSeed(context.Background(), "bab", fromApp, fromHost, id); err != nil {
61 t.Fatal(err)
62 }
63 if !bound {
64 t.Fatal("the finish never reached its teardown checks")
65 }
66 store := f.canaryStore(t)
67 if store == nil || store.Grant == nil || store.Declared == nil || store.Declared.Seed != "" {
68 t.Fatalf("canary store after the finish = %+v; want it standing, its mark cleared", store)
69 }
70 if _, ok := f.fake.Buckets[bucket]; !ok {
71 t.Fatalf("bucket %s was torn down under an instance that bound it", bucket)
72 }
73 if rec, err := f.record(t, bindHost); err != nil || rec.Environment != "canary" {
74 t.Fatalf("the binder's record = %+v, %v", rec, err)
75 }
76 f.requireRecordGone(t, fromHost)
77}
78
79// The update door's binding waits for the store's walk lock, which the
80// finish's teardown holds, and clears another seed's mark in the same
81// locked stretch as its record write.
82func TestEnvironmentBindingWaitsForTheStoreLock(t *testing.T) {
83 f := finishFixture(t)
84 id := newSeedID(t)
85 f.seedRecord(t, fromHost, "canary", id, seeds.KindImport, seeds.StatePlaced)
86 f.markedStore(t, id)
87 f.plainInstance(t, bindHost, "staging")
88 unlock := f.fx.srv.storeWalks.lock("bab", fromApp, "canary")
89 done := make(chan int, 1)
90 go func() {
91 code, _, _ := rawReq(t, http.MethodPatch, f.url+"/"+bindHost, f.token, `{"environment":"canary"}`)
92 done <- code
93 }()
94 waitForStoreLockQueue(t, f, "canary", 2, unlock)
95 if store := f.canaryStore(t); store.Declared.Seed != id {
96 t.Fatalf("the binding cleared the mark without the lock: %+v", store.Declared)
97 }
98 if rec, _ := f.record(t, bindHost); rec.Environment != "staging" {
99 t.Fatalf("the binding landed without the lock: %+v", rec)
100 }
101 unlock()
102 if code := <-done; code != http.StatusOK {
103 t.Fatalf("bind canary = %d", code)
104 }
105 if store := f.canaryStore(t); store.Declared.Seed != "" {
106 t.Fatalf("canary store after the binding = %+v; want the mark cleared", store.Declared)
107 }
108}
109
110// waitForStoreLockQueue waits until env's store lock has refs holders and
111// waiters. On a timeout it unlocks and fails.
112func waitForStoreLockQueue(t *testing.T, f fromFixture, env string, refs int, unlock func()) {
113 t.Helper()
114 deadline := time.Now().Add(3 * time.Second)
115 for {
116 f.fx.srv.storeWalks.mu.Lock()
117 n := 0
118 if e := f.fx.srv.storeWalks.m[storeWalkKey("bab", fromApp, env)]; e != nil {
119 n = e.refs
120 }
121 f.fx.srv.storeWalks.mu.Unlock()
122 if n == refs {
123 return
124 }
125 if time.Now().After(deadline) {
126 unlock()
127 t.Fatalf("the store lock of %s has %d holders and waiters, want %d", env, n, refs)
128 }
129 time.Sleep(time.Millisecond)
130 }
131}
132
133// A binding leaves alone: a store with no mark, the binder's own seed's
134// store, and an unchanged environment. Only another seed's mark is cleared.
135func TestEnvironmentBindingClearsOnlyAnotherSeedsMark(t *testing.T) {
136 for _, c := range []struct {
137 name string
138 mark func(own string) string
139 binder func(f fromFixture, t *testing.T, own string)
140 cleared bool
141 }{
142 {"another seed's", func(string) string { return newSeedID(t) },
143 func(f fromFixture, t *testing.T, _ string) { f.plainInstance(t, bindHost, "staging") }, true},
144 {"no mark", func(string) string { return "" },
145 func(f fromFixture, t *testing.T, _ string) { f.plainInstance(t, bindHost, "staging") }, false},
146 {"the binder's own seed's", func(own string) string { return own },
147 func(f fromFixture, t *testing.T, own string) {
148 f.seedRecord(t, bindHost, "staging", own, seeds.KindImport, seeds.StateCommitted)
149 }, false},
150 } {
151 t.Run(c.name, func(t *testing.T) {
152 f := finishFixture(t)
153 own := newSeedID(t)
154 c.binder(f, t, own)
155 mark := c.mark(own)
156 f.markedStore(t, mark)
157 if code, out := f.bindEnvironment(t, bindHost, "canary"); code != http.StatusOK {
158 t.Fatalf("bind canary = %d %s", code, out)
159 }
160 got := f.canaryStore(t).Declared.Seed
161 if want := map[bool]string{true: "", false: mark}[c.cleared]; got != want {
162 t.Fatalf("mark after the binding = %q, want %q", got, want)
163 }
164 })
165 }
166 // Re-sending the environment an instance already names takes nothing.
167 f := finishFixture(t)
168 f.plainInstance(t, bindHost, "canary")
169 mark := newSeedID(t)
170 f.markedStore(t, mark)
171 if code, out := f.bindEnvironment(t, bindHost, "canary"); code != http.StatusOK {
172 t.Fatalf("re-bind canary = %d %s", code, out)
173 }
174 if got := f.canaryStore(t).Declared.Seed; got != mark {
175 t.Fatalf("mark after an unchanged binding = %q, want %q", got, mark)
176 }
177}
178
179// A create naming an environment, on an app that provisions no store per
180// instance, takes the store from the seed that made it.
181func TestEnvironmentCreateTakesASeedsStore(t *testing.T) {
182 f := newImportFixture(t)
183 mark := newSeedID(t)
184 at := time.Now().UTC()
185 putCanaryStoreOf(t, f.fx, fromApp, &objstore.Record{
186 Declared: &objstore.Declared{EnvPrefix: "S3", At: at, By: "bob@keymail.dev", Seed: mark},
187 Grant: &objstore.GrantBlock{Backend: "mem", Bucket: "carlos-bab-shop-canary", Source: objstore.GrantSourceAccountApproval, At: at, By: "operator@keymail.dev"},
188 })
189 if code, out := f.create(t, `{"host":"`+bindHost+`","environment":"canary"}`); code != http.StatusCreated {
190 t.Fatalf("create = %d %v", code, out)
191 }
192 if got := f.canaryStore(t).Declared.Seed; got != "" {
193 t.Fatalf("mark after the create = %q, want it cleared", got)
194 }
195}
196