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 "database/sql"
6 "errors"
7 "os"
8
9 "github.com/carlosframework/platform/internal/hostops"
10)
11
12func (s *Store) BindClaimAppIdentity(accountID int64, app, appID string) error {
13 if appID == "" {
14 return hostops.ErrIdentity
15 }
16 result, err := s.db.Exec(`UPDATE apps SET app_id=? WHERE account_id=? AND app=? AND (app_id='' OR app_id=?)`, appID, accountID, app, appID)
17 if err != nil {
18 return err
19 }
20 n, err := result.RowsAffected()
21 if err != nil {
22 return err
23 }
24 if n != 1 {
25 return hostops.ErrPending
26 }
27 return nil
28}
29
30func (s *Server) claimControl() (*hostops.ControlWriter, error) {
31 if !s.cfg.HostClaim || s.cfg.ClaimAuthority == nil || s.cfg.ClaimAuthority.Control == nil {
32 return nil, hostops.ErrContract
33 }
34 return s.cfg.ClaimAuthority.Control, nil
35}
36
37// claimAdmissionNameFree is claimApp's read-only exemption for its own
38// prefix: only a fully completed, retained admission (and a request naming
39// that retired incarnation) may remain. It never writes; EnrollNewApp
40// repeats the census and discards the request itself.
41func (s *Server) claimAdmissionNameFree(ctx context.Context, account, app string) (bool, error) {
42 prefix := "apps/" + account + "/" + app + "/"
43 keys, err := s.cfg.Releases.List(ctx, prefix)
44 if err != nil {
45 return false, err
46 }
47 if len(keys) == 0 {
48 return true, nil
49 }
50 a, err := hostops.ReadCanonicalAdmission(ctx, s.cfg.Releases, account, app)
51 if err != nil || !hostops.CompletedAdmission(a.Value) {
52 return false, err
53 }
54 for _, key := range keys {
55 if key != prefix+"admission.json" && key != prefix+"purge-request.json" {
56 return false, nil
57 }
58 }
59 return true, nil
60}
61
62// bindClaimAppIdentity writes the canonical app_id into this console's row
63// as an effect of a short exclusive "enroll" operation: the SQL gate admits
64// it on that permit, and no purge or other mutation can interleave.
65func (s *Server) bindClaimAppIdentity(ctx context.Context, acct *Account, app, appID string) error {
66 var err error
67 ctx, err = s.claimEnrollmentContext(ctx, acct, app, appID)
68 if err != nil {
69 return err
70 }
71 if _, err := s.claimControl(); err != nil {
72 return err
73 }
74 scope := claimSQLScope{AccountID: acct.ID, App: app, AppID: appID}
75 return s.claimAppMutation(ctx, acct, app, "enroll", func(ctx context.Context) error {
76 // This local SQL call has joined when it returns. Its error cannot
77 // authorize a late remote effect, and the next sweep retries the bind.
78 return s.store.WithAppMutation(ctx, []sqlGrant{permitGrant(ctx, s.cfg.Releases, acct.Sqid, scope)}, func(st *Store) error {
79 return st.BindClaimAppIdentity(acct.ID, app, appID)
80 })
81 })
82}
83
84// reserveClaimAppAdmission runs only after this request won SQL new-row
85// creation. The canonical identity is chosen by the bucket CAS; SQL binds it.
86func (s *Server) reserveClaimAppAdmission(ctx context.Context, acct *Account, app string) error {
87 w, err := s.claimControl()
88 if err != nil {
89 return err
90 }
91 appID, err := w.EnrollNewApp(ctx, acct.Sqid, app)
92 if err != nil {
93 return err
94 }
95 return s.bindClaimAppIdentity(ctx, acct, app, appID)
96}
97
98func (s *Server) initializeBackfillClaimAppAdmission(ctx context.Context, acct *Account, app string) error {
99 w, err := s.claimControl()
100 if err != nil {
101 return err
102 }
103 grant, err := s.cfg.ClaimAuthority.BackfillAppGrant(ctx, acct.Sqid, app)
104 if err != nil {
105 return err
106 }
107 appID, err := w.EnrollBackfillApp(ctx, grant)
108 if err != nil {
109 return err
110 }
111 return s.bindClaimAppIdentity(ctx, acct, app, appID)
112}
113
114func (s *Server) bindExistingClaimAppAdmission(ctx context.Context, acct *Account, app string) error {
115 a, err := hostops.ReadCanonicalAdmission(ctx, s.cfg.Releases, acct.Sqid, app)
116 if err != nil {
117 return err
118 }
119 if hostops.CompletedAdmission(a.Value) {
120 return hostops.ErrPending
121 }
122 bound, err := s.store.ClaimAppID(acct.ID, app)
123 if err != nil && !errors.Is(err, hostops.ErrIdentity) {
124 return err
125 }
126 if bound != "" {
127 if bound != a.Value.AppID {
128 return hostops.ErrPending
129 }
130 return nil
131 }
132 // A legacy unbound row carries no evidence that it belongs to a name's
133 // new incarnation. Only creation binds new rows after a completed purge.
134 if a.Value.LastPurge != nil {
135 return hostops.ErrPending
136 }
137 return s.bindClaimAppIdentity(ctx, acct, app, a.Value.AppID)
138}
139
140func (s *Server) discardCompletedPurgeRequest(ctx context.Context, account, app string) (bool, error) {
141 w, err := s.claimControl()
142 if err != nil {
143 return false, err
144 }
145 return w.DiscardPurgeRequest(ctx, account, app)
146}
147
148func (s *Server) SweepClaimAppIdentities(ctx context.Context) error {
149 if !s.cfg.HostClaim {
150 return nil
151 }
152 rows, err := s.store.AppRows()
153 if err != nil {
154 return err
155 }
156 var failures []error
157 for _, row := range rows {
158 if err := ctx.Err(); err != nil {
159 return errors.Join(append(failures, err)...)
160 }
161 acct, err := s.store.AccountByID(row.AccountID)
162 if err != nil {
163 failures = append(failures, err)
164 continue
165 }
166 if acct == nil || acct.Sqid != row.Sqid {
167 failures = append(failures, hostops.ErrIdentity)
168 continue
169 }
170 _, err = hostops.ReadCanonicalAdmission(ctx, s.cfg.Releases, row.Sqid, row.App)
171 if errors.Is(err, os.ErrNotExist) {
172 err = s.initializeBackfillClaimAppAdmission(ctx, acct, row.App)
173 } else if err == nil {
174 err = s.bindExistingClaimAppAdmission(ctx, acct, row.App)
175 }
176 if err != nil && !errors.Is(err, hostops.ErrPending) {
177 failures = append(failures, err)
178 }
179 }
180 return errors.Join(failures...)
181}
182
183// Enrollment alone may supply an identity before the local row is bound.
184// The value is private to this package and scoped to one account and app.
185// Existing nonempty identities must match; errors never authorize enrollment.
186type claimEnrollmentKey struct{}
187type claimEnrollmentIdentity struct{ account, app, id string }
188
189func (s *Server) claimEnrollmentContext(ctx context.Context, acct *Account, app, appID string) (context.Context, error) {
190 if acct == nil || appID == "" {
191 return ctx, hostops.ErrIdentity
192 }
193 id, err := s.store.ClaimAppID(acct.ID, app)
194 if err != nil && !errors.Is(err, sql.ErrNoRows) && !errors.Is(err, hostops.ErrIdentity) {
195 return ctx, err
196 }
197 if id != "" && id != appID {
198 return ctx, hostops.ErrIdentity
199 }
200 return context.WithValue(ctx, claimEnrollmentKey{}, claimEnrollmentIdentity{acct.Sqid, app, appID}), nil
201}
202
203func (s *Server) mutationAppID(ctx context.Context, account, app string) (string, error) {
204 acct, err := s.store.AccountBySqid(account)
205 if err != nil {
206 return "", err
207 }
208 if acct == nil {
209 return "", hostops.ErrIdentity
210 }
211 id, err := s.store.ClaimAppID(acct.ID, app)
212 if enrollment, ok := ctx.Value(claimEnrollmentKey{}).(claimEnrollmentIdentity); ok && enrollment.account == account && enrollment.app == app {
213 if err != nil && !errors.Is(err, sql.ErrNoRows) && !errors.Is(err, hostops.ErrIdentity) {
214 return "", err
215 }
216 if id != "" && id != enrollment.id {
217 return "", hostops.ErrIdentity
218 }
219 return enrollment.id, nil
220 }
221 return id, err
222}
223