package console import ( "context" "errors" "log" "net/http" "time" "github.com/carlosframework/platform/internal/objstore" ) // Provision spends the account approval on one app. Grant owns retry and // repair: a grant block without provisioning must still run the walk. func (s *Server) handleAPIStoreProvision(w http.ResponseWriter, r *http.Request) { actor, acct, app, ok := s.apiAccountApp(w, r) if !ok { return } if s.refuseTemporary(w, acct, app, "give it object storage") { return } if s.cfg.Releases == nil { storeNoBucket(w) return } preCtx, preCancel := context.WithTimeout(r.Context(), storeTimeout) defer preCancel() if _, ok := s.storeProvisionApproval(preCtx, w, acct); !ok { return } p, _ := principalFrom(r.Context()) if !s.storeWalkGates(w, p) { return } pub, ok := s.storeSealKey(w) if !ok { return } var req struct{} if !readJSON(w, r, &req) { return } env, ok := s.storeEnvParam(w, r) if !ok { return } defer s.storeWalks.lock(acct.Sqid, app, env)() // The walk's deadline starts HERE rather than at the top of the handler. // This is the door that queues in ordinary use, and a request that waited // out a full-length walk ahead of it would otherwise reach the lock // holding a spent context and fail its first Get for no reason at all. ctx, cancel := context.WithTimeout(r.Context(), storeGrantTimeout) defer cancel() // A queued request may have waited while an operator revoked approval. // An already-running walk completes; decline does not tear down estate. e, ok := s.storeProvisionApproval(ctx, w, acct) if !ok { return } rec, err := claimAppCall(s, ctx, acct, app, "store-provision", func(ctx context.Context) (*objstore.Record, error) { return objstore.GrantIn(ctx, s.cfg.Releases, s.cfg.StoreProvisioner, s.cfg.StoreDeployment, pub, acct.Sqid, app, env, e.Decision.By, objstore.GrantSourceAccountApproval, "", time.Now()) }) switch { case errors.Is(err, objstore.ErrNotDeclared): jsonError(w, http.StatusConflict, "no store is declared for this app. run `carlos store create` first") return case errors.Is(err, objstore.ErrStoreNameTaken): log.Printf("console: api store provision %s/%s: %v", acct.Sqid, app, err) jsonError(w, http.StatusConflict, storeNameTakenMsg) return case err != nil: log.Printf("console: api store provision %s/%s: %v", acct.Sqid, app, err) jsonError(w, http.StatusBadGateway, "could not provision the store. re-run `carlos store create` to resume; after destroying a store, its bucket name may take time to become available again") return } s.audit(acct, app, actor, "store-provision", "provisioned a store in bucket "+storeBucketName(rec)+storeEnvSuffix(env)+" (cli)") s.bumpSerial(ctx) s.storeWalkReply(ctx, w, acct, app, env, rec) } func (s *Server) storeProvisionApproval(ctx context.Context, w http.ResponseWriter, acct *Account) (*objstore.Entitlement, bool) { e, _, err := objstore.GetEntitlement(ctx, s.cfg.Releases, acct.Sqid) if err != nil { log.Printf("console: api store provision %s: read entitlement: %v", acct.Sqid, err) jsonError(w, http.StatusBadGateway, "could not read the storage application. try again") return nil, false } if !e.Approved() { jsonError(w, http.StatusForbidden, "this account is not approved for object storage. run `carlos store apply` to ask for it") return nil, false } return e, true }