package console import ( "errors" "net/http" "github.com/carlosframework/platform/internal/hostclaim" "github.com/carlosframework/platform/internal/hostops" "github.com/carlosframework/platform/internal/provision" ) // claimRetireAccepted reports a delete whose exclusive operation this // console owns and has made durable, but which is still waiting (draining, // box acknowledgements, parking). Recovery or a retry of the same delete // carries it on. It unwraps to hostops.ErrPending, so a caller that only // checks for failure stays conservative. type claimRetireAccepted struct { operation, phase, host string } func (a *claimRetireAccepted) Error() string { return "console: delete of " + a.host + " accepted, operation " + a.operation + " is " + a.phase } func (a *claimRetireAccepted) Unwrap() error { return hostops.ErrPending } // claimRetireWaiting turns a pending answer from a step of an owned delete // into claimRetireAccepted. Every other error, and nil, passes through. func claimRetireWaiting(h *hostops.OperationHandle, phase, host string, err error) error { if err == nil { return nil } var accepted *claimRetireAccepted if errors.As(err, &accepted) { return err } if h != nil && errors.Is(err, hostops.ErrPending) { return &claimRetireAccepted{operation: h.ID(), phase: phase, host: host} } return err } func claimRetireRecordKey(ref provision.RetireRef) (string, error) { switch ref.Family { case "instance": return provision.RecordKey(ref.Account, ref.App, ref.Host), nil case "sidecar": if ref.Name == "" || ref.Host != provision.SidecarHost(ref.Name, ref.App, ref.Account) { return "", provision.ErrClaimRetirer } return provision.SidecarRecordKey(ref.Account, ref.App, ref.Name), nil default: return "", provision.ErrClaimRetirer } } // claimRetireHolderIs: the claim's holder is exactly the record this delete // names (and, when the caller pinned one, that incarnation). func claimRetireHolderIs(h *hostclaim.Holder, ref provision.RetireRef, key string) bool { return h != nil && h.Key == key && h.Family == ref.Family && h.Account == ref.Account && h.App == ref.App && (ref.Incarnation == "" || h.Incarnation == ref.Incarnation) } // claimRetireOutcome answers a delete door's retire error. It reports // whether it wrote the response: // // - an owned, durable delete still in progress is 202 with the operation // ID and phase (the caller audits the request first); // - a delete refused before any effect because another exclusive // operation holds the app (or the host is not ready for this console) // is 409; // - anything else is left to the caller. func claimRetireOutcome(w http.ResponseWriter, err error) bool { var accepted *claimRetireAccepted if errors.As(err, &accepted) { w.Header().Set("Retry-After", "2") writeJSON(w, http.StatusAccepted, map[string]string{"result": "accepted", "operation_id": accepted.operation, "phase": accepted.phase, "host": accepted.host}) return true } if errors.Is(err, hostops.ErrPending) { w.Header().Set("Retry-After", "2") jsonError(w, http.StatusConflict, claimRetireBusyMsg) return true } return false } // claimRetireBusyMsg is the 409 a delete meets when another operation // holds the app or the host. const claimRetireBusyMsg = "another operation is in progress on this app; try again shortly" // claimRetireAcceptedOf returns the accepted delete, if err is one. func claimRetireAcceptedOf(err error) (*claimRetireAccepted, bool) { var accepted *claimRetireAccepted if errors.As(err, &accepted) { return accepted, true } return nil, false } // Explicit host-claim refusals of intentionally unsupported doors. Each // answers 409 with its own message before any registration or effect. // // Rename and account transfer are refused for EVERY app on a host-claim // console, with or without provisioning records. Both rewrite an app's // identity: its SQL rows (apps.app / account_id and every row keyed by the // name), its bucket prefixes and its admission object, which is keyed by // account and name. Doing that safely needs one operation that closes the // old incarnation's admission and creates the new name's under the same // gate, atomically, with no request admitted against either name in // between. That protocol is not built, so neither door may run, whatever // the app holds. // // Instance moves (request, cut, abort) rewrite the record's move journal, // which only the unbuilt host-claim Moves protocol may do under its own // operation. const ( claimRenameUnsupportedMsg = "renaming an app is not supported on this console yet: it needs a host-claim operation that gates both the old and the new name" claimTransferUnsupportedMsg = "moving an app to another account is not supported on this console yet: it needs a host-claim operation that gates the app under both accounts" claimMoveUnsupportedMsg = "instance moves are not available on this console yet: they need the host-claim Moves protocol" )