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 "sync"
7 "testing"
8 "time"
9
10 "github.com/carlosframework/platform/internal/objstore"
11 "github.com/carlosframework/platform/internal/provision"
12 "github.com/carlosframework/platform/internal/release"
13)
14
15// envInUse is the refusal of a seeded create whose derived environment
16// another instance of the app already names and no store holds.
17func envInUse(env string) string {
18 return "environment " + env + " is already used by another instance of " + fromApp + ". A seeded instance gets a new environment named after its host, so choose another host."
19}
20
21// Final review F6: a source created with --environment canary before its
22// app took private storage, then a --from seed of canary.<domain>, whose
23// derived environment is canary. Both would have shared the environment
24// and the seed's new store. The seed is refused before its record is
25// reserved, and no store is made.
26func TestSeededCreateRefusesAnEnvironmentAnotherInstanceNames(t *testing.T) {
27 f := newFromFixture(t, 3)
28 f.addSource(t, provision.Record{Host: "src.shop.example.com", Slug: "src", Environment: "canary"}, 3)
29 enableInstances(t, f.fx, fromApp, fromManifest(true))
30 code, out := f.create(t, `{"host":"`+fromHost+`","from":{"host":"src.shop.example.com"}}`)
31 if code != http.StatusConflict || str(t, out, "error") != envInUse("canary") {
32 t.Fatalf("--from into a named environment = %d %v, want 409 %q", code, out, envInUse("canary"))
33 }
34 f.requireNoRecord(t, fromHost)
35 if store := f.canaryStore(t); store != nil {
36 t.Fatalf("a store was made for the refused seed: %+v", store)
37 }
38
39 // The import door answers the same, for an instance of any shape.
40 code, out = f.create(t, importBody(fromHost, importSHA, 10, importAddr))
41 if code != http.StatusConflict || str(t, out, "error") != envInUse("canary") {
42 t.Fatalf("an import into a named environment = %d %v, want 409 %q", code, out, envInUse("canary"))
43 }
44 f.requireNoRecord(t, fromHost)
45}
46
47// recordOnRecordWrite writes another instance's record naming env right
48// after the seed's own record lands: the binding made between the door's
49// first check and its walk under the store lock.
50type recordOnRecordWrite struct {
51 release.Store
52 key, env string
53 once sync.Once
54}
55
56func (s *recordOnRecordWrite) Put(ctx context.Context, key string, body []byte, ifMatch string) (string, error) {
57 tag, err := s.Store.Put(ctx, key, body, ifMatch)
58 if err == nil && key == s.key && ifMatch == "" {
59 s.once.Do(func() {
60 // Another writer's own write, so its own context: ctx carries
61 // the record write this Put belongs to (provision.WriteRecord),
62 // which a second record written under it would be refused as.
63 if cerr := provision.CreateRecord(context.Background(), s.Store, provision.Record{Schema: 1, Host: bindHost, Slug: "other", App: fromApp,
64 Account: "bab", Channel: "stable", Environment: s.env, CreatedAt: time.Now().UTC()}); cerr != nil {
65 panic(cerr)
66 }
67 })
68 }
69 return tag, err
70}
71
72func (s *recordOnRecordWrite) DeleteIfMatch(ctx context.Context, key, etag string) error {
73 return release.DeleteIfMatch(ctx, s.Store, key, etag)
74}
75
76// The walk's own check under the store lock refuses an environment another
77// instance came to name after the door's first check: the in-use sentence, the import
78// rolled back and its host free, and no store declared for it.
79func TestImportBeginEnvironmentNamedUnderTheLock(t *testing.T) {
80 f := newImportFixture(t)
81 enableInstances(t, f.fx, fromApp, fromManifest(true))
82 f.fx.srv.cfg.Releases = &recordOnRecordWrite{Store: f.fx.rel, key: provision.RecordKey("bab", fromApp, importHost), env: "old"}
83 code, out := beginImport(t, f, importHost, importSHA, 10)
84 if code != http.StatusConflict || str(t, out, "error") != envInUse("old") {
85 t.Fatalf("= %d %v, want 409 %q", code, out, envInUse("old"))
86 }
87 f.requireNoRecord(t, importHost)
88 if store, _, err := objstore.GetIn(context.Background(), f.fx.rel, "bab", fromApp, "old"); err != nil || store != nil {
89 t.Fatalf("old store = %+v, %v; want none declared", store, err)
90 }
91}
92
93// A record of the app that will not decode may name the environment: the
94// seeded create cannot tell, so it refuses with the door's retry sentence
95// and writes nothing.
96func TestSeededCreateRefusesWhenARecordCannotBeRead(t *testing.T) {
97 f := newImportFixture(t)
98 enableInstances(t, f.fx, fromApp, fromManifest(true))
99 if _, err := f.fx.rel.Put(context.Background(), provision.RecordKey("bab", fromApp, bindHost), []byte("{not json"), ""); err != nil {
100 t.Fatal(err)
101 }
102 code, out := beginImport(t, f, importHost, importSHA, 10)
103 if code != http.StatusBadGateway || str(t, out, "error") != importFailed {
104 t.Fatalf("= %d %v, want 502 %q", code, out, importFailed)
105 }
106 f.requireNoRecord(t, importHost)
107}
108