package provision // The replica provisioning surface (#162, edge checkout). The instance // record family's own verb, exactly as the discussion's Ask 2 frames it: a // replica is the sandbox verb with a different role and a chosen placement — // same app, same account, role=replica, placed on the box that carries the // placement code. // // The record the console's create door writes (apiinstances.go) and the // record a home's own code would write share ONE constructor, Replica, so // "what a replica record looks like" has exactly one answer: everything // copied from the home record's policy half (app, account, pool, channel), // Region = the replica's placement, Role = "replica", and ValidateShape run // over the result — the same rails the door applies, because neither side // trusts the other about what is in the bucket. // // The routing half of #162 lives in the reconciler and the edges file; the // lease/drain protocol that runs BETWEEN home and replica is titogo's own. import ( "context" "errors" "time" "github.com/carlosframework/platform/internal/release" ) // ErrReplicaOfReplica and ErrReplicaIncompleteHome are Replica's refusals: // a replica of a replica is not a shape the platform carries, and a home // link to an incomplete record would mint a replica whose account or app is // nothing the home ever was. var ( ErrReplicaOfReplica = errors.New("provision: record is itself a replica — a replica of a replica is not a shape the platform carries") ErrReplicaIncompleteHome = errors.New("provision: home record is not a complete instance record (host/app/account)") ) // Replica turns a home-shaped record into the replica record // CreateRecordReplica writes, validating the result as it goes. host is the // replica's own host (one label + the app's instance domain — the same shape // a home claims), placement is the replica's placement code (ValidRegion's // vocabulary, the same one the create door validates as armed), home is the // home record as read from the bucket. The returned record has passed // ValidateShape — the proof ReplicaRecord's type carries. func Replica(home Record, host, placement string) (ReplicaRecord, error) { if home.Role == "replica" { return ReplicaRecord{}, ErrReplicaOfReplica } if home.Host == "" || home.App == "" || home.Account == "" { return ReplicaRecord{}, ErrReplicaIncompleteHome } rec := Record{ Schema: 1, Host: host, Slug: host, App: home.App, Account: home.Account, Pool: home.Pool, Region: placement, Channel: home.Channel, CreatedAt: time.Now().UTC(), Role: "replica", } if err := ValidateShape(rec); err != nil { return ReplicaRecord{}, err } return ReplicaRecord{rec}, nil } // ReplicaRecord is a Record that has passed the replica rails: ValidateShape // (which refuses a replica role on anything but a socket-backed instance) // has seen this exact value. Constructors return it; nothing else may build // one, so a writer that wants to stamp the role onto a record must go // through the same validation the door applies. type ReplicaRecord struct { Record } // CreateRecordReplica writes the replica record — create-only, the // CreateRecord contract (a lost race is release.ErrCASConflict, never a // silent overwrite). The record MUST have come from Replica: the type is // the proof ValidateShape's replica rails have already run, and this writer // re-checks nothing. func CreateRecordReplica(ctx context.Context, s release.Store, rec ReplicaRecord) error { return CreateRecord(ctx, s, rec.Record) } // ListRecordReplicas returns an app's records that ARE replicas — the // instance-API list the home instance walks to find whom it leases to, // addressable by Host + Region. Records are listed through ListRecords (the // same capped, path-validated fold the instance list uses) and filtered to // role=replica, so a home instance can never mistake a sibling home for its // own replica even before it checks the role on the wire view. func ListRecordReplicas(ctx context.Context, s release.Store, acct, app string, max int) ([]Record, error) { recs, _, err := ListRecords(ctx, s, acct, app, max) if err != nil { return nil, err } var out []Record for _, rec := range recs { if rec.Role == "replica" { out = append(out, rec) } } return out, nil }