carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package host
2
3import (
4 "context"
5 "github.com/carlosframework/platform/internal/activator"
6 "os"
7 "path/filepath"
8 "reflect"
9 "sort"
10 "strings"
11 "time"
12
13 "github.com/carlosframework/platform/internal/hostbox"
14 "github.com/carlosframework/platform/internal/hostclaim"
15 "github.com/carlosframework/platform/internal/hostfiles"
16 "github.com/carlosframework/platform/internal/instancefs"
17 "github.com/carlosframework/platform/internal/provision"
18 "github.com/carlosframework/platform/internal/regionname"
19 "github.com/carlosframework/platform/internal/registry"
20 "github.com/carlosframework/platform/internal/release"
21)
22
23type filesSeedSelection struct {
24 State string
25 Observation hostclaim.Observed
26 Chain []string
27 Env claimEnvFile
28}
29
30func inspectFilesSeed(ctx context.Context, claims release.Store, f *movesFiles, o hostclaim.Observed, actor hostbox.Actor, rt *registry.Route) (out filesSeedSelection, resultErr error) {
31 if ctx != nil {
32 var cancel context.CancelFunc
33 ctx, cancel = context.WithTimeout(ctx, hostbox.RequestLimit)
34 defer cancel()
35 }
36 out.Observation = o
37 out.State = "blocked"
38 if ctx == nil || claims == nil || f == nil || f.Data == nil || f.EnvFile == nil || !actor.Eligible || actor.Quarantined || actor.Enrollment == "" || actor.Revision == 0 || actor.Binding == "" || actor.Box != f.Box || actor.Bucket != f.Bucket || !regionname.SameRegion(f.HomeRegion, actor.Region, f.Region) {
39 return out, hostbox.ErrUnsupported
40 }
41 check := func() error {
42 fresh, err := hostclaim.Read(ctx, claims, o.Host)
43 if err != nil {
44 return err
45 }
46 if fresh.ETag != o.ETag || fresh.Absent != o.Absent || !reflect.DeepEqual(fresh.Claim, o.Claim) {
47 return hostbox.ErrChanged
48 }
49 return ctx.Err()
50 }
51 if err := check(); err != nil {
52 return out, err
53 }
54 defer func() {
55 if resultErr == nil {
56 resultErr = check()
57 }
58 }()
59 if o.Absent || o.Claim == nil || o.Claim.Holder == nil || o.Claim.Op.Kind != hostclaim.KindServing || o.Claim.Op.Exec != actor.Box || o.Claim.Op.ID != "" || o.Claim.Op.Phase != "" {
60 out.State = "outOfScope"
61 return out, nil
62 }
63 if rt == nil || rt.Host != o.Host {
64 out.State = "noRow"
65 return out, hostbox.ErrChanged
66 }
67 if rt.Replica {
68 out.State = "replica"
69 return out, nil
70 }
71 if rt.Seed != "" || rt.Backing != "" || rt.Unit != "" || rt.IsSidecar() || rt.Tunnel != "" {
72 out.State = "unsupported"
73 return out, nil
74 }
75 if rt.Account != o.Claim.Holder.Account || rt.App != o.Claim.Holder.App {
76 return out, hostbox.ErrChanged
77 }
78 env, err := checkFilesSeedLayout(f, *rt, claimEmptyBootstrap(o.Claim.Holder))
79 if err != nil {
80 out.State = "layoutBlocked"
81 return out, err
82 }
83 out.Env = env
84 if _, pending := hostclaim.PendingMoveFenceLoss(o.Claim); pending {
85 return out, hostclaim.ErrFence
86 }
87 for key := range o.Claim.Op.Journal {
88 if strings.HasPrefix(key, "fence.") {
89 return out, hostclaim.ErrFence
90 }
91 }
92 h := o.Claim.Holder
93 if claimEmptyBootstrap(h) {
94 versions, err := f.Data.ListVersions(ctx, f.Bucket, strings.TrimSuffix(h.Location, "/")+"/")
95 if err != nil {
96 return out, err
97 }
98 if len(versions) != 0 {
99 return out, hostbox.ErrChanged
100 }
101 out.State = "emptyBootstrap"
102 return out, nil
103 }
104 chain, err := filesSeedChain(h, f.HomeRegion, f.Region, f.Box, o.Host)
105 if err != nil {
106 return out, err
107 }
108 out.Chain = chain
109 established, err := f.inspectSeedManifests(ctx, o, chain)
110 if err != nil {
111 return out, err
112 }
113 if established {
114 out.State = "established"
115 return out, nil
116 }
117 root, err := instancefs.OpenDirBeneath(env.Dir, o.Host)
118 if err != nil {
119 out.State = "sourceMissing"
120 return out, err
121 }
122 if err := root.Close(); err != nil {
123 return out, err
124 }
125 out.State = "eligible"
126 return out, nil
127}
128
129func checkFilesSeedLayout(f *movesFiles, rt registry.Route, emptyBootstrap bool) (claimEnvFile, error) {
130 var zero claimEnvFile
131 if f == nil || !filepath.IsAbs(f.DataDir) || rt.DBPath != provision.NestedDBPath(f.DataDir, rt.Host) {
132 return zero, hostbox.ErrChanged
133 }
134 env, err := f.EnvFile(rt.Host, rt.DBPath)
135 if err != nil {
136 return zero, err
137 }
138 if !env.Nested || env.Dir != provision.InstanceDir(f.DataDir, rt.Host) || env.Name != rt.Host+".env" {
139 return zero, hostbox.ErrChanged
140 }
141 parent, err := instancefs.OpenDirBeneath(f.DataDir, filepath.Join("instances", provision.InstanceID(rt.Host)))
142 if err != nil {
143 if emptyBootstrap && os.IsNotExist(err) {
144 return env, nil
145 }
146 return zero, err
147 }
148 if err := parent.Close(); err != nil {
149 return zero, err
150 }
151 return env, nil
152}
153
154// Retained admissions constrain the complete chain; they never prove an origin.
155// A gap or a foreign cold writer refuses even when no manifest is present.
156func filesSeedChain(h *hostclaim.Holder, home, region, box, host string) ([]string, error) {
157 if h == nil || h.WriterBox != box || h.Location != "instances/"+host || h.Incarnation == "" {
158 return nil, hostbox.ErrChanged
159 }
160 current := h.Generation
161 if current == "" {
162 if h.Session != 0 || len(h.Admissions) != 0 || h.Stamp == 0 {
163 return nil, hostbox.ErrChanged
164 }
165 return []string{""}, nil
166 }
167 seen := map[string]bool{}
168 var chain []string
169 session := h.Session
170 for current != "" {
171 if !movesFilesGeneration(current) || seen[current] {
172 return nil, hostbox.ErrChanged
173 }
174 seen[current] = true
175 chain = append(chain, current)
176 var entry *hostclaim.AdmissionRecord
177 for i := range h.Admissions {
178 a := &h.Admissions[i]
179 if a.Generation == current {
180 if entry != nil {
181 return nil, hostbox.ErrChanged
182 }
183 entry = a
184 }
185 }
186 if entry == nil || entry.Session != session || entry.Box != box || entry.Mode != "cold" || !regionname.SameRegion(home, entry.Region, region) || !regionname.SameRegion(home, entry.PredecessorRegion, region) {
187 return nil, hostbox.ErrChanged
188 }
189 current = entry.Predecessor
190 if current != "" {
191 found := false
192 for _, a := range h.Admissions {
193 if a.Generation == current && a.Session < session {
194 session = a.Session
195 found = true
196 break
197 }
198 }
199 if !found {
200 return nil, hostbox.ErrChanged
201 }
202 }
203 }
204 return append(chain, ""), nil
205}
206
207// Inspect the whole chain, including older entries after an established one.
208// A bad manifest is never absence and never grants permission to seed past it.
209func (f *movesFiles) inspectSeedManifests(ctx context.Context, o hostclaim.Observed, chain []string) (bool, error) {
210 found := false
211 for _, gen := range chain {
212 prefix := movesFilesPrefix(o.Claim.Holder.Location, gen)
213 key := hostfiles.ManifestKey(prefix)
214 version, err := f.manifestVersion(ctx, key)
215 if err != nil {
216 return false, err
217 }
218 if version == "" {
219 continue
220 }
221 found = true
222 m, present, err := hostfiles.Load(ctx, versionStore{data: f.Data, bucket: f.Bucket, key: key, version: version}, prefix)
223 if err != nil {
224 return false, err
225 }
226 if !present {
227 return false, hostfiles.ErrNoEvidence
228 }
229 if err := hostfiles.RestorableWithinCap(m); err != nil {
230 return false, err
231 }
232 if gen == "" {
233 return false, hostfiles.ErrOrigin
234 }
235 if _, err := f.movesFilesAllowed(ctx, o, hostclaim.Admission{}, o.Claim.Holder.Location, gen, m); err != nil {
236 return false, err
237 }
238 }
239 return found, nil
240}
241
242// Claims, including a local claim without a route, define the census. The
243// registry is consulted only after each authoritative observation is read.
244func filesSeedCandidates(ctx context.Context, claims release.Store, box, host string) ([]hostclaim.Observed, error) {
245 if claims == nil || box == "" || ctx == nil {
246 return nil, hostbox.ErrUnsupported
247 }
248 ctx, cancel := context.WithTimeout(ctx, hostbox.RequestLimit)
249 defer cancel()
250 if host != "" {
251 o, err := hostclaim.Read(ctx, claims, host)
252 if err != nil {
253 return nil, err
254 }
255 return []hostclaim.Observed{o}, nil
256 }
257 keys, err := claims.List(ctx, "control/hosts/")
258 if err != nil {
259 return nil, err
260 }
261 var out []hostclaim.Observed
262 for _, key := range keys {
263 tail, ok := strings.CutPrefix(key, "control/hosts/")
264 if !ok {
265 return nil, hostbox.ErrChanged
266 }
267 if strings.Contains(tail, "/") {
268 continue
269 }
270 host := strings.TrimSuffix(tail, ".json")
271 want, err := hostclaim.Key(host)
272 if err != nil || want != key {
273 return nil, hostbox.ErrChanged
274 }
275 o, err := hostclaim.Read(ctx, claims, host)
276 if err != nil {
277 return nil, err
278 }
279 if !o.Absent && o.Claim != nil && o.Claim.Op.Exec == box {
280 out = append(out, o)
281 }
282 }
283 sort.Slice(out, func(i, j int) bool { return out[i].Host < out[j].Host })
284 return out, nil
285}
286
287func selectFilesSeedInputs(ctx context.Context, inputs ClaimWakeInputs, rt registry.Route, selected filesSeedSelection) (ClaimLaunchInputs, error) {
288 if inputs == nil || selected.State != "eligible" {
289 return ClaimLaunchInputs{}, hostbox.ErrUnsupported
290 }
291 in, err := inputs.Select(ctx, rt, selected.Observation, activator.StartPolicy{MaxStarts: 5, Window: 30 * time.Second})
292 if err != nil {
293 return ClaimLaunchInputs{}, err
294 }
295 if len(in.Roles) != 1 || in.Roles[0] != "main" || in.Executable == "" || len(in.ExecutableSHA256) != 64 || in.DBPath != rt.DBPath || in.Identity.Config == "" || in.Identity.Release == "" || in.Admission.Mode != "cold" || in.Baseline == nil || in.Empty || in.Prepared {
296 return ClaimLaunchInputs{}, hostbox.ErrUnsupported
297 }
298 return in, nil
299}
300