carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package maintenanceguard
2
3import (
4 "context"
5 "errors"
6 "math"
7 "os"
8 "slices"
9)
10
11type GuardSnapshot struct {
12 repository *Repository
13 value Guard
14 etag string
15 exists bool
16}
17
18func (s GuardSnapshot) Exists() bool { return s.exists }
19func (s GuardSnapshot) Value() Guard { return cloneGuard(s.value) }
20func (s GuardSnapshot) Projection() (Projection, error) {
21 if !s.exists {
22 return Projection{}, ErrInvalid
23 }
24 raw, e := guardBytes(s.value)
25 if e != nil {
26 return Projection{}, e
27 }
28 return Projection{Version, s.value.Generation, digest(raw)}, nil
29}
30func (r *Repository) ReadGuard(ctx context.Context, host string) (GuardSnapshot, error) {
31 key, e := GuardKey(host)
32 if e != nil {
33 return GuardSnapshot{}, e
34 }
35 raw, etag, e := r.store.Get(ctx, key)
36 if errors.Is(e, os.ErrNotExist) {
37 return GuardSnapshot{repository: r}, nil
38 }
39 if e != nil {
40 return GuardSnapshot{}, e
41 }
42 var g Guard
43 if decode(raw, &g) != nil || !g.valid() || g.Identity.Host != host || !cleanETag(etag) {
44 return GuardSnapshot{}, ErrInvalid
45 }
46 if e = r.checkGuardReferences(ctx, g); e != nil {
47 return GuardSnapshot{}, e
48 }
49 return GuardSnapshot{r, cloneGuard(g), etag, true}, nil
50}
51func (r *Repository) guardClaim(ctx context.Context, c ClaimSnapshot, g Guard) error {
52 if c.value.Kind != MaintenanceOperation || c.value.Account != g.Identity.Account || c.value.App != g.Identity.App || c.value.OperationID != g.OperationID {
53 return ErrConflict
54 }
55 return r.currentClaim(ctx, c)
56}
57func (r *Repository) writeGuard(ctx context.Context, c ClaimSnapshot, g Guard, etag string) (GuardSnapshot, error) {
58 if e := r.guardClaim(ctx, c, g); e != nil {
59 return GuardSnapshot{}, e
60 }
61 raw, e := guardBytes(g)
62 if e != nil {
63 return GuardSnapshot{}, e
64 }
65 if e = r.checkGuardReferences(ctx, g); e != nil {
66 return GuardSnapshot{}, e
67 }
68 key, _ := GuardKey(g.Identity.Host)
69 next, e := r.store.Put(ctx, key, raw, etag)
70 if e != nil {
71 return GuardSnapshot{}, e
72 }
73 if !cleanETag(next) {
74 return GuardSnapshot{}, ErrUnavailable
75 }
76 // There is no cross-object atomicity. A lost claim leaves the durable guard
77 // in place, but the caller receives no successful execution/resume handoff.
78 if e = r.currentClaim(ctx, c); e != nil {
79 return GuardSnapshot{}, e
80 }
81 return GuardSnapshot{r, cloneGuard(g), next, true}, nil
82}
83func (r *Repository) CreateGuard(ctx context.Context, c ClaimSnapshot, g Guard) (GuardSnapshot, error) {
84 if !g.valid() || g.Generation != 1 || g.Phase != Requested || g.Evidence != nil {
85 return GuardSnapshot{}, ErrInvalid
86 }
87 return r.writeGuard(ctx, c, g, "")
88}
89func newAttempt(old, next Guard) bool {
90 return old.Request.ID != next.Request.ID && old.Attempt.ID != next.Attempt.ID && next.Evidence == nil
91}
92func samePins(old, next Guard) bool {
93 return old.RecordRevision == next.RecordRevision && old.ArtifactVersion == next.ArtifactVersion && old.ArtifactDigest == next.ArtifactDigest && old.ConfigDigest == next.ConfigDigest && slices.Equal(old.RequiredContracts, next.RequiredContracts)
94}
95func validAdvance(old, next Guard) bool {
96 if !next.valid() || old.Generation == math.MaxInt64 || next.Generation != old.Generation+1 || next.Identity != old.Identity || next.OwnerBox != old.OwnerBox || !containsAll(next.RequiredContracts, old.RequiredContracts) {
97 return false
98 }
99 if old.Phase == Resumed {
100 return next.Phase == Requested && next.OperationID != old.OperationID && newAttempt(old, next)
101 }
102 if next.OperationID != old.OperationID {
103 return false
104 }
105 if next.Phase == Requested {
106 return (old.Phase == Succeeded || old.Phase == Failed || old.Phase == Ambiguous) && newAttempt(old, next)
107 }
108 if next.Phase == ResumeRequested {
109 return (old.Phase == Fenced || old.Phase == Succeeded || old.Phase == Failed || old.Phase == Ambiguous) && newAttempt(old, next)
110 }
111 if old.Request != next.Request || old.Attempt != next.Attempt || !samePins(old, next) {
112 return false
113 }
114 switch old.Phase {
115 case Requested:
116 return next.Phase == Quiescing || next.Phase == Failed || next.Phase == Ambiguous
117 case Quiescing:
118 return next.Phase == Fenced || next.Phase == Failed || next.Phase == Ambiguous
119 case Fenced:
120 return next.Phase == Executing || next.Phase == Failed || next.Phase == Ambiguous
121 case Executing:
122 return next.Phase == Succeeded || next.Phase == Failed || next.Phase == Ambiguous
123 case ResumeRequested:
124 return next.Phase == Resumed || next.Phase == Failed || next.Phase == Ambiguous
125 }
126 return false
127}
128
129// AdvanceGuard validates continuity and state progression, then performs one
130// CAS. Fresh owner approval, stopped-process/lease proofs and checker semantics
131// remain caller preconditions; valid references alone do not prove them.
132func (r *Repository) AdvanceGuard(ctx context.Context, c ClaimSnapshot, old GuardSnapshot, next Guard) (GuardSnapshot, error) {
133 if old.repository != r || !old.exists || !cleanETag(old.etag) || !validAdvance(old.value, next) {
134 return GuardSnapshot{}, ErrConflict
135 }
136 return r.writeGuard(ctx, c, next, old.etag)
137}
138
139type ServingDecision string
140
141const (
142 Unmanaged ServingDecision = "unmanaged"
143 Managed ServingDecision = "managed"
144 Denied ServingDecision = "denied"
145)
146
147// CheckServing is a denial check, not a lease or process-health permit. The
148// caller supplies contracts from an already verified signed release manifest,
149// verifies the current record identity, and repeats this check after lease
150// acquisition before starting or seeding anything. Legacy unmanaged records may
151// omit Record: both barriers must be absent before that omission is accepted.
152func (r *Repository) CheckServing(ctx context.Context, identity Identity, projection *Projection, verifiedContracts []string) (ServingDecision, error) {
153 if !identity.locationValid() || !ValidContracts(verifiedContracts) {
154 return Denied, ErrDenied
155 }
156 s, e := r.ReadGuard(ctx, identity.Host)
157 if e != nil {
158 return Denied, ErrDenied
159 }
160 if !s.exists {
161 if projection == nil {
162 return Unmanaged, nil
163 }
164 return Denied, ErrDenied
165 }
166 if !identity.valid() || projection == nil || !projection.valid() || s.value.Identity != identity || s.value.Phase != Resumed || !containsAll(verifiedContracts, s.value.RequiredContracts) {
167 return Denied, ErrDenied
168 }
169 expected, e := s.Projection()
170 if e != nil || *projection != expected {
171 return Denied, ErrDenied
172 }
173 return Managed, nil
174}
175