package hostops import ( "context" "crypto/rand" "encoding/hex" "encoding/json" "errors" "os" "github.com/carlosframework/platform/internal/release" ) // BackfillAppGrant is the only legacy-app initialization proof. Its fields // are unexported: console code cannot fabricate one. type BackfillAppGrant struct { account, app, digest string } func (a ProductionAuthority) BackfillAppGrant(ctx context.Context, account, app string) (BackfillAppGrant, error) { digest, err := a.AuthorizeBackfillApp(ctx, account, app) if err != nil { return BackfillAppGrant{}, err } if digest == "" { return BackfillAppGrant{}, ErrIdentity } return BackfillAppGrant{account: account, app: app, digest: digest}, nil } func newAppID() (string, error) { var nonce [16]byte if _, err := rand.Read(nonce[:]); err != nil { return "", err } return hex.EncodeToString(nonce[:]), nil } func freshAdmission(appID string) AppAdmission { return AppAdmission{V: 1, AppID: appID, Revision: 1, Epoch: 1, State: "open", Registrations: map[string]Registration{}, Effects: map[string]Effect{}} } // nameFree is the recreation census: under the app prefix only the retained // admission object may remain, after discarding a box request that names // the retired incarnation. func (w *ControlWriter) nameFree(ctx context.Context, account, app string, completed bool) (bool, error) { prefix := "apps/" + account + "/" + app + "/" keys, err := w.raw.List(ctx, prefix) if err != nil { return false, err } requestKey := prefix + "purge-request.json" for _, key := range keys { if key == requestKey { removed, err := w.DiscardPurgeRequest(ctx, account, app) if err != nil || !removed { return false, err } if keys, err = w.raw.List(ctx, prefix); err != nil { return false, err } break } } if !completed { return len(keys) == 0, nil } return len(keys) == 1 && keys[0] == prefix+"admission.json", nil } // EnrollNewApp reserves the canonical identity for a name this console just // won in SQL. Absent: first incarnation. Completed and name-free: the // dedicated recreation transition (new app_id, epoch+1, last_purge kept). // Open: another console or an ambiguous earlier write already enrolled it. func (w *ControlWriter) EnrollNewApp(ctx context.Context, account, app string) (string, error) { if err := w.ready(); err != nil { return "", err } key, err := AdmissionKey(account, app) if err != nil { return "", err } existing, err := ReadCanonicalAdmission(ctx, w.raw, account, app) absent := errors.Is(err, os.ErrNotExist) if err != nil && !absent { return "", err } if !absent && !CompletedAdmission(existing.Value) { if existing.Value.State != "open" || existing.Value.Op != nil { return "", ErrPending } return existing.Value.AppID, nil } free, err := w.nameFree(ctx, account, app, !absent) if err != nil { return "", err } if !free { return "", ErrPending } appID, err := newAppID() if err != nil { return "", err } next, tag := freshAdmission(appID), "" if !absent { if existing.Value.Revision == ^uint64(0) || existing.Value.Epoch == ^uint64(0) { return "", ErrPending } next.Revision, next.Epoch = existing.Value.Revision+1, existing.Value.Epoch+1 next.LastPurge, tag = existing.Value.LastPurge, existing.ETag } body, err := json.Marshal(next) if err != nil { return "", err } if _, err = w.raw.Put(ctx, key, body, tag); err != nil { if !errors.Is(err, release.ErrCASConflict) { return "", err } winner, rerr := ReadCanonicalAdmission(ctx, w.raw, account, app) if rerr != nil { return "", rerr } if winner.Value.State != "open" || winner.Value.Op != nil { return "", ErrPending } return winner.Value.AppID, nil } return appID, nil } // EnrollBackfillApp initializes a legacy live app from a signed backfill // grant. It never revives a completed incarnation. func (w *ControlWriter) EnrollBackfillApp(ctx context.Context, g BackfillAppGrant) (string, error) { if err := w.ready(); err != nil { return "", err } if g.digest == "" { return "", ErrIdentity } key, err := AdmissionKey(g.account, g.app) if err != nil { return "", err } existing, err := ReadCanonicalAdmission(ctx, w.raw, g.account, g.app) if err == nil { if CompletedAdmission(existing.Value) || existing.Value.State != "open" || existing.Value.Op != nil { return "", ErrPending } return existing.Value.AppID, nil } if !errors.Is(err, os.ErrNotExist) { return "", err } appID, err := newAppID() if err != nil { return "", err } body, err := json.Marshal(freshAdmission(appID)) if err != nil { return "", err } if _, err = w.raw.Put(ctx, key, body, ""); err != nil { if !errors.Is(err, release.ErrCASConflict) { return "", err } winner, rerr := ReadCanonicalAdmission(ctx, w.raw, g.account, g.app) if rerr != nil { return "", rerr } if winner.Value.State != "open" || winner.Value.Op != nil { return "", ErrPending } return winner.Value.AppID, nil } return appID, nil }