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 "encoding/json"
6 "reflect"
7 "time"
8
9 "github.com/carlosframework/platform/internal/hostclaim"
10 "github.com/carlosframework/platform/internal/park"
11 "github.com/carlosframework/platform/internal/provision"
12 "github.com/carlosframework/platform/internal/release"
13)
14
15type lifecycleRecordKey struct{}
16type lifecycleRecordPermit struct{ Observation hostclaim.Observed }
17
18// setLifecycleRecordMove carries the exact claim observation into the record CAS.
19// A caller retries from its operation guard after any changed observation.
20func setLifecycleRecordMove(ctx context.Context, s release.Store, o hostclaim.Observed, mut func(*provision.Record) error) (*provision.Record, error) {
21 if o.Absent || o.Claim == nil || o.Claim.Holder == nil || o.ETag == "" || mut == nil {
22 return nil, ErrIdentity
23 }
24 h := o.Claim.Holder
25 return provision.SetRecordMove(context.WithValue(provision.WithRecordSequence(ctx), lifecycleRecordKey{}, lifecycleRecordPermit{o}), s, h.Account, h.App, o.Host, mut)
26}
27
28func (s conditionalRecordStore) putLifecycle(ctx context.Context, key string, body []byte, etag string, p lifecycleRecordPermit) (string, error) {
29 o := p.Observation
30 if o.Absent || o.Claim == nil || o.Claim.Holder == nil || o.ETag == "" || etag == "" || key != o.Claim.Holder.Key {
31 return "", ErrIdentity
32 }
33 if !operationHomeMatches(s.homeRegion, o.Claim) {
34 return "", ErrIdentity
35 }
36 switch o.Claim.Op.Kind {
37 case hostclaim.KindMoving, hostclaim.KindLanding, hostclaim.KindRestoring, hostclaim.KindReverting, hostclaim.KindRelocating:
38 default:
39 return "", ErrPending
40 }
41 old, err := ReadRecord(ctx, s.raw, key)
42 if err != nil {
43 return "", err
44 }
45 if old.ETag != etag {
46 return "", release.ErrCASConflict
47 }
48 if !matches(old, o.Claim.Holder) {
49 return "", ErrIdentity
50 }
51 var before, next map[string]json.RawMessage
52 if json.Unmarshal(old.Body, &before) != nil || json.Unmarshal(body, &next) != nil {
53 return "", ErrIdentity
54 }
55 type rollback struct {
56 ID string `json:"id"`
57 RequestedAt *time.Time `json:"requested_at"`
58 TargetPhase string `json:"target_phase"`
59 SourcePhase string `json:"source_phase"`
60 ReleaseDeadline *time.Time `json:"release_deadline"`
61 }
62 type move struct {
63 CutPhase string `json:"cut_phase"`
64 FlippedAt *time.Time `json:"flipped_at"`
65 CommittedAt *time.Time `json:"committed_at"`
66 LandingDeadline *time.Time `json:"landing_deadline"`
67 Rollback *rollback `json:"rollback"`
68 Relocation json.RawMessage `json:"relocation"`
69 ID string `json:"id"`
70 Incarnation string `json:"incarnation"`
71 From string `json:"from"`
72 To string `json:"to"`
73 State string `json:"state"`
74 }
75 var a, b move
76 if json.Unmarshal(next["move"], &b) != nil || b.ID != o.Claim.Op.ID || b.Incarnation != o.Claim.Holder.Incarnation {
77 return "", ErrIdentity
78 }
79 if raw := before["move"]; len(raw) > 0 && string(raw) != "null" {
80 if json.Unmarshal(raw, &a) != nil || a.ID != b.ID || a.Incarnation != b.Incarnation || a.From != b.From || a.To != b.To {
81 return "", ErrIdentity
82 }
83 }
84 if a.Rollback != nil && (b.Rollback == nil || a.Rollback.ID != b.Rollback.ID) {
85 return "", ErrIdentity
86 }
87 if len(a.Relocation) > 0 && string(a.Relocation) != "null" && (!reflect.DeepEqual(a.Relocation, b.Relocation) || b.State != "relocating") {
88 return "", ErrIdentity
89 }
90 var from, to string
91 if (len(before["region"]) != 0 && json.Unmarshal(before["region"], &from) != nil) || (len(next["region"]) != 0 && json.Unmarshal(next["region"], &to) != nil) || !provision.ValidRegion(from) || !provision.ValidRegion(to) {
92 return "", ErrIdentity
93 }
94 if from != to {
95 flip := o.Claim.Op.Kind == hostclaim.KindMoving && o.Claim.Op.Phase == "sealed" && a.State == "cutting" && a.CutPhase == "archived" && a.FlippedAt == nil && a.CommittedAt == nil && a.Rollback == nil && b.State == "landing" && b.CutPhase == "flipped" && b.FlippedAt != nil && b.LandingDeadline != nil && b.LandingDeadline.After(*b.FlippedAt) && b.Rollback == nil && from == b.From && to == b.To && park.PlacementMatches(o.Claim.Op.Intent["home_region"], to, o.Claim.Op.Intent["to"])
96 restore := o.Claim.Op.Kind == hostclaim.KindLanding && o.Claim.Op.Phase != "committed" && a.State == "landing" && a.CommittedAt == nil && a.Rollback == nil && b.State == "rolling_back" && b.CommittedAt == nil && b.Rollback != nil && b.Rollback.ID != "" && b.Rollback.RequestedAt != nil && b.Rollback.TargetPhase == "releasing" && b.Rollback.SourcePhase == "waiting" && b.Rollback.ReleaseDeadline != nil && b.Rollback.ReleaseDeadline.Equal(b.Rollback.RequestedAt.Add(15*time.Minute)) && from == b.To && to == b.From
97 // The rollback region is the original record's Move.From (a.From ==
98 // b.From above), never claim input: the claim carries no source region.
99 if !flip && !restore {
100 return "", ErrIdentity
101 }
102 }
103 // The provision writer owns this field; WriteRecord validates its exact
104 // base and prepared counter at the final conditional Put.
105 delete(before, "record_seq")
106 delete(next, "record_seq")
107 delete(before, "move")
108 delete(next, "move")
109 delete(before, "region")
110 delete(next, "region")
111 // Compare decoded values, so indentation is immaterial but every other field is held.
112 var av, bv any
113 ar, _ := json.Marshal(before)
114 br, _ := json.Marshal(next)
115 if json.Unmarshal(ar, &av) != nil || json.Unmarshal(br, &bv) != nil || !reflect.DeepEqual(av, bv) {
116 return "", ErrIdentity
117 }
118 fresh, err := hostclaim.Read(ctx, s.raw, o.Host)
119 if err != nil {
120 return "", err
121 }
122 if fresh.Absent || fresh.ETag != o.ETag {
123 return "", hostclaim.ErrConflict
124 }
125 h := o.Claim.Holder
126 appID, id := o.Claim.Op.Intent["app_id"], o.Claim.Op.Intent["admission_id"]
127 // The move registration is named by the move's own op id (RegisterMove);
128 // any other id is drainable while the move still holds the claim.
129 if id == "" || id != o.Claim.Op.ID {
130 return "", ErrIdentity
131 }
132 admission, err := ReadAdmission(ctx, s.raw, h.Account, h.App, appID)
133 if err != nil {
134 return "", err
135 }
136 reg, ok := admission.Value.Registrations[id]
137 if !ok || reg.Kind != "move" || reg.AppID != appID || reg.Epoch != admission.Value.Epoch || reg.Host != o.Host || reg.Incarnation != h.Incarnation {
138 return "", ErrPending
139 }
140 // Accepted work drains under closed admission; expiry does not revoke it.
141 MarkAppMutationIssued(ctx)
142 return putSequencedRecord(ctx, s.raw, key, body, etag)
143}
144
145func sameMoveJournal(a, b []byte) bool {
146 var before, after map[string]any
147 if json.Unmarshal(a, &before) != nil || json.Unmarshal(b, &after) != nil {
148 return false
149 }
150 return reflect.DeepEqual(before["move"], after["move"])
151}
152