carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1// Package hostclaim is the host lifecycle claim (spec
2// 2026-10-02-host-lifecycle-claim-design.md): one object per host that
3// names the incarnation entitled to it, the operation running on it, where
4// its data lives and what is still owed. This package only reads,
5// validates and conditionally writes that object. It never deletes it
6// (C5a).
7package hostclaim
8
9import (
10 "crypto/sha256"
11 "encoding/hex"
12 "errors"
13 "fmt"
14 "strings"
15 "time"
16
17 "github.com/carlosframework/platform/internal/controlname"
18 "github.com/carlosframework/platform/internal/maintenanceguard"
19)
20
21const (
22 KindServing = "serving"
23 KindCreating = "creating"
24 KindDeleting = "deleting"
25 KindParking = "parking"
26 KindPurging = "purging"
27 KindPlacing = "placing"
28 KindMoving = "moving" // move-any-app
29 KindLanding = "landing" // move-any-app
30 KindReverting = "reverting" // move-any-app
31 KindRestoring = "restoring" // move-any-app
32 KindPushing = "pushing" // later push spec
33
34 schemaVersion = 2
35 retiredCap = 32
36)
37
38var knownKinds = map[string]bool{KindServing: true, KindCreating: true, KindDeleting: true, KindParking: true,
39 KindPurging: true, KindPlacing: true, KindMoving: true, KindLanding: true, KindReverting: true,
40 KindRestoring: true, KindPushing: true, KindRelocating: true}
41
42var (
43 ErrMalformed = errors.New("hostclaim: malformed claim")
44 ErrInvalidHost = errors.New("hostclaim: invalid host")
45)
46
47type Holder struct {
48 Acquisition string `json:"acq,omitempty"`
49 Family string `json:"family"` // "instance" | "sidecar"
50 Key string `json:"key"` // exact record key
51 Account string `json:"account"`
52 App string `json:"app"`
53 Name string `json:"name,omitempty"` // sidecars only
54 Incarnation string `json:"incarnation"`
55 CreatedAt time.Time `json:"created_at"`
56 Location string `json:"location"`
57 Generation string `json:"generation,omitempty"`
58 Stamp uint64 `json:"stamp,omitempty"`
59 Session uint64 `json:"session,omitempty"`
60 WriterBox string `json:"writer_box,omitempty"`
61 Admissions []AdmissionRecord `json:"admissions,omitempty"`
62 WakeRetry *WakeRetry `json:"wake_retry,omitempty"`
63}
64
65type AdmissionRecord struct {
66 Region string `json:"region"`
67 PredecessorRegion string `json:"predecessor_region,omitempty"`
68 Session uint64 `json:"session"`
69 Box string `json:"box"`
70 Generation string `json:"generation"`
71 Predecessor string `json:"predecessor,omitempty"`
72 Mode string `json:"mode"`
73 Coverage string `json:"coverage,omitempty"`
74 Covered bool `json:"covered"`
75 WritesLost *WritesLost `json:"writes_lost,omitempty"`
76 AdditionalWritesLost []WritesLost `json:"additional_writes_lost,omitempty"`
77 LossID string `json:"loss_id,omitempty"`
78 LossArchivedClaimGen uint64 `json:"loss_archived_claim_gen,omitempty"`
79}
80
81type WritesLost struct {
82 Box string `json:"box"`
83 Generation string `json:"generation"`
84 Session uint64 `json:"session"`
85 RemoteMaxTXID uint64 `json:"remote_max_txid"`
86}
87
88type Op struct {
89 Kind string `json:"kind,omitempty"`
90 ID string `json:"id,omitempty"`
91 Phase string `json:"phase,omitempty"`
92 Intent map[string]string `json:"intent,omitempty"`
93 Journal map[string]string `json:"journal,omitempty"`
94 By string `json:"by,omitempty"`
95 StartedAt time.Time `json:"started_at,omitempty"`
96 Exec string `json:"exec,omitempty"`
97 Participants []string `json:"participants,omitempty"`
98}
99
100type Touched struct {
101 DiscoveryBound bool `json:"discovery_bound,omitempty"`
102 DiscoveryBoxes []string `json:"discovery_boxes,omitempty"`
103 Complete bool `json:"complete"`
104 Regions []string `json:"regions,omitempty"`
105 Boxes []string `json:"boxes,omitempty"`
106 DiscoveryOp string `json:"discovery_op,omitempty"`
107}
108
109// InventoryEntry is one box's local paths for the host (§6.3), each tagged
110// with the installation stamp that produced it (§6.4).
111type InventoryEntry struct {
112 Layout string `json:"layout"` // "flat" | "nested"
113 Paths map[string]uint64 `json:"paths"` // path -> stamp
114}
115
116type ParkScope struct {
117 Location string `json:"location"`
118 Incarnation string `json:"incarnation"`
119 EvidenceAt time.Time `json:"evidence_at"`
120 RequestLimit time.Duration `json:"request_limit_ns"`
121}
122
123func (p *ParkScope) Validate(host string) error {
124 if p == nil || !isHex64(p.Incarnation) || p.Incarnation != strings.ToLower(p.Incarnation) || p.EvidenceAt.IsZero() || p.RequestLimit <= 0 || p.RequestLimit > time.Duration(1<<63-1)-15*time.Minute {
125 return fmt.Errorf("%w: park scope", ErrMalformed)
126 }
127 base, staging := "instances/"+host, "moves/"+host
128 if p.Location == base || p.Location == base+"/"+p.Incarnation || p.Location == staging {
129 return nil
130 }
131 if op, ok := strings.CutPrefix(p.Location, staging+"/"); ok && op != "" && op != "." && op != ".." && !strings.Contains(op, "/") {
132 return nil
133 }
134 return fmt.Errorf("%w: park location", ErrMalformed)
135}
136
137type Obligation struct {
138 EvidenceRefs map[string]RetirementEvidenceRef `json:"evidence_refs,omitempty"`
139 ParkTarget string `json:"park_target,omitempty"`
140 Park *ParkScope `json:"park,omitempty"`
141 ID string `json:"id"`
142 Op string `json:"op"`
143 Kind string `json:"kind"` // "park-replica" | "park-local" | "park-generation"
144 Region string `json:"region,omitempty"`
145 Generation string `json:"generation,omitempty"`
146 Box string `json:"box,omitempty"`
147 Retirement string `json:"retirement"`
148 Owner string `json:"owner"` // "region:<r>" | "box:<b>"
149}
150
151type CancelledCreate struct {
152 Key string `json:"key"`
153 Incarnation string `json:"incarnation"`
154 Until time.Time `json:"until"`
155}
156
157type Claim struct {
158 RetainedAdmissionLosses []RetainedAdmissionLoss `json:"retained_admission_losses,omitempty"`
159 Origins *OriginHead `json:"admitted_origins,omitempty"`
160 originAdmission *originTuple
161
162 RetiredLocations map[string]string `json:"retired_locations,omitempty"`
163 EvidenceRefs []AutomaticLoss `json:"evidence_refs,omitempty"`
164 V int `json:"v"`
165 Host string `json:"host"`
166 Gen uint64 `json:"gen"`
167 Holder *Holder `json:"holder,omitempty"`
168 Op Op `json:"op"`
169 Touched Touched `json:"touched"`
170 Inventory map[string]InventoryEntry `json:"inventory,omitempty"`
171 Obligations []Obligation `json:"obligations,omitempty"`
172 Lost []string `json:"lost,omitempty"`
173 LostRevisions map[string]uint64 `json:"lost_revisions,omitempty"`
174 LossRefs []string `json:"loss_refs,omitempty"`
175 RetiredIncarnations []string `json:"retired_incarnations,omitempty"`
176 QuietAfter map[string]time.Time `json:"quiet_after,omitempty"`
177 CancelledCreates []CancelledCreate `json:"cancelled_creates,omitempty"`
178}
179
180// Free reports whether the host has no holder and no operation.
181func (c *Claim) Free() bool { return c.Holder == nil && c.Op.Kind == "" }
182
183func Key(host string) (string, error) {
184 if !controlname.ValidHostShape(host) {
185 return "", ErrInvalidHost
186 }
187 return "control/hosts/" + host + ".json", nil
188}
189
190// Incarnation is §2's identity. A record with a zero CreatedAt (written
191// before CreatedAt existed) gets the domain-separated legacy identity.
192func Incarnation(account, app, host string, created time.Time) (string, error) {
193 if created.IsZero() {
194 if !controlname.ValidAccount(account) || !controlname.ValidApp(app) || !controlname.ValidHostShape(host) {
195 return "", ErrMalformed
196 }
197 sum := sha256.Sum256([]byte("carlos-legacy-incarnation-v1\x00" + account + "\x00" + app + "\x00" + host))
198 return hex.EncodeToString(sum[:]), nil
199 }
200 return maintenanceguard.RecordIdentity(account, app, host, created)
201}
202
203// sidecarSuffix restates provision.SidecarSuffix (no import: avoids a cycle).
204const sidecarSuffix = ".sidecar.internal"
205
206// holderKeyMatches is the one identity check for a holder's record key and
207// host, shared by Validate, BeginCreate and Adopt. An instance's key is
208// exactly apps/<acct>/<app>/instances/<host>.json. A sidecar's key is
209// apps/<acct>/<app>/sidecars/<name>.json, and its host is exactly
210// <name>.<app>.<acct>.sidecar.internal (provision.SidecarHost).
211func holderKeyMatches(family, key, account, app, name, host string) bool {
212 base := "apps/" + account + "/" + app + "/"
213 switch family {
214 case "instance":
215 return name == "" && key == base+"instances/"+host+".json"
216 case "sidecar":
217 return controlname.ValidLabel(name) && key == base+"sidecars/"+name+".json" &&
218 host == name+"."+app+"."+account+sidecarSuffix
219 }
220 return false
221}
222
223func isHex64(s string) bool {
224 if len(s) != 64 {
225 return false
226 }
227 _, err := hex.DecodeString(s)
228 return err == nil
229}
230
231func (c *Claim) Validate() error {
232 if c != nil && !originHeadValid(c.Origins) {
233 return ErrMalformed
234 }
235 if c == nil || c.V != schemaVersion || !controlname.ValidHostShape(c.Host) || c.Gen == 0 {
236 return fmt.Errorf("%w: header", ErrMalformed)
237 }
238 if err := validateRetainedAdmissionLosses(c); err != nil {
239 return err
240 }
241 if err := validateRelocating(c); err != nil {
242 return err
243 }
244 if c.Op.Kind != "" && !knownKinds[c.Op.Kind] {
245 return fmt.Errorf("%w: unknown op kind %q", ErrMalformed, c.Op.Kind)
246 }
247 if c.Op.Kind != "" && c.Op.Kind != KindServing && c.Op.ID == "" {
248 return fmt.Errorf("%w: op %q without id", ErrMalformed, c.Op.Kind)
249 }
250 if c.Holder == nil {
251 if c.Op.Kind != "" && c.Op.Kind != KindPurging && !holderlessSweep(c) { // a purge fence has no holder (CC21)
252 return fmt.Errorf("%w: op %q without holder", ErrMalformed, c.Op.Kind)
253 }
254 if len(c.Obligations) > 0 && !holderlessSweep(c) {
255 return fmt.Errorf("%w: a free claim with outstanding obligations", ErrMalformed)
256 }
257 } else {
258 h := c.Holder
259 if (h.Family != "instance" && h.Family != "sidecar") || h.Key == "" || !isHex64(h.Incarnation) ||
260 !controlname.ValidAccount(h.Account) || !controlname.ValidApp(h.App) {
261 return fmt.Errorf("%w: holder identity", ErrMalformed)
262 }
263 if !holderKeyMatches(h.Family, h.Key, h.Account, h.App, h.Name, c.Host) {
264 return fmt.Errorf("%w: holder key %q is not this host's", ErrMalformed, h.Key)
265 }
266 if want, err := Incarnation(h.Account, h.App, c.Host, h.CreatedAt); err != nil || want != h.Incarnation {
267 return fmt.Errorf("%w: incarnation does not match the holder's creation fields", ErrMalformed)
268 }
269 legacy, scoped := "instances/"+c.Host, "instances/"+c.Host+"/"+h.Incarnation
270 if h.Location != legacy && h.Location != scoped {
271 return fmt.Errorf("%w: holder location %q", ErrMalformed, h.Location)
272 }
273 if c.Op.Kind == "" {
274 return fmt.Errorf("%w: holder without op", ErrMalformed)
275 }
276 for _, record := range h.Admissions {
277 if record.Region == "" || !validWriteLosses(record.WritesLost, record.AdditionalWritesLost) {
278 return fmt.Errorf("%w: invalid admission", ErrMalformed)
279 }
280 if record.LossID != "" && !safeLossID(record.LossID) {
281 return fmt.Errorf("%w: invalid loss declaration id", ErrMalformed)
282 }
283 if record.LossArchivedClaimGen != 0 && (!admissionHasLoss(record) || record.LossArchivedClaimGen != record.Session) {
284 return fmt.Errorf("%w: invalid archived admission loss", ErrMalformed)
285 }
286 }
287 if err := h.WakeRetry.validate(); err != nil {
288 return err
289 }
290 if h.WakeRetry != nil && h.WakeRetry.State == "attempting" && h.WakeRetry.Attempt.Session != 0 &&
291 (h.WakeRetry.Attempt.Session != h.Session || h.WakeRetry.Attempt.Box != h.WriterBox) {
292 return fmt.Errorf("%w: wake retry attempt is not the live writer session", ErrMalformed)
293 }
294 }
295 lost := set(c.Lost)
296 for box, revision := range c.LostRevisions {
297 if box == "" || revision == 0 || !lost[box] {
298 return fmt.Errorf("%w: invalid lost revision", ErrMalformed)
299 }
300 }
301 seenLossRefs := map[string]bool{}
302 for _, id := range c.LossRefs {
303 if !safeLossID(id) || seenLossRefs[id] {
304 return fmt.Errorf("%w: invalid loss references", ErrMalformed)
305 }
306 seenLossRefs[id] = true
307 }
308 if err := validateAutomaticLosses(c, seenLossRefs); err != nil {
309 return err
310 }
311 for inc, location := range c.RetiredLocations {
312 if !isHex64(inc) || !stringPresent(c.RetiredIncarnations, inc) || (location != "instances/"+c.Host && location != "instances/"+c.Host+"/"+inc) {
313 return ErrMalformed
314 }
315 }
316 if len(c.RetiredIncarnations) > retiredCap {
317 return fmt.Errorf("%w: retired_incarnations over cap", ErrMalformed)
318 }
319 seen := map[string]bool{}
320 for _, o := range c.Obligations {
321 if o.ID == "" || o.Op == "" || o.Retirement == "" || seen[o.ID] {
322 return fmt.Errorf("%w: obligation ids", ErrMalformed)
323 }
324 if o.Park != nil {
325 if o.Kind != "park-replica" && o.Kind != "park-generation" {
326 return fmt.Errorf("%w: park scope", ErrMalformed)
327 }
328 if err := o.Park.Validate(c.Host); err != nil {
329 return err
330 }
331 }
332 seen[o.ID] = true
333 switch o.Kind {
334 case "relocation-release":
335 if o.Region == "" || !validBoxReference(o.Box) || o.Box == "none" || o.Generation != "" || o.ParkTarget != "" || o.Owner != "region:"+o.Region {
336 return fmt.Errorf("%w: relocation-release identity", ErrMalformed)
337 }
338 case "park-generation":
339 if o.Region == "" || o.Generation == "" || o.Box != "" || !validRegionalOwner(o.Owner, o.Region) {
340 return fmt.Errorf("%w: park-generation %q location/generation/owner", ErrMalformed, o.ID)
341 }
342 case "park-replica":
343 if o.Region == "" || o.Box != "" || o.Generation != "" || !validRegionalOwner(o.Owner, o.Region) {
344 return fmt.Errorf("%w: park-replica %q location/owner", ErrMalformed, o.ID)
345 }
346 case "park-local":
347 if o.Box == "" || o.Region != "" || o.Generation != "" || o.Owner != "box:"+o.Box {
348 return fmt.Errorf("%w: park-local %q location/owner", ErrMalformed, o.ID)
349 }
350 default:
351 return fmt.Errorf("%w: obligation kind %q", ErrMalformed, o.Kind)
352 }
353 }
354 return nil
355}
356
357func holderlessSweep(c *Claim) bool {
358 if c.Holder != nil || c.Op.Kind != KindParking || c.Op.ID == "" || c.Op.Intent["park_sweep"] != "v1" || c.Op.Exec != "" || len(c.Obligations) == 0 {
359 return false
360 }
361 for _, o := range c.Obligations {
362 if o.Op != c.Op.ID || o.Park == nil || (o.Kind != "park-replica" && o.Kind != "park-generation") {
363 return false
364 }
365 }
366 return true
367}
368