carlos / platform Public

console: attach core — alias-branch side effects pinned, idempotent adoption narrowed

Re-review round, two items:

- The alias attach branch had no test for its audit row or serial bump;
  both attach branches and detach now pin them.
- The idempotent record-conflict branch accepted any readable record at the
  key. An app with a custom manifest Domain can have carlos instances
  create write an INSTANCE record there, and adopting one would let a later
  detach retire a tenant instance this door never created. Adoption is now
  limited to the fixed shape a first-route attach writes (static, no slug,
  shared pool, unconstrained region); anything else keeps the 422 and the
  claim rollback, with the record untouched.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Paul Campbell 5d4bb5d6e694e21a5842e99c40a6a793879ea349 parent c6cb4f6
2 files changed, +63 −6
  • internal/console/domainattach.go +20 −6
  • internal/console/domainattach_test.go +43 −0
diff --git a/internal/console/domainattach.go b/internal/console/domainattach.go
index 5249a5c91..7807bcf1a 100644
--- a/internal/console/domainattach.go
+++ b/internal/console/domainattach.go
@@ -127,16 +127,17 @@ func (s *Server) attachDomain(ctx context.Context, acct *Account, app, host, kin
// not the one this call would have written.
if errors.Is(err, release.ErrCASConflict) {
existing, gerr := provision.GetRecord(ctx, s.cfg.Releases, acct.Sqid, app, host)
- if gerr == nil {
+ if gerr == nil && attachWrote(*existing) {
s.audit(acct, app, principal, "domain-attach", host+" (first route, "+existing.Channel+", record already present)")
s.bumpSerial(ctx)
return attachResult{Claim: claim, FirstRoute: true, Channel: existing.Channel}, nil
}
- // Unreadable: malformed, or a body that lies about its identity.
- // Not ours to delete and not safe to write over, so the claim
- // goes back and the member is told what stands in the way — the
- // fact of a record, and nothing about who wrote it.
- log.Printf("console: attach %s: existing record unreadable: %v", host, gerr)
+ // Unreadable (malformed, or a body that lies about its
+ // identity) or written by a door other than this one. Not ours
+ // to delete and not safe to write over, so the claim goes back
+ // and the member is told what stands in the way — the fact of a
+ // record, and nothing about who wrote it.
+ log.Printf("console: attach %s: existing record is not this door's to adopt (read: %v)", host, gerr)
s.rollbackAttach(ctx, acct.Sqid, app, host, false)
return attachResult{}, &doorRefusal{http.StatusUnprocessableEntity,
"a route record already exists for this host — detach it or pick another hostname"}
@@ -315,6 +316,19 @@ func (s *Server) attachChannel(ctx context.Context, sqid, app, channelFlag, self
") — say which one this domain serves"}
}
+// attachWrote reports whether rec is a record THIS door could have written:
+// the fixed shape of a first-route attach — kind static, no slug, and the
+// shared pool and unconstrained region (§3). It is the guard on the
+// idempotent conflict branch, and it is narrow on purpose. The record key
+// is host-scoped within an app, and an app with a custom manifest Domain
+// can have `carlos instances create` write an INSTANCE record at the very
+// same key; adopting one of those as "an attach that already happened"
+// would hand detach the authority to retire a tenant instance this door
+// never created. Anything that fails this test is left alone and refused.
+func attachWrote(rec provision.Record) bool {
+ return rec.Kind == "static" && rec.Slug == "" && rec.Region == "" && rec.Pool == ""
+}
+
// bucketRefusal is the "this console cannot answer at all" check both verbs
// open with: a deployment with no bucket has nowhere to put a claim or a
// record. Same 503 and the same sentence the instance API's gate uses
diff --git a/internal/console/domainattach_test.go b/internal/console/domainattach_test.go
index 5e3c8bc93..a9ab27bcd 100644
--- a/internal/console/domainattach_test.go
+++ b/internal/console/domainattach_test.go
@@ -155,6 +155,8 @@ func TestAttachAliasWhenAppHasReadyRoute(t *testing.T) {
Account: fx.sqid, Channel: "canary/live", Status: "ready"},
)
const host = "story.carlosframework.com"
+ ops := &opStore{Store: fx.rel}
+ fx.srv.cfg.Releases = ops
res, ref := fx.srv.attachDomain(context.Background(), acct, "story", host, "", "", "paul@keymail.dev")
if ref != nil {
t.Fatalf("attach: refused %+v", ref)
@@ -183,6 +185,14 @@ func TestAttachAliasWhenAppHasReadyRoute(t *testing.T) {
if len(keys) != 0 {
t.Errorf("records after an alias attach = %v, want none (the alias is derived box-side)", keys)
}
+ // The alias branch owes the same two side effects the first-route
+ // branch does: both doors get them from here, or neither does.
+ auditedOnce(t, fx, acct, "story", "domain-attach", host)
+ claimPut := ops.opIndex("put " + domainclaim.Key(host))
+ serial := ops.opIndex("put " + release.SerialKey)
+ if claimPut < 0 || serial < claimPut {
+ t.Errorf("ops %v: want the serial bumped after the claim it announces (claim=%d serial=%d)", ops.ops, claimPut, serial)
+ }
}
// TestAttachNeverAliasesAHostToItself is the operator-added custom domain
@@ -364,6 +374,39 @@ func TestAttachUnreadableExistingRecordRefuses422(t *testing.T) {
claimAbsent(t, fx, host)
}
+// TestAttachInstanceRecordAtTheKeyIsNotAdopted is the narrow half of the
+// idempotent branch: an app with a custom manifest Domain can have `carlos
+// instances create` write an INSTANCE record at this very key. Treating one
+// of those as "an attach that already happened" would hand detach the
+// authority to retire a tenant instance this door never created.
+func TestAttachInstanceRecordAtTheKeyIsNotAdopted(t *testing.T) {
+ fx, acct := attachFixture(t)
+ shipVersion(t, fx, "story", "v1")
+ seedChannel(t, fx, "story", "canary/live", "v1")
+ const host = "story.carlosframework.com"
+ // Kind "" is an instance record — what reconcile minted before Kind
+ // existed, and what the instance door writes today.
+ instance := provision.Record{
+ Schema: 1, Host: host, App: "story", Account: fx.sqid,
+ Slug: "story", Channel: "stable",
+ }
+ if err := provision.CreateRecord(context.Background(), fx.rel, instance); err != nil {
+ t.Fatal(err)
+ }
+ _, ref := fx.srv.attachDomain(context.Background(), acct, "story", host, "", "", "paul@keymail.dev")
+ if ref == nil || ref.Status != 422 {
+ t.Fatalf("refusal = %+v, want 422 — an instance record is not an attach to adopt", ref)
+ }
+ claimAbsent(t, fx, host)
+ got, err := provision.GetRecord(context.Background(), fx.rel, fx.sqid, "story", host)
+ if err != nil {
+ t.Fatalf("record: %v, want the instance record untouched", err)
+ }
+ if got.Kind != "" || got.Slug != "story" || got.Channel != "stable" {
+ t.Errorf("record = %+v, want the instance record exactly as it was", *got)
+ }
+}
+
// TestAttachAmbiguousRecordFailureUnwindsBoth: a write that LANDS and then
// reports failure (a timeout) must not leave the record behind the claim it
// rolls back — that record would materialize a route for a host anyone may