carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package provision
2
3import (
4 "context"
5 "errors"
6 "fmt"
7 "log"
8 "regexp"
9 "strings"
10 "time"
11
12 "github.com/carlosframework/platform/internal/registry"
13 "github.com/carlosframework/platform/internal/release"
14 "github.com/carlosframework/platform/internal/seeds"
15)
16
17// importShape is the legacy row's shape check (spec §5.4, codex 10, R1-10):
18// "" when the row is an addr-backed instance or service this import may
19// take over, else what makes it not one (the column's name).
20//
21// Addr-backed means the edge serves the row from its addr or addrs and
22// from nothing else. The edge's dispatch (edge.Reload) takes a tunnel,
23// then a fleet placement, then a socket, before it reaches the addrs, and
24// an instance flagged hibernate goes through the activator, which starts
25// a process of its own. So each of those is refused here, beside the
26// columns shapeMismatch refuses adoption over and a claim's ownership,
27// which the import journal cannot put back (registry.SwapForImport).
28func importShape(rt registry.Route) string {
29 switch {
30 case rt.Kind != "instance" && rt.Kind != "service":
31 return "kind " + rt.Kind
32 case rt.Socket != "":
33 return "socket"
34 case rt.Addr == "" && rt.Addrs == "":
35 return "no address"
36 case rt.Provisioned:
37 return "provisioned"
38 case rt.Backing != "":
39 return "backing"
40 case rt.Tunnel != "":
41 return "tunnel"
42 case rt.Caps != "":
43 return "caps"
44 case rt.Platform:
45 return "platform"
46 case rt.Canary:
47 return "canary"
48 case rt.FleetBox != "":
49 return "fleet_box"
50 case rt.Kind == "instance" && rt.Hibernate:
51 return "hibernate"
52 case rt.ClaimOwnership != (registry.ClaimOwnership{}):
53 return "claim_owner"
54 }
55 if _, fleet := registry.FleetPlacement(rt.Region); fleet {
56 return "region"
57 }
58 return ""
59}
60
61// importAddr is the address the row serves, as the edge reads it
62// (registry.Route.AddrList): addrs when set, else addr. A row whose addr
63// is Replaces but whose addrs names something else serves the addrs, so
64// it is not the route the import recorded.
65func importAddr(rt registry.Route) string {
66 return strings.Join(rt.AddrList(), ",")
67}
68
69// importRefusal is §5.4's predicates in order: "" to swap, else the
70// refusal reason the record keeps. owners is consulted only for a row
71// filed under the operator account: release.RouteAccount folds a legacy
72// row's "" (and any account that is not a sqid) into release.OpsAccount,
73// so such a row is the operator's too (preflight M5).
74func importRefusal(rec Record, existing *registry.Route, cfg ReconcileConfig, owners *seeds.LegacyOwners, nested bool) string {
75 switch {
76 case cfg.ClaimEnabled:
77 return "claim control is on for this box"
78 case !nested:
79 return "this box does not run instances nested"
80 case existing == nil:
81 return fmt.Sprintf("there is no route at %s to replace", rec.Host)
82 }
83 if what := importShape(*existing); what != "" {
84 return fmt.Sprintf("the route is not an address route this import can take over (%s)", what)
85 }
86 if addr := importAddr(*existing); addr != rec.Seed.Replaces {
87 return fmt.Sprintf("the route's address is %s, not %s", addr, rec.Seed.Replaces)
88 }
89 if existing.App != rec.App {
90 return fmt.Sprintf("the route belongs to app %s", existing.App)
91 }
92 switch {
93 case release.RouteAccount(*existing) == rec.Account:
94 return ""
95 case operatorFiled(rec, existing):
96 if o, ok := owners.Lookup(existing.App); ok && o.Account == rec.Account && o.App == rec.App {
97 return ""
98 }
99 return fmt.Sprintf("the route is filed under the operator account, and no assignment maps %s to this app", existing.App)
100 }
101 return "the route is filed under another account"
102}
103
104// operatorFiled reports a row the swap may take only through the operator
105// assignment: filed under the operator account, for a record that is not
106// the operator's own. importRefusal's account rule and swapImport's
107// decision to read the assignment both ask this one question.
108func operatorFiled(rec Record, rt *registry.Route) bool {
109 if rt == nil {
110 return false
111 }
112 acct := release.RouteAccount(*rt)
113 return acct != rec.Account && acct == release.OpsAccount
114}
115
116var verifyHashShape = regexp.MustCompile(`^[0-9a-f]{64}$`)
117
118// importUngateable is why the swap must not run for this record at all
119// (preflight M3), or "": the gate needs a hash (an empty one would swap in
120// an ungated row), the host and seed id name the snapshot and must pass
121// seeds.Check, and the new row must be one the activator manages, nested
122// and hibernating, since only that row places the snapshot before it
123// serves. These are not refusals: the record stays ready, logged, until
124// it is aborted.
125func importUngateable(rec Record, next registry.Route, dataDir string) string {
126 if err := seeds.Check(rec.Host, rec.Seed.ID); err != nil {
127 return fmt.Sprintf("its host or seed id is malformed (%v)", err)
128 }
129 switch {
130 case !verifyHashShape.MatchString(rec.Seed.VerifyHash):
131 return "its verify hash is not a sha256"
132 case !rec.DerivesPaths() || !next.Hibernate || !NestedPaths(dataDir, next):
133 return "its record does not describe a nested, hibernating instance"
134 }
135 return ""
136}
137
138// importRow is the exec row the swap installs (spec §5.4): the record's
139// route at the nested paths, explicitly, never NestedPaths(existing),
140// which reads the addr row as flat (codex 12); no seed command; filed
141// under the record's account; gated by the verify hash. Its seed column
142// already says swapped, the state the record's CAS moves it to. policy is
143// the app's update policy, as for any mint (RouteFor): the swap's row is
144// born with the record's effective update settings.
145func importRow(rec Record, dataDir string, policy *UpdatePolicy) registry.Route {
146 swapped := *rec.Seed
147 swapped.State = seeds.StateSwapped
148 r := rec
149 r.Seed = &swapped
150 next := RouteFor(r, dataDir, "", true, policy)
151 next.ImportGate = rec.Seed.VerifyHash
152 return next
153}
154
155// swapImport is the swap (spec §5.4): the journal first, then the
156// predicates, then one registry transaction, then the record's CAS to
157// swapped. Transient failures defer to the next pass and write nothing; a
158// failed predicate is a durable refusal. policy is the app's update policy,
159// known (Reconcile defers the swap, as it defers a mint, while it cannot be
160// read).
161func swapImport(ctx context.Context, s release.Store, reg *registry.Registry, cfg ReconcileConfig, rec Record, existing *registry.Route, nested func() (bool, error), policy *UpdatePolicy) {
162 if cfg.DryRun {
163 log.Printf("provision: dry-run: would consider swapping %s for import %s", rec.Host, rec.Seed.ID)
164 return
165 }
166 // The journal comes first (codex R2-3): a row there means this box has
167 // already swapped, so the legacy-row predicates never judge the row it
168 // made. This seed's own row at swapped is a crash between the swap's
169 // transaction and the record's CAS: only the CAS is redone.
170 //
171 // Reconcile's journalPass decides every journaled host before the swap
172 // is reached, so in a pass this check finds nothing. It is kept as a
173 // backstop: without it, a journal row this function did not expect
174 // (another caller, or a row written after the pass read the journal)
175 // would be judged as a legacy row and refused, terminally.
176 j, err := reg.ImportJournal(rec.Host)
177 if err != nil {
178 log.Printf("provision: %s: import deferred: the import journal could not be read: %v", rec.Host, err)
179 return
180 }
181 if j != nil {
182 if j.SeedID != rec.Seed.ID || j.State != registry.JournalSwapped || j.RecordKey != RecordKey(rec.Account, rec.App, rec.Host) {
183 log.Printf("provision: %s: import %s left alone: the import journal holds seed %s at %s", rec.Host, rec.Seed.ID, j.SeedID, j.State)
184 return
185 }
186 markSwapped(ctx, s, rec)
187 return
188 }
189
190 next := importRow(rec, cfg.DataDir, policy)
191 if why := importUngateable(rec, next, cfg.DataDir); why != "" {
192 log.Printf("provision: %s: not swapping for import %s: %s", rec.Host, rec.Seed.ID, why)
193 return
194 }
195 isNested := false
196 if !cfg.ClaimEnabled {
197 ok, err := nested()
198 if err != nil {
199 log.Printf("provision: %s: import deferred: the box's layout intent could not be read: %v", rec.Host, err)
200 return
201 }
202 // Only the intent refuses (SD-N97). The live probe can fail for a
203 // moment, and a refusal is terminal: an error waits for the next
204 // pass (preflight M4).
205 if ok && cfg.Seeds.NestedOK != nil {
206 if err := cfg.Seeds.NestedOK(); err != nil {
207 log.Printf("provision: %s: import deferred: the box's nested probe did not answer: %v", rec.Host, err)
208 return
209 }
210 }
211 isNested = ok
212 }
213 if existing != nil && existing.HoldOp != "" {
214 log.Printf("provision: %s: import deferred: the route is held by operation %s", rec.Host, existing.HoldOp)
215 return
216 }
217 // The assignment is read only when it can decide: claims off (SD-N98
218 // comes first) and the box nested (SD-N97 next).
219 var owners *seeds.LegacyOwners
220 if !cfg.ClaimEnabled && isNested && operatorFiled(rec, existing) {
221 if cfg.Seeds.LegacyOwners == nil {
222 log.Printf("provision: %s: import deferred: this box cannot read the legacy route assignments", rec.Host)
223 return
224 }
225 o, err := cfg.Seeds.LegacyOwners(ctx)
226 if err != nil {
227 log.Printf("provision: %s: import deferred: the legacy route assignments could not be read: %v", rec.Host, err)
228 return
229 }
230 owners = o
231 }
232 if reason := importRefusal(rec, existing, cfg, owners, isNested); reason != "" {
233 if _, err := SetRecordSeed(ctx, s, rec.Account, rec.App, rec.Host, SeedRefuse(rec.Seed.ID, reason)); err != nil {
234 log.Printf("provision: %s: record the import's refusal (%s): %v", rec.Host, reason, err)
235 return
236 }
237 log.Printf("provision: %s: import %s refused: %s", rec.Host, rec.Seed.ID, reason)
238 return
239 }
240 if cfg.ConfigGate != nil {
241 if err := cfg.ConfigGate(ctx, next); err != nil {
242 return // the gate logs its own refusal; retried every pass
243 }
244 }
245 // What the host's own env file holds before the import's row exists,
246 // for the journal: the rollback retires only the instance key the
247 // import's launches created (merge review S1). Read last, so nothing
248 // can launch the import between this read and the transaction.
249 if cfg.Seeds.EnvBefore == nil {
250 log.Printf("provision: %s: import deferred: this box cannot read the instance's env file", rec.Host)
251 return
252 }
253 envBefore, err := cfg.Seeds.EnvBefore(next.DBPath, rec.Host)
254 if err != nil {
255 log.Printf("provision: %s: import deferred: the instance's env file could not be read: %v", rec.Host, err)
256 return
257 }
258 now := time.Now
259 if cfg.Now != nil {
260 now = cfg.Now
261 }
262 journal := registry.ImportJournal{Host: rec.Host, SeedID: rec.Seed.ID, Original: *existing,
263 RecordKey: RecordKey(rec.Account, rec.App, rec.Host), At: now().Unix(), EnvBefore: envBefore}
264 if err := reg.SwapForImport(journal, next); err != nil {
265 log.Printf("provision: %s: import swap deferred: %v", rec.Host, err)
266 return
267 }
268 log.Printf("provision: %s: swapped for import %s (the original route is journaled)", rec.Host, rec.Seed.ID)
269 markSwapped(ctx, s, rec)
270}
271
272// markSwapped is the swap's record CAS, ready to swapped. It runs only
273// after the registry transaction committed; a failure leaves the journal
274// at swapped and the record at ready, which the next pass resumes.
275func markSwapped(ctx context.Context, s release.Store, rec Record) {
276 if _, err := SetRecordSeed(ctx, s, rec.Account, rec.App, rec.Host, SeedSwapped(rec.Seed.ID)); err != nil {
277 if errors.Is(err, ErrSeedConflict) {
278 log.Printf("provision: %s: import %s swapped, but the record moved on: %v", rec.Host, rec.Seed.ID, err)
279 return
280 }
281 log.Printf("provision: %s: import %s swapped, record CAS deferred to the next pass: %v", rec.Host, rec.Seed.ID, err)
282 }
283}
284