carlos / platform Public

console: resolveDBSource, the db door's source checks, factored out (Plan 5 Task 1)

dbSource carries claimErr so the claim read's own error reaches
dbClaimFailure (errors.Unwrap of a two-%w wrap is nil). errSourceResolve
keeps the door's own answer for a resolveReplica failure.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 9c98d55d7190f419efc8110c6ee3ffefca2c5aff parent 9207b1c
2 files changed, +176 −37
  • internal/console/apidb.go +82 −37
  • internal/console/apidb_source_test.go +94 −0

CI passed — run details

diff --git a/internal/console/apidb.go b/internal/console/apidb.go
index d474a3b88..7a3c82571 100644
--- a/internal/console/apidb.go
+++ b/internal/console/apidb.go
@@ -128,63 +128,108 @@ func (s *Server) dbGate(w http.ResponseWriter, r *http.Request) (dbRequest, bool
return dbRequest{}, false
}
host := r.PathValue("host")
- if !provision.ValidHostShape(host) {
- http.NotFound(w, r)
- return dbRequest{}, false
- }
ctx := r.Context()
- rec, err := provision.GetRecord(ctx, s.cfg.Releases, acct.Sqid, app, host)
- if errors.Is(err, os.ErrNotExist) {
- http.NotFound(w, r)
- return dbRequest{}, false
- }
- if err != nil {
- log.Printf("console: api db %s/%s/%s: read record: %v", acct.Sqid, app, host, err)
- jsonError(w, http.StatusBadGateway, "could not read the instance. Try again.")
- return dbRequest{}, false
- }
- if msg := dbClassRefusal(*rec); msg != "" {
- jsonError(w, http.StatusConflict, msg)
- return dbRequest{}, false
- }
- prefix, err := s.resolveReplica(*rec, host)
+ src, err := s.resolveDBSource(ctx, acct, actor, app, host)
switch {
- case errors.Is(err, ErrBadHost):
+ case errors.Is(err, os.ErrNotExist), errors.Is(err, ErrBadHost):
http.NotFound(w, r)
return dbRequest{}, false
+ case errors.Is(err, errSourceClass):
+ jsonError(w, http.StatusConflict, dbClassRefusal(src.rec))
+ return dbRequest{}, false
case errors.Is(err, ErrPooled):
jsonError(w, http.StatusConflict, fmt.Sprintf(
- "%s runs in the pool %s, so its data is on the pool's own server. carlos db cannot read it.", host, rec.Pool))
+ "%s runs in the pool %s, so its data is on the pool's own server. carlos db cannot read it.", host, src.rec.Pool))
return dbRequest{}, false
case errors.Is(err, ErrResidency):
- jsonError(w, http.StatusConflict, s.dbResidencyRefusal(*rec))
- return dbRequest{}, false
- case err != nil:
- log.Printf("console: api db %s/%s/%s: resolve: %v", acct.Sqid, app, host, err)
- jsonError(w, http.StatusBadGateway, "could not read the replica. Try again.")
+ jsonError(w, http.StatusConflict, s.dbResidencyRefusal(src.rec))
return dbRequest{}, false
- }
- if s.cfg.HostClaim {
+ case errors.Is(err, errSourceClaim):
g := dbRequest{acct: acct, actor: actor, app: app, host: host}
if s.cfg.DBVersions == nil {
jsonError(w, http.StatusServiceUnavailable, "this console has no versioned database reader")
return dbRequest{}, false
}
- g.claim, err = s.dbClaim(ctx, g, *rec)
- if err != nil {
- s.dbClaimFailure(w, r, g, err)
- return dbRequest{}, false
- }
- g.prefix = g.claim.Claim.Holder.Location
- return g, true
+ s.dbClaimFailure(w, r, g, src.claimErr)
+ return dbRequest{}, false
+ case errors.Is(err, errSourceResolve):
+ log.Printf("console: api db %s/%s/%s: resolve: %v", acct.Sqid, app, host, err)
+ jsonError(w, http.StatusBadGateway, "could not read the replica. Try again.")
+ return dbRequest{}, false
+ case err != nil:
+ log.Printf("console: api db %s/%s/%s: read record: %v", acct.Sqid, app, host, err)
+ jsonError(w, http.StatusBadGateway, "could not read the instance. Try again.")
+ return dbRequest{}, false
+ }
+ if s.cfg.HostClaim {
+ return dbRequest{claim: src.claim, acct: acct, actor: actor, app: app, host: host, prefix: src.prefix}, true
}
- rd, err := s.dbReplica(ctx, prefix)
+ rd, err := s.dbReplica(ctx, src.prefix)
if err != nil {
log.Printf("console: api db %s/%s/%s: replica client: %v", acct.Sqid, app, host, err)
jsonError(w, http.StatusBadGateway, "could not read the replica. Try again.")
return dbRequest{}, false
}
- return dbRequest{acct: acct, actor: actor, app: app, host: host, prefix: prefix, rd: rd}, true
+ return dbRequest{acct: acct, actor: actor, app: app, host: host, prefix: src.prefix, rd: rd}, true
+}
+
+var (
+ errSourceClass = errors.New("db: not an instance the platform keeps a replica of")
+ errSourceClaim = errors.New("db: the instance's claim could not be read")
+ // errSourceResolve marks a resolveReplica failure that is none of the
+ // named refusals, so the db door keeps its own log line and answer for
+ // it apart from a failed record read.
+ errSourceResolve = errors.New("db: the replica could not be resolved")
+)
+
+// dbSource is a replica source both the db door and the --from door may
+// read (seeded-instances spec §4, codex 17): a record of this account and
+// app, an instance with a replica of its own, homed in this console's
+// region, and, with claims on, its versioned claim. claimErr is the claim
+// read's own error, set beside the errSourceClaim wrap so a caller can hand
+// it to dbClaimFailure unwrapped.
+type dbSource struct {
+ rec provision.Record
+ prefix string
+ claim hostclaim.Observed
+ claimErr error
+}
+
+// resolveDBSource holds the source checks dbGate made inline. It does no
+// authorization: dbGate keeps its person-only gate, and the --from door
+// authorizes through cliRoute's instance role.
+func (s *Server) resolveDBSource(ctx context.Context, acct *Account, actor, app, host string) (dbSource, error) {
+ if !provision.ValidHostShape(host) {
+ return dbSource{}, fmt.Errorf("%w: %q", ErrBadHost, host)
+ }
+ rec, err := provision.GetRecord(ctx, s.cfg.Releases, acct.Sqid, app, host)
+ if err != nil {
+ return dbSource{}, err // os.ErrNotExist passes through as itself
+ }
+ if msg := dbClassRefusal(*rec); msg != "" {
+ return dbSource{rec: *rec}, fmt.Errorf("%w: %s", errSourceClass, msg)
+ }
+ prefix, err := s.resolveReplica(*rec, host)
+ if err != nil {
+ switch {
+ case errors.Is(err, ErrBadHost), errors.Is(err, ErrPooled), errors.Is(err, ErrResidency):
+ return dbSource{rec: *rec}, err
+ }
+ return dbSource{rec: *rec}, fmt.Errorf("%w: %w", errSourceResolve, err)
+ }
+ src := dbSource{rec: *rec, prefix: prefix}
+ if s.cfg.HostClaim {
+ if s.cfg.DBVersions == nil {
+ return src, fmt.Errorf("%w: this console has no versioned database reader", errSourceClaim)
+ }
+ g := dbRequest{acct: acct, actor: actor, app: app, host: host}
+ if src.claim, err = s.dbClaim(ctx, g, *rec); err != nil {
+ src.claimErr = err
+ return src, fmt.Errorf("%w: %w", errSourceClaim, err)
+ }
+ src.prefix = src.claim.Claim.Holder.Location
+ }
+ return src, nil
}
// dbClassRefusal is the class rail as a sentence, or "" for an
diff --git a/internal/console/apidb_source_test.go b/internal/console/apidb_source_test.go
new file mode 100644
index 000000000..f07450037
--- /dev/null
+++ b/internal/console/apidb_source_test.go
@@ -0,0 +1,94 @@
+package console
+
+import (
+ "context"
+ "errors"
+ "net/http"
+ "os"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/carlosframework/platform/internal/dbdoor"
+ "github.com/carlosframework/platform/internal/lease/leasetest"
+ "github.com/carlosframework/platform/internal/provision"
+)
+
+func TestResolveDBSource(t *testing.T) {
+ fx, _ := instanceFixture(t, "code-db-source")
+ if _, err := fx.st.CreateAppIn(OpsAccountID, "shop", "paul@keymail.dev", time.Now()); err != nil {
+ t.Fatal(err)
+ }
+ m := provision.Manifest{Schema: 1, App: "shop", Domain: "shop.example.com",
+ Channel: "stable", Regions: []string{"ie"}, Seed: []string{"seed"}}
+ enableInstances(t, fx, "shop", m)
+ fx.srv.cfg.Region = "ie"
+ fx.srv.cfg.HibernatePrefix = "instances"
+ acct, err := fx.st.AccountBySqid("bab")
+ if err != nil {
+ t.Fatal(err)
+ }
+ ctx := context.Background()
+ put := func(host string, mut func(*provision.Record)) {
+ rec := provision.Record{Schema: 1, Host: host, Slug: strings.Split(host, ".")[0], App: "shop", Account: "bab",
+ Region: "ie", Channel: "stable", CreatedAt: time.Now().UTC()}
+ if mut != nil {
+ mut(&rec)
+ }
+ if err := provision.CreateRecord(ctx, fx.rel, rec); err != nil {
+ t.Fatal(err)
+ }
+ }
+ put("acme.shop.example.com", nil)
+ put("pooled.shop.example.com", func(r *provision.Record) { r.Pool = "shared" })
+ put("far.shop.example.com", func(r *provision.Record) { r.Region = "uk" })
+ put("addr.shop.example.com", func(r *provision.Record) { r.Addr = "127.0.0.1:9000" })
+
+ src, err := fx.srv.resolveDBSource(ctx, acct, "alice@keymail.dev", "shop", "acme.shop.example.com")
+ if err != nil || src.prefix != "instances/acme.shop.example.com" || src.rec.Host != "acme.shop.example.com" {
+ t.Fatalf("acme = %+v, %v", src, err)
+ }
+ for host, want := range map[string]error{
+ "nope.shop.example.com": os.ErrNotExist,
+ "pooled.shop.example.com": ErrPooled,
+ "far.shop.example.com": ErrResidency,
+ "addr.shop.example.com": errSourceClass,
+ "not a host": ErrBadHost,
+ } {
+ if _, err := fx.srv.resolveDBSource(ctx, acct, "alice@keymail.dev", "shop", host); !errors.Is(err, want) {
+ t.Errorf("%s: err = %v, want %v", host, err, want)
+ }
+ }
+}
+
+// A claim-read failure is wrapped in errSourceClaim and also kept whole on
+// dbSource.claimErr: errors.Unwrap of a two-%w wrap is nil, and
+// dbClaimFailure needs the original to pick its answer.
+func TestResolveDBSourceClaimReadError(t *testing.T) {
+ fx := dbFixture(t)
+ fx.srv.cfg.HomeRegion = "ie"
+ fx.srv.cfg.HostClaim = true
+ fx.srv.cfg.DBVersions = leasetest.NewVersionedBucket()
+ acct, err := fx.st.AccountBySqid("bab")
+ if err != nil {
+ t.Fatal(err)
+ }
+ app := titoManifest().App
+ src, err := fx.srv.resolveDBSource(t.Context(), acct, "alice@keymail.dev", app, dbTestHost)
+ if !errors.Is(err, errSourceClaim) {
+ t.Fatalf("err = %v, want errSourceClaim", err)
+ }
+ if src.claimErr == nil || !errors.Is(src.claimErr, errDBNotAdopted) {
+ t.Fatalf("claimErr = %v, want errDBNotAdopted", src.claimErr)
+ }
+ if errors.Unwrap(err) != nil {
+ t.Fatalf("errors.Unwrap(err) = %v, expected nil for a two-%%w wrap", errors.Unwrap(err))
+ }
+
+ // Through the door: the original error reaches dbClaimFailure, which
+ // answers a missing adoption with a 409. A nil error would be a 502.
+ status, out := getRaw(t, fx.url(dbdoor.ListPath("bab", app, dbTestHost, 0)), fx.member)
+ if status != http.StatusConflict || !strings.Contains(string(out), errDBNotAdopted.Error()) {
+ t.Fatalf("door answered %d %q, want 409 with %q", status, out, errDBNotAdopted)
+ }
+}