carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package console
2
3import (
4 "context"
5 "errors"
6 "log"
7 "net/http"
8 "time"
9
10 "github.com/carlosframework/platform/internal/objstore"
11)
12
13// Provision spends the account approval on one app. Grant owns retry and
14// repair: a grant block without provisioning must still run the walk.
15func (s *Server) handleAPIStoreProvision(w http.ResponseWriter, r *http.Request) {
16 actor, acct, app, ok := s.apiAccountApp(w, r)
17 if !ok {
18 return
19 }
20 if s.refuseTemporary(w, acct, app, "give it object storage") {
21 return
22 }
23 if s.cfg.Releases == nil {
24 storeNoBucket(w)
25 return
26 }
27 preCtx, preCancel := context.WithTimeout(r.Context(), storeTimeout)
28 defer preCancel()
29 if _, ok := s.storeProvisionApproval(preCtx, w, acct); !ok {
30 return
31 }
32 p, _ := principalFrom(r.Context())
33 if !s.storeWalkGates(w, p) {
34 return
35 }
36 pub, ok := s.storeSealKey(w)
37 if !ok {
38 return
39 }
40 var req struct{}
41 if !readJSON(w, r, &req) {
42 return
43 }
44 env, ok := s.storeEnvParam(w, r)
45 if !ok {
46 return
47 }
48 defer s.storeWalks.lock(acct.Sqid, app, env)()
49 // The walk's deadline starts HERE rather than at the top of the handler.
50 // This is the door that queues in ordinary use, and a request that waited
51 // out a full-length walk ahead of it would otherwise reach the lock
52 // holding a spent context and fail its first Get for no reason at all.
53 ctx, cancel := context.WithTimeout(r.Context(), storeGrantTimeout)
54 defer cancel()
55 // A queued request may have waited while an operator revoked approval.
56 // An already-running walk completes; decline does not tear down estate.
57 e, ok := s.storeProvisionApproval(ctx, w, acct)
58 if !ok {
59 return
60 }
61 rec, err := claimAppCall(s, ctx, acct, app, "store-provision", func(ctx context.Context) (*objstore.Record, error) {
62 return objstore.GrantIn(ctx, s.cfg.Releases, s.cfg.StoreProvisioner, s.cfg.StoreDeployment,
63 pub, acct.Sqid, app, env, e.Decision.By, objstore.GrantSourceAccountApproval, "", time.Now())
64 })
65 switch {
66 case errors.Is(err, objstore.ErrNotDeclared):
67 jsonError(w, http.StatusConflict, "no store is declared for this app. run `carlos store create` first")
68 return
69 case errors.Is(err, objstore.ErrStoreNameTaken):
70 log.Printf("console: api store provision %s/%s: %v", acct.Sqid, app, err)
71 jsonError(w, http.StatusConflict, storeNameTakenMsg)
72 return
73 case err != nil:
74 log.Printf("console: api store provision %s/%s: %v", acct.Sqid, app, err)
75 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")
76 return
77 }
78 s.audit(acct, app, actor, "store-provision", "provisioned a store in bucket "+storeBucketName(rec)+storeEnvSuffix(env)+" (cli)")
79 s.bumpSerial(ctx)
80 s.storeWalkReply(ctx, w, acct, app, env, rec)
81}
82
83func (s *Server) storeProvisionApproval(ctx context.Context, w http.ResponseWriter, acct *Account) (*objstore.Entitlement, bool) {
84 e, _, err := objstore.GetEntitlement(ctx, s.cfg.Releases, acct.Sqid)
85 if err != nil {
86 log.Printf("console: api store provision %s: read entitlement: %v", acct.Sqid, err)
87 jsonError(w, http.StatusBadGateway, "could not read the storage application. try again")
88 return nil, false
89 }
90 if !e.Approved() {
91 jsonError(w, http.StatusForbidden, "this account is not approved for object storage. run `carlos store apply` to ask for it")
92 return nil, false
93 }
94 return e, true
95}
96