package console import ( "context" "net/http" "sync" "testing" "time" "github.com/carlosframework/platform/internal/objstore" "github.com/carlosframework/platform/internal/provision" "github.com/carlosframework/platform/internal/release" ) // envInUse is the refusal of a seeded create whose derived environment // another instance of the app already names and no store holds. func envInUse(env string) string { 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." } // Final review F6: a source created with --environment canary before its // app took private storage, then a --from seed of canary., whose // derived environment is canary. Both would have shared the environment // and the seed's new store. The seed is refused before its record is // reserved, and no store is made. func TestSeededCreateRefusesAnEnvironmentAnotherInstanceNames(t *testing.T) { f := newFromFixture(t, 3) f.addSource(t, provision.Record{Host: "src.shop.example.com", Slug: "src", Environment: "canary"}, 3) enableInstances(t, f.fx, fromApp, fromManifest(true)) code, out := f.create(t, `{"host":"`+fromHost+`","from":{"host":"src.shop.example.com"}}`) if code != http.StatusConflict || str(t, out, "error") != envInUse("canary") { t.Fatalf("--from into a named environment = %d %v, want 409 %q", code, out, envInUse("canary")) } f.requireNoRecord(t, fromHost) if store := f.canaryStore(t); store != nil { t.Fatalf("a store was made for the refused seed: %+v", store) } // The import door answers the same, for an instance of any shape. code, out = f.create(t, importBody(fromHost, importSHA, 10, importAddr)) if code != http.StatusConflict || str(t, out, "error") != envInUse("canary") { t.Fatalf("an import into a named environment = %d %v, want 409 %q", code, out, envInUse("canary")) } f.requireNoRecord(t, fromHost) } // recordOnRecordWrite writes another instance's record naming env right // after the seed's own record lands: the binding made between the door's // first check and its walk under the store lock. type recordOnRecordWrite struct { release.Store key, env string once sync.Once } func (s *recordOnRecordWrite) Put(ctx context.Context, key string, body []byte, ifMatch string) (string, error) { tag, err := s.Store.Put(ctx, key, body, ifMatch) if err == nil && key == s.key && ifMatch == "" { s.once.Do(func() { // Another writer's own write, so its own context: ctx carries // the record write this Put belongs to (provision.WriteRecord), // which a second record written under it would be refused as. if cerr := provision.CreateRecord(context.Background(), s.Store, provision.Record{Schema: 1, Host: bindHost, Slug: "other", App: fromApp, Account: "bab", Channel: "stable", Environment: s.env, CreatedAt: time.Now().UTC()}); cerr != nil { panic(cerr) } }) } return tag, err } func (s *recordOnRecordWrite) DeleteIfMatch(ctx context.Context, key, etag string) error { return release.DeleteIfMatch(ctx, s.Store, key, etag) } // The walk's own check under the store lock refuses an environment another // instance came to name after the door's first check: the in-use sentence, the import // rolled back and its host free, and no store declared for it. func TestImportBeginEnvironmentNamedUnderTheLock(t *testing.T) { f := newImportFixture(t) enableInstances(t, f.fx, fromApp, fromManifest(true)) f.fx.srv.cfg.Releases = &recordOnRecordWrite{Store: f.fx.rel, key: provision.RecordKey("bab", fromApp, importHost), env: "old"} code, out := beginImport(t, f, importHost, importSHA, 10) if code != http.StatusConflict || str(t, out, "error") != envInUse("old") { t.Fatalf("= %d %v, want 409 %q", code, out, envInUse("old")) } f.requireNoRecord(t, importHost) if store, _, err := objstore.GetIn(context.Background(), f.fx.rel, "bab", fromApp, "old"); err != nil || store != nil { t.Fatalf("old store = %+v, %v; want none declared", store, err) } } // A record of the app that will not decode may name the environment: the // seeded create cannot tell, so it refuses with the door's retry sentence // and writes nothing. func TestSeededCreateRefusesWhenARecordCannotBeRead(t *testing.T) { f := newImportFixture(t) enableInstances(t, f.fx, fromApp, fromManifest(true)) if _, err := f.fx.rel.Put(context.Background(), provision.RecordKey("bab", fromApp, bindHost), []byte("{not json"), ""); err != nil { t.Fatal(err) } code, out := beginImport(t, f, importHost, importSHA, 10) if code != http.StatusBadGateway || str(t, out, "error") != importFailed { t.Fatalf("= %d %v, want 502 %q", code, out, importFailed) } f.requireNoRecord(t, importHost) }