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 "bytes"
5 "context"
6 "errors"
7 "net/http"
8 "net/http/httptest"
9 "os"
10 "testing"
11 "time"
12
13 "github.com/carlosframework/platform/internal/objstore"
14 "github.com/carlosframework/platform/internal/release"
15)
16
17func TestSeedOwnsStore(t *testing.T) {
18 id := "sd_aaaaaaaaaaaaaaaaaaaaaaaaaa"
19 for _, c := range []struct {
20 rec *objstore.Record
21 want bool
22 }{
23 {nil, true}, // no store: this seed may make one
24 {&objstore.Record{Declared: &objstore.Declared{}}, true}, // declared only: no estate yet
25 {&objstore.Record{Declared: &objstore.Declared{Seed: id}, Grant: &objstore.GrantBlock{}}, true},
26 {&objstore.Record{Declared: &objstore.Declared{Seed: "sd_bbbbbbbbbbbbbbbbbbbbbbbbbb"}, Grant: &objstore.GrantBlock{}}, false},
27 {&objstore.Record{Declared: &objstore.Declared{}, Provisioned: &objstore.Provisioned{}}, false},
28 {&objstore.Record{Grant: &objstore.GrantBlock{}}, false}, // estate nobody declared: not this seed's
29 } {
30 if got := seedOwnsStore(c.rec, id); got != c.want {
31 t.Errorf("seedOwnsStore(%+v) = %v, want %v", c.rec, got, c.want)
32 }
33 }
34}
35
36const (
37 seedA = "sd_aaaaaaaaaaaaaaaaaaaaaaaaaa"
38 seedB = "sd_bbbbbbbbbbbbbbbbbbbbbbbbbb"
39)
40
41// seededStoreFixture is privateStorageFixture with a neutral app, shop,
42// whose canary environment the seeded tests provision.
43func seededStoreFixture(t *testing.T, code string) (*consoleFixture, *objstore.MemProvisioner, *Account) {
44 t.Helper()
45 fx, fake, _, _ := privateStorageFixture(t, code)
46 if _, err := fx.st.CreateAppIn(OpsAccountID, "shop", "paul@keymail.dev", time.Now()); err != nil {
47 t.Fatal(err)
48 }
49 acct, err := fx.st.AccountByID(OpsAccountID)
50 if err != nil {
51 t.Fatal(err)
52 }
53 return fx, fake, acct
54}
55
56func putCanaryStore(t *testing.T, fx *consoleFixture, rec *objstore.Record) {
57 t.Helper()
58 if _, err := objstore.PutIn(context.Background(), fx.rel, release.OpsAccount, "shop", "canary", rec, ""); err != nil {
59 t.Fatal(err)
60 }
61}
62
63func canaryStore(t *testing.T, fx *consoleFixture) *objstore.Record {
64 t.Helper()
65 rec, _, err := objstore.GetIn(context.Background(), fx.rel, release.OpsAccount, "shop", "canary")
66 if err != nil {
67 t.Fatal(err)
68 }
69 return rec
70}
71
72// A store that already holds estate and is not this seed's is refused
73// under the store lock, before any declaration or grant: a concurrent
74// `carlos store create --environment canary`, or another seed, must never
75// hand a seeded instance a store that is not new and empty. Nothing is
76// answered on w; the caller speaks the refusal.
77func TestInstanceStoreProvisionForRefusesAStoreThatIsNotThisSeeds(t *testing.T) {
78 at := time.Now().UTC().Add(-time.Hour)
79 for name, planted := range map[string]*objstore.Record{
80 "another seed's grant": {
81 Declared: &objstore.Declared{EnvPrefix: "CARLOS_STORE", At: at, By: "alice@keymail.dev", Seed: seedB},
82 Grant: &objstore.GrantBlock{Backend: "mem", Bucket: "carlos-bab-shop-canary", Source: objstore.GrantSourceAccountApproval, At: at, By: "operator@keymail.dev"},
83 },
84 "an ordinary store's grant": {
85 Declared: &objstore.Declared{EnvPrefix: "CARLOS_STORE", At: at, By: "alice@keymail.dev"},
86 Grant: &objstore.GrantBlock{Backend: "mem", Bucket: "carlos-bab-shop-canary", Source: objstore.GrantSourceAccountApproval, At: at, By: "operator@keymail.dev"},
87 },
88 "an ordinary provisioned store": {
89 Declared: &objstore.Declared{EnvPrefix: "CARLOS_STORE", At: at, By: "alice@keymail.dev"},
90 Provisioned: &objstore.Provisioned{Bucket: "carlos-bab-shop-canary", Region: "eu-west-1", IAMUser: "carlos-store-bab-shop-canary", At: at},
91 },
92 } {
93 t.Run(name, func(t *testing.T) {
94 fx, fake, acct := seededStoreFixture(t, "code-seed-taken")
95 putCanaryStore(t, fx, planted)
96 w := httptest.NewRecorder()
97 got, ok, err := fx.srv.instanceStoreProvisionFor(context.Background(), w, acct, "shop", "canary", "alice@keymail.dev", seedA)
98 if ok || !errors.Is(err, errSeedStoreTaken) {
99 t.Fatalf("provision = %v, %v; want errSeedStoreTaken", ok, err)
100 }
101 if got == nil || storeBucketName(got) != "carlos-bab-shop-canary" {
102 t.Fatalf("returned record = %+v, want the store that is there, for the caller to name", got)
103 }
104 if w.Body.Len() != 0 {
105 t.Fatalf("answered %d %q, want nothing (the caller speaks this refusal)", w.Code, w.Body.String())
106 }
107 if len(fake.Buckets) != 0 || len(fake.Users) != 0 {
108 t.Fatalf("backend buckets %v, users %v; want no grant walk", fake.Buckets, fake.Users)
109 }
110 after := canaryStore(t, fx)
111 if after.Declared == nil || after.Declared.Seed != planted.Declared.Seed || !after.Declared.At.Equal(at) {
112 t.Fatalf("declaration = %+v, want the planted one untouched", after.Declared)
113 }
114 })
115 }
116}
117
118// With no store, the seed declares one marked with its id and provisions
119// it; a retry of the same seed then resumes that store.
120func TestInstanceStoreProvisionForMarksANewStoreWithTheSeed(t *testing.T) {
121 fx, fake, acct := seededStoreFixture(t, "code-seed-new")
122 for range 2 {
123 w := httptest.NewRecorder()
124 out, ok, err := fx.srv.instanceStoreProvisionFor(context.Background(), w, acct, "shop", "canary", "alice@keymail.dev", seedA)
125 if !ok || err != nil {
126 t.Fatalf("provision = %v, %v (%d %s)", ok, err, w.Code, w.Body.String())
127 }
128 if out.Provisioned == nil || out.Declared == nil || out.Declared.Seed != seedA {
129 t.Fatalf("store = %+v, want a provisioned store declared by %s", out, seedA)
130 }
131 }
132 if _, ok := fake.Buckets["carlos-bab-shop-canary"]; !ok || len(fake.Buckets) != 1 {
133 t.Fatalf("backend buckets = %v, want the one canary bucket", fake.Buckets)
134 }
135}
136
137// A declared-only record holds no estate, so a seed takes it over: the
138// declaration is rewritten for this seed (keeping no other seed's mark)
139// and the store is provisioned.
140func TestInstanceStoreProvisionForRedeclaresADeclaredOnlyRecord(t *testing.T) {
141 for name, decl := range map[string]*objstore.Declared{
142 "another seed's": {EnvPrefix: "CARLOS_STORE", At: time.Now().UTC().Add(-time.Hour), By: "alice@keymail.dev", Seed: seedB},
143 "an ordinary": {EnvPrefix: "CARLOS_STORE", At: time.Now().UTC().Add(-time.Hour), By: "alice@keymail.dev"},
144 } {
145 t.Run(name, func(t *testing.T) {
146 fx, _, acct := seededStoreFixture(t, "code-seed-redeclare")
147 putCanaryStore(t, fx, &objstore.Record{Declared: decl})
148 w := httptest.NewRecorder()
149 out, ok, err := fx.srv.instanceStoreProvisionFor(context.Background(), w, acct, "shop", "canary", "bob@keymail.dev", seedA)
150 if !ok || err != nil {
151 t.Fatalf("provision = %v, %v (%d %s)", ok, err, w.Code, w.Body.String())
152 }
153 if out.Declared == nil || out.Declared.Seed != seedA || out.Declared.By != "bob@keymail.dev" || out.Provisioned == nil {
154 t.Fatalf("store = %+v / %+v, want provisioned and declared by %s", out, out.Declared, seedA)
155 }
156 })
157 }
158}
159
160// takeCanaryBucketName plants another store of shop that names canary's
161// bucket, so canary's first grant fails with ErrStoreNameTaken before it
162// writes a grant block.
163func takeCanaryBucketName(t *testing.T, fx *consoleFixture) {
164 t.Helper()
165 at := time.Now().UTC()
166 rec := &objstore.Record{
167 Declared: &objstore.Declared{EnvPrefix: "CARLOS_STORE", At: at, By: "alice@keymail.dev"},
168 Grant: &objstore.GrantBlock{Backend: "mem", Bucket: "carlos-bab-shop-canary", Source: objstore.GrantSourceAccountApproval, At: at, By: "operator@keymail.dev"},
169 }
170 if _, err := objstore.PutIn(context.Background(), fx.rel, release.OpsAccount, "shop", "staging", rec, ""); err != nil {
171 t.Fatal(err)
172 }
173}
174
175func canaryRaw(t *testing.T, fx *consoleFixture) []byte {
176 t.Helper()
177 raw, _, err := fx.rel.Get(context.Background(), objstore.KeyIn(release.OpsAccount, "shop", "canary"))
178 if errors.Is(err, os.ErrNotExist) {
179 return nil
180 }
181 if err != nil {
182 t.Fatal(err)
183 }
184 return raw
185}
186
187// A seed whose grant fails before writing its block leaves no record
188// marked with its id. Another path could otherwise grant that record and
189// keep the mark, and the seed's finish would tear down a store it never
190// made. Here the seed created the record, so the record is gone.
191func TestInstanceStoreProvisionForRollsBackAFreshDeclaration(t *testing.T) {
192 fx, fake, acct := seededStoreFixture(t, "code-seed-rollback-fresh")
193 takeCanaryBucketName(t, fx)
194 w := httptest.NewRecorder()
195 _, ok, err := fx.srv.instanceStoreProvisionFor(context.Background(), w, acct, "shop", "canary", "alice@keymail.dev", seedA)
196 if ok || err != nil || w.Code != http.StatusConflict {
197 t.Fatalf("provision = %v, %v (%d %s); want the name-taken refusal answered", ok, err, w.Code, w.Body.String())
198 }
199 if raw := canaryRaw(t, fx); raw != nil {
200 t.Fatalf("canary record = %s, want none: the seed's declaration must not outlive its failed grant", raw)
201 }
202 if len(fake.Buckets) != 0 {
203 t.Fatalf("backend buckets = %v, want none", fake.Buckets)
204 }
205}
206
207// Here the seed re-declared a record that was there before it, so the
208// record goes back to exactly the bytes it had.
209func TestInstanceStoreProvisionForRestoresARedeclaredRecord(t *testing.T) {
210 for name, decl := range map[string]*objstore.Declared{
211 "another seed's": {EnvPrefix: "SHOP_FILES", At: time.Now().UTC().Add(-time.Hour), By: "alice@keymail.dev", Seed: seedB},
212 "an ordinary": {EnvPrefix: "SHOP_FILES", At: time.Now().UTC().Add(-time.Hour), By: "alice@keymail.dev"},
213 } {
214 t.Run(name, func(t *testing.T) {
215 fx, _, acct := seededStoreFixture(t, "code-seed-rollback-redeclare")
216 putCanaryStore(t, fx, &objstore.Record{Declared: decl})
217 takeCanaryBucketName(t, fx)
218 before := canaryRaw(t, fx)
219 w := httptest.NewRecorder()
220 _, ok, err := fx.srv.instanceStoreProvisionFor(context.Background(), w, acct, "shop", "canary", "bob@keymail.dev", seedA)
221 if ok || err != nil || w.Code != http.StatusConflict {
222 t.Fatalf("provision = %v, %v (%d %s); want the name-taken refusal answered", ok, err, w.Code, w.Body.String())
223 }
224 if after := canaryRaw(t, fx); !bytes.Equal(after, before) {
225 t.Fatalf("canary record after the failed seed:\n%s\nwant byte-identical to before:\n%s", after, before)
226 }
227 })
228 }
229}
230
231// A grant that wrote its block before failing has made estate for this
232// seed: the record keeps the block and the mark, for the seed's finish.
233func TestInstanceStoreProvisionForKeepsAGrantItWrote(t *testing.T) {
234 fx, fake, acct := seededStoreFixture(t, "code-seed-keep-grant")
235 fake.FailNext = map[string]error{"EnsureUser": errors.New("iam is down")}
236 w := httptest.NewRecorder()
237 _, ok, err := fx.srv.instanceStoreProvisionFor(context.Background(), w, acct, "shop", "canary", "alice@keymail.dev", seedA)
238 if ok || err != nil || w.Code != http.StatusBadGateway {
239 t.Fatalf("provision = %v, %v (%d %s); want the walk failure answered", ok, err, w.Code, w.Body.String())
240 }
241 rec := canaryStore(t, fx)
242 if rec == nil || rec.Grant == nil || rec.Declared == nil || rec.Declared.Seed != seedA {
243 t.Fatalf("canary record = %+v, want the grant block and the seed's mark kept", rec)
244 }
245}
246
247// A seed that wrote its grant block and then aborted leaves a store marked
248// with its id. An ordinary create sharing that environment adopts the
249// store, and the mark goes in the same write, so the seed's finish cannot
250// tear down a store another host now uses.
251func TestInstanceStoreProvisionAdoptingASeedsStoreClearsTheMark(t *testing.T) {
252 fx, fake, acct := seededStoreFixture(t, "code-seed-adopted")
253 fake.FailNext = map[string]error{"EnsureUser": errors.New("iam is down")}
254 w := httptest.NewRecorder()
255 if _, ok, _ := fx.srv.instanceStoreProvisionFor(context.Background(), w, acct, "shop", "canary", "alice@keymail.dev", seedA); ok {
256 t.Fatal("the seeded provision succeeded, want its walk to fail after the grant block")
257 }
258 if rec := canaryStore(t, fx); rec == nil || rec.Grant == nil || rec.Declared.Seed != seedA {
259 t.Fatalf("canary record = %+v, want the seed's grant block and mark", rec)
260 }
261 w = httptest.NewRecorder()
262 out, ok := fx.srv.instanceStoreProvision(context.Background(), w, acct, "shop", "canary", "bob@keymail.dev")
263 if !ok {
264 t.Fatalf("ordinary provision refused: %d %s", w.Code, w.Body.String())
265 }
266 for _, rec := range []*objstore.Record{out, canaryStore(t, fx)} {
267 if rec.Declared == nil || rec.Declared.Seed != "" || rec.Provisioned == nil {
268 t.Fatalf("store after adoption = %+v / %+v, want provisioned with no seed mark", rec, rec.Declared)
269 }
270 }
271}
272
273// Without a seed nothing is rolled back: a failed grant leaves the
274// declaration standing, as it always has, for a retry to resume.
275func TestInstanceStoreProvisionLeavesItsDeclarationOnAFailedGrant(t *testing.T) {
276 fx, _, acct := seededStoreFixture(t, "code-unseeded-failed-grant")
277 takeCanaryBucketName(t, fx)
278 w := httptest.NewRecorder()
279 if _, ok := fx.srv.instanceStoreProvision(context.Background(), w, acct, "shop", "canary", "alice@keymail.dev"); ok || w.Code != http.StatusConflict {
280 t.Fatalf("provision = %v (%d %s); want the name-taken refusal", ok, w.Code, w.Body.String())
281 }
282 rec := canaryStore(t, fx)
283 if rec == nil || rec.Declared == nil || rec.Declared.Seed != "" || rec.Declared.By != "alice@keymail.dev" || rec.Grant != nil {
284 t.Fatalf("canary record = %+v, want the unmarked declaration left standing", rec)
285 }
286}
287
288// Without a seed, a declared-only record keeps its author and timestamp,
289// exactly as before: only a seeded create re-declares. A seed's mark on it
290// does not survive the grant: the store is this create's, not the seed's,
291// and the seed's finish must never tear it down.
292func TestInstanceStoreProvisionKeepsAnExistingDeclaration(t *testing.T) {
293 fx, _, acct := seededStoreFixture(t, "code-unseeded-keep")
294 at := time.Now().UTC().Add(-time.Hour).Truncate(time.Second)
295 putCanaryStore(t, fx, &objstore.Record{Declared: &objstore.Declared{EnvPrefix: "CARLOS_STORE", At: at, By: "alice@keymail.dev", Seed: seedB}})
296 w := httptest.NewRecorder()
297 out, ok := fx.srv.instanceStoreProvision(context.Background(), w, acct, "shop", "canary", "bob@keymail.dev")
298 if !ok {
299 t.Fatalf("provision refused: %d %s", w.Code, w.Body.String())
300 }
301 for _, rec := range []*objstore.Record{out, canaryStore(t, fx)} {
302 if rec.Declared == nil || rec.Declared.By != "alice@keymail.dev" || !rec.Declared.At.Equal(at) || rec.Declared.Seed != "" || rec.Grant == nil {
303 t.Fatalf("declaration = %+v, want the existing one with the seed's mark cleared by the grant", rec.Declared)
304 }
305 }
306}
307