package hostops import ( "context" "encoding/json" "errors" "os" "time" "github.com/carlosframework/platform/internal/domains" "github.com/carlosframework/platform/internal/hostclaim" "github.com/carlosframework/platform/internal/provision" "github.com/carlosframework/platform/internal/release" ) type productionCreateAuthority interface { StartCreate(context.Context, hostclaim.Observed, Record, string, string) (hostclaim.Observed, error) RecordEffects(context.Context, hostclaim.Observed, Record) error } // RecordEffects writes the ownership marker and the record as two effects // under the request's own registration. The runner comes from the // StartHandle the console put in ctx; without it (recovery, another // process) this refuses rather than impersonating the registration owner. func (a ProductionAuthority) RecordEffects(ctx context.Context, obs hostclaim.Observed, r Record) error { if obs.Absent || obs.Claim.Holder == nil || obs.Claim.Op.Kind != hostclaim.KindCreating || obs.Claim.Op.Phase == "cancelling" || !matches(r, obs.Claim.Holder) { return ErrIdentity } h, op := obs.Claim.Holder, obs.Claim.Op if h.Acquisition == "" || op.Intent["ownership_effect"] == "" || op.Intent["record_effect"] == "" { return ErrContract } appID := op.Intent["app_id"] start, ok := startHandleFor(ctx, h.Account, h.App, appID, op.ID) if !ok || start.owner.Console != a.ConsoleID || start.owner.Enrollment != a.Enrollment { return ErrPending } adm, err := ReadAdmission(ctx, a.Store, h.Account, h.App, appID) if err != nil { return err } reg, ok := adm.Value.Registrations[op.ID] if !ok || reg.Owner != start.owner || !RegistrationLive(reg, a.Now()) { return ErrPending } runner := start.Runner(a.Now) marker := domains.Claim{Schema: 1, Host: obs.Host, Account: h.Account, App: h.App, CreatedAt: h.CreatedAt.UTC().Format(time.RFC3339Nano), CreatedBy: op.By, Acquisition: h.Acquisition, RecordIncarnation: h.Incarnation, OwnershipState: "active"} body, err := json.Marshal(marker) if err != nil { return err } key := domains.ClaimKey(obs.Host) expectedTag := "" previous, tag, readErr := a.Store.Get(ctx, key) if readErr != nil && !errors.Is(readErr, os.ErrNotExist) { return readErr } if readErr == nil { var prior domains.Claim if json.Unmarshal(previous, &prior) != nil || prior.Host != obs.Host || prior.Account != h.Account || prior.App != h.App { return ErrIdentity } if prior.Acquisition == "" && prior.RecordIncarnation == "" { if prior.Pending && op.Intent["ownership_verified"] != "true" { return ErrIdentity } // Existing same-owner TXT reservations become the same ownership // record; never delete/recreate the marker or discard capabilities. prior.Acquisition, prior.RecordIncarnation = h.Acquisition, h.Incarnation prior.OwnershipState, prior.Pending, prior.PendingUntil = "active", false, "" if body, err = json.Marshal(prior); err != nil { return err } expectedTag = tag } else if prior.Acquisition == h.Acquisition && prior.RecordIncarnation == h.Incarnation && prior.OwnershipState == "active" { body, expectedTag = previous, tag } else { return ErrIdentity } } _, err = runner.Run(ctx, Effect{ID: op.Intent["ownership_effect"], Resource: key, Action: "ownership-put", Inputs: map[string]string{"body": string(body), "expected_etag": expectedTag}}, func(ctx context.Context) (string, error) { etag, err := a.Store.Put(ctx, key, body, expectedTag) if !errors.Is(err, release.ErrCASConflict) { return etag, err } raw, etag, err := a.Store.Get(ctx, key) if err != nil { return "", err } var got domains.Claim if err = json.Unmarshal(raw, &got); err != nil { return "", err } if got.Host != obs.Host || got.Account != h.Account || got.App != h.App || got.Acquisition != h.Acquisition || got.RecordIncarnation != h.Incarnation || got.OwnershipState != "active" { return "", ErrIdentity } return etag, nil }) if err != nil { return err } _, err = runner.Run(ctx, Effect{ID: op.Intent["record_effect"], Resource: h.Key, Action: "record-put", Inputs: map[string]string{"body": string(r.Body)}}, func(ctx context.Context) (string, error) { etag, _, err := provision.WriteRecord(provision.WithRecordSequence(ctx), a.Store, h.Key, []byte(`{}`), r.Body, "") if !errors.Is(err, release.ErrCASConflict) { return etag, err } existing, err := ReadRecord(ctx, a.Store, h.Key) if err != nil { return "", err } if !matches(existing, h) { return "", ErrIdentity } return existing.ETag, nil }) return err }