carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package hostops
2
3import (
4 "context"
5 "crypto/rand"
6 "crypto/sha256"
7 "crypto/subtle"
8 "encoding/hex"
9 "encoding/json"
10 "errors"
11 "os"
12 "time"
13
14 "github.com/carlosframework/platform/internal/release"
15)
16
17// The shared mail-resource gate serializes every enabled SES, DNS and
18// email-index mutation deployment-wide. It has no expiry and no takeover:
19// a gate retained after an uncertain effect waits for fenced
20// reconciliation. Possession is proved by a secret token whose SHA-256 is
21// stored; reading the gate object is not possession.
22type MailGateOwner struct{ Console, Enrollment, Controller string }
23type MailGateScope struct{ Account, App, AppID, Operation string }
24
25type MailGateLease struct {
26 ID string
27 Owner MailGateOwner
28 Scope MailGateScope
29 token string
30 // revision of the held state this lease wrote
31 revision uint64
32}
33
34type mailGate struct {
35 V int `json:"v"`
36 Revision uint64 `json:"revision"`
37 State string `json:"state"`
38 ID string `json:"id"`
39 Owner MailGateOwner `json:"owner"`
40 Scope MailGateScope `json:"scope"`
41 TokenSHA256 string `json:"token_sha256"`
42 HeldAt time.Time `json:"held_at,omitempty"`
43 // Members is every app operation/effect this hold admitted, recorded
44 // BEFORE the member's operation or effect exists, so a reconciler finds
45 // every effect the hold could have issued without scanning admissions.
46 Members []MailGateMember `json:"members,omitempty"`
47 // Reconciling is set by the reconciler's fencing CAS (revision bumped,
48 // token cleared): no lease of the old holder checks any more.
49 Reconciling bool `json:"reconciling,omitempty"`
50 // Reconciled is the evidence of the last fenced reconciliation.
51 Reconciled *MailGateReconciliation `json:"reconciled,omitempty"`
52}
53
54// MailGateMember names one app effect admitted under a held gate.
55type MailGateMember struct {
56 Account string `json:"account"`
57 App string `json:"app"`
58 AppID string `json:"app_id"`
59 Operation string `json:"operation"`
60 Effect string `json:"effect"`
61}
62
63type mailGateLeaseKey struct{}
64
65func WithMailGateLease(ctx context.Context, l *MailGateLease) context.Context {
66 return context.WithValue(ctx, mailGateLeaseKey{}, l)
67}
68func MailGateLeaseFrom(ctx context.Context) (*MailGateLease, bool) {
69 l, ok := ctx.Value(mailGateLeaseKey{}).(*MailGateLease)
70 return l, ok && l != nil
71}
72
73func (w *ControlWriter) readMailGate(ctx context.Context) (mailGate, string, error) {
74 body, tag, err := w.raw.Get(ctx, MailGateKey)
75 if errors.Is(err, os.ErrNotExist) {
76 return mailGate{V: 1, State: "free"}, "", nil
77 }
78 if err != nil {
79 return mailGate{}, "", err
80 }
81 var g mailGate
82 if err = json.Unmarshal(body, &g); err != nil {
83 return mailGate{}, "", err
84 }
85 if g.V != 1 || g.Revision == 0 || (g.State != "free" && g.State != "held") || tag == "" {
86 return mailGate{}, "", ErrIdentity
87 }
88 return g, tag, nil
89}
90
91func tokenDigest(token string) string {
92 sum := sha256.Sum256([]byte(token))
93 return hex.EncodeToString(sum[:])
94}
95
96func (w *ControlWriter) AcquireMailGate(ctx context.Context, owner MailGateOwner, scope MailGateScope) (*MailGateLease, error) {
97 if err := w.ready(); err != nil {
98 return nil, err
99 }
100 if owner.Console == "" || owner.Enrollment == "" || owner.Controller == "" {
101 return nil, ErrIdentity
102 }
103 g, tag, err := w.readMailGate(ctx)
104 if err != nil {
105 return nil, err
106 }
107 if g.State != "free" {
108 return nil, ErrPending
109 }
110 var id, secret [16]byte
111 if _, err = rand.Read(id[:]); err != nil {
112 return nil, err
113 }
114 if _, err = rand.Read(secret[:]); err != nil {
115 return nil, err
116 }
117 token := hex.EncodeToString(secret[:])
118 g.Revision++
119 g.State, g.ID, g.Owner, g.Scope, g.TokenSHA256, g.HeldAt = "held", hex.EncodeToString(id[:]), owner, scope, tokenDigest(token), w.now().UTC()
120 g.Members, g.Reconciling = nil, false
121 body, err := json.Marshal(g)
122 if err != nil {
123 return nil, err
124 }
125 if _, err = w.raw.Put(ctx, MailGateKey, body, tag); err != nil {
126 return nil, err
127 }
128 return &MailGateLease{ID: g.ID, Owner: owner, Scope: scope, token: token, revision: g.Revision}, nil
129}
130
131// CheckMailGate proves the exact held revision belongs to this lease.
132func (w *ControlWriter) CheckMailGate(ctx context.Context, l *MailGateLease) error {
133 if err := w.ready(); err != nil {
134 return err
135 }
136 if l == nil || l.token == "" {
137 return ErrPending
138 }
139 g, _, err := w.readMailGate(ctx)
140 if err != nil {
141 return err
142 }
143 if g.State != "held" || g.ID != l.ID || g.Owner != l.Owner || g.Revision != l.revision ||
144 subtle.ConstantTimeCompare([]byte(g.TokenSHA256), []byte(tokenDigest(l.token))) != 1 {
145 return ErrPending
146 }
147 return nil
148}
149
150// ReleaseMailGate frees the gate only from this lease's exact held revision.
151// Callers release only after every issued effect under the gate settled
152// complete; an uncertain effect retains the gate.
153func (w *ControlWriter) ReleaseMailGate(ctx context.Context, l *MailGateLease) error {
154 if err := w.CheckMailGate(ctx, l); err != nil {
155 return err
156 }
157 g, tag, err := w.readMailGate(ctx)
158 if err != nil {
159 return err
160 }
161 if g.Revision != l.revision {
162 return ErrPending
163 }
164 g.Revision++
165 g.State, g.TokenSHA256, g.HeldAt, g.Members = "free", "", time.Time{}, nil
166 body, err := json.Marshal(g)
167 if err != nil {
168 return err
169 }
170 _, err = w.raw.Put(ctx, MailGateKey, body, tag)
171 return err
172}
173
174// RecordMailGateMember durably names one app effect this lease is about to
175// admit. It must succeed before the member's BeginExclusive/ResumeExclusive
176// and Issue. The revision is unchanged (the ETag CAS orders it), so the
177// lease keeps checking.
178func (w *ControlWriter) RecordMailGateMember(ctx context.Context, l *MailGateLease, m MailGateMember) error {
179 if m.Account == "" || m.App == "" || m.AppID == "" || m.Operation == "" || m.Effect == "" {
180 return ErrIdentity
181 }
182 for attempt := 1; ; attempt++ {
183 if err := w.CheckMailGate(ctx, l); err != nil {
184 return err
185 }
186 g, tag, err := w.readMailGate(ctx)
187 if err != nil {
188 return err
189 }
190 if g.Revision != l.revision || g.Reconciling {
191 return ErrPending
192 }
193 for _, have := range g.Members {
194 if have == m {
195 return nil
196 }
197 }
198 g.Members = append(g.Members, m)
199 body, err := json.Marshal(g)
200 if err != nil {
201 return err
202 }
203 if _, err = w.raw.Put(ctx, MailGateKey, body, tag); err == nil || !errors.Is(err, release.ErrCASConflict) || attempt >= casRetries {
204 return err
205 }
206 }
207}
208