carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1// Package maintenanceguard stores denial state and lifecycle exclusion. Its
2// records do not prove human approval, process health, or lease ownership.
3package maintenanceguard
4
5import (
6 "amadan.net/carlos/platform.git/launchctx/wireid"
7 "crypto/sha256"
8 "encoding/binary"
9 "encoding/hex"
10 "encoding/json"
11 "errors"
12 "github.com/carlosframework/platform/internal/controlname"
13 "regexp"
14 "sort"
15 "strings"
16 "time"
17)
18
19const Version = 1
20const MaxContracts = 16
21const MaxReferenceBytes = 2 << 20
22
23var (
24 ErrUnavailable = errors.New("maintenance guard: unavailable")
25 ErrConflict = errors.New("maintenance guard: conflict")
26 ErrBusy = errors.New("maintenance guard: lifecycle claimed")
27 ErrDenied = errors.New("maintenance guard: serving denied")
28 atomRE = regexp.MustCompile(`^[a-zA-Z0-9._-]{1,64}$`)
29 contractRE = regexp.MustCompile(`^[a-z][a-z0-9]*([.-][a-z0-9]+)*$`)
30)
31
32func ValidID(s string) bool { return wireid.ValidID(s) }
33func ValidDigest(s string) bool { return wireid.ValidDigest(s) }
34func ValidContract(s string) bool { return len(s) > 0 && len(s) <= 64 && contractRE.MatchString(s) }
35func ValidContracts(set []string) bool {
36 if len(set) > MaxContracts {
37 return false
38 }
39 for i, s := range set {
40 if !ValidContract(s) || (i > 0 && set[i-1] >= s) {
41 return false
42 }
43 }
44 return true
45}
46func atom(s string) bool { return s != "." && s != ".." && atomRE.MatchString(s) }
47func revision(s string) bool {
48 if s == "" || len(s) > 256 {
49 return false
50 }
51 for _, c := range []byte(s) {
52 if c <= 32 || c > 126 || c == '*' {
53 return false
54 }
55 }
56 return true
57}
58func digest(raw []byte) string { sum := sha256.Sum256(raw); return hex.EncodeToString(sum[:]) }
59
60// RecordIdentity binds immutable creation fields. It is continuity metadata,
61// not proof against a privileged writer restoring or forging those fields.
62func RecordIdentity(account, app, host string, created time.Time) (string, error) {
63 if !controlname.ValidAccount(account) || !controlname.ValidApp(app) || !controlname.ValidHostShape(host) || created.IsZero() {
64 return "", ErrInvalid
65 }
66 stamp := created.UTC().Format(time.RFC3339Nano)
67 if parsed, e := time.Parse(time.RFC3339Nano, stamp); e != nil || !parsed.Equal(created) {
68 return "", ErrInvalid
69 }
70 raw := []byte("carlos-maintenance-record-identity-v1\x00")
71 for _, s := range []string{account, app, host, stamp} {
72 raw = binary.BigEndian.AppendUint32(raw, uint32(len(s)))
73 raw = append(raw, s...)
74 }
75 return digest(raw), nil
76}
77
78type Identity struct {
79 Account string `json:"account"`
80 App string `json:"app"`
81 Host string `json:"host"`
82 Record string `json:"record"`
83}
84
85func (i Identity) valid() bool {
86 return i.locationValid() && ValidDigest(i.Record)
87}
88func (i Identity) locationValid() bool {
89 return controlname.ValidAccount(i.Account) && controlname.ValidApp(i.App) && controlname.ValidHostShape(i.Host)
90}
91
92type Reference struct {
93 ID string `json:"id"`
94 Digest string `json:"digest"`
95}
96
97func (r Reference) valid() bool { return ValidID(r.ID) && ValidDigest(r.Digest) }
98
99type Phase string
100
101const (
102 Requested Phase = "requested"
103 Quiescing Phase = "quiescing"
104 Fenced Phase = "fenced"
105 Executing Phase = "executing"
106 Succeeded Phase = "succeeded"
107 Failed Phase = "failed"
108 Ambiguous Phase = "ambiguous"
109 ResumeRequested Phase = "resume-requested"
110 Resumed Phase = "resumed"
111)
112
113func (p Phase) valid() bool {
114 switch p {
115 case Requested, Quiescing, Fenced, Executing, Succeeded, Failed, Ambiguous, ResumeRequested, Resumed:
116 return true
117 }
118 return false
119}
120
121type Guard struct {
122 Version int `json:"version"`
123 Identity Identity `json:"identity"`
124 Generation int64 `json:"generation"`
125 Phase Phase `json:"phase"`
126 OperationID string `json:"operation_id"`
127 OwnerBox string `json:"owner_box"`
128 RecordRevision string `json:"record_revision"`
129 Request Reference `json:"request"`
130 Attempt Reference `json:"attempt"`
131 ArtifactVersion string `json:"artifact_version"`
132 ArtifactDigest string `json:"artifact_digest"`
133 ConfigDigest string `json:"config_digest"`
134 RequiredContracts []string `json:"required_contracts"`
135 Evidence *Reference `json:"evidence,omitempty"`
136}
137
138func (g Guard) valid() bool {
139 if g.Version != Version || !g.Identity.valid() || g.Generation <= 0 || !g.Phase.valid() || !ValidID(g.OperationID) || !atom(g.OwnerBox) || !revision(g.RecordRevision) || !g.Request.valid() || !g.Attempt.valid() || !atom(g.ArtifactVersion) || !ValidDigest(g.ArtifactDigest) || !ValidDigest(g.ConfigDigest) || !ValidContracts(g.RequiredContracts) {
140 return false
141 }
142 if g.Evidence != nil && !g.Evidence.valid() {
143 return false
144 }
145 return (g.Phase != Resumed && g.Phase != Succeeded) || (g.Evidence != nil && (g.Phase != Resumed || len(g.RequiredContracts) > 0))
146}
147func cloneGuard(g Guard) Guard {
148 g.RequiredContracts = append([]string{}, g.RequiredContracts...)
149 if g.Evidence != nil {
150 r := *g.Evidence
151 g.Evidence = &r
152 }
153 return g
154}
155func guardBytes(g Guard) ([]byte, error) {
156 if !g.valid() {
157 return nil, ErrInvalid
158 }
159 return json.Marshal(cloneGuard(g))
160}
161
162type Projection struct {
163 Version int `json:"version"`
164 Generation int64 `json:"generation"`
165 GuardDigest string `json:"guard_digest"`
166}
167
168func (p Projection) valid() bool {
169 return p.Version == Version && p.Generation > 0 && ValidDigest(p.GuardDigest)
170}
171func EncodeProjection(p Projection) ([]byte, error) {
172 if !p.valid() {
173 return nil, ErrInvalid
174 }
175 return json.Marshal(p)
176}
177func DecodeProjection(raw []byte) (Projection, error) {
178 var p Projection
179 if decode(raw, &p) != nil || !p.valid() {
180 return Projection{}, ErrInvalid
181 }
182 return p, nil
183}
184func containsAll(superset, subset []string) bool {
185 for _, s := range subset {
186 i := sort.SearchStrings(superset, s)
187 if i == len(superset) || superset[i] != s {
188 return false
189 }
190 }
191 return true
192}
193func hostBase(host string) (string, error) {
194 if !controlname.ValidHostShape(host) {
195 return "", ErrInvalid
196 }
197 return "control/maintenance/hosts/" + host + "/", nil
198}
199func GuardKey(host string) (string, error) {
200 p, e := hostBase(host)
201 if e != nil {
202 return "", e
203 }
204 return p + "guard.json", nil
205}
206func refKey(host, kind, id string) (string, error) {
207 p, e := hostBase(host)
208 if e != nil || !ValidID(id) {
209 return "", ErrInvalid
210 }
211 return p + kind + "/" + id + ".json", nil
212}
213func RequestKey(host, id string) (string, error) { return refKey(host, "requests", id) }
214func AttemptKey(host, id string) (string, error) { return refKey(host, "attempts", id) }
215func ReceiptKey(host, id string) (string, error) { return refKey(host, "receipts", id) }
216func appBase(account, app string) (string, error) {
217 if !controlname.ValidAccount(account) || !controlname.ValidApp(app) {
218 return "", ErrInvalid
219 }
220 return "control/maintenance/apps/" + account + "/" + app + "/", nil
221}
222func LifecycleKey(account, app string) (string, error) {
223 p, e := appBase(account, app)
224 if e != nil {
225 return "", e
226 }
227 return p + "lifecycle.json", nil
228}
229func cleanETag(s string) bool { return s != "" && len(s) <= 512 && !strings.ContainsAny(s, "\r\n\x00") }
230