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 "encoding/json"
6 "errors"
7 "fmt"
8 "io"
9 "os"
10 "path/filepath"
11
12 "github.com/carlosframework/platform/internal/release"
13 "github.com/carlosframework/platform/internal/seeds"
14)
15
16// The seeds protocol floor on a box (seeded-instances spec §3.3, codex 16):
17// the moves floor's mechanism (movesfloor.go), beside it. Its two local
18// files live in the same .moves directory and are written under the same
19// floor.lock; its verdict on carlos.prev sits beside the moves verdict; the
20// two system units carry a second clause for it; the activation rule
21// checks it as well. A deployment that never publishes it has none of the
22// files, and every path here then does what it did before.
23
24func seedsPrevAllowedPath(optDir string) string { return filepath.Join(optDir, seeds.PrevAllowedFile) }
25
26// readLocalSeedsFloor reads <dataDir>/.moves/seeds-floor.json without
27// following a symlink below dataDir. (nil, nil): not armed. A file that is
28// there but unusable is an error, which callers treat as armed and unknown.
29func readLocalSeedsFloor(dataDir string) (*seeds.Floor, error) {
30 f, err := openBeneath(dataDir, filepath.Dir(seeds.FloorFile), filepath.Base(seeds.FloorFile))
31 if errors.Is(err, os.ErrNotExist) {
32 return nil, nil
33 }
34 if err != nil {
35 return nil, err
36 }
37 defer f.Close()
38 st, err := f.Stat()
39 if err != nil {
40 return nil, err
41 }
42 if !st.Mode().IsRegular() || st.Size() > movesFloorMaxBytes {
43 return nil, fmt.Errorf("%s is not a regular file under %d bytes", f.Name(), movesFloorMaxBytes)
44 }
45 var fl seeds.Floor
46 if err := json.NewDecoder(io.LimitReader(f, movesFloorMaxBytes)).Decode(&fl); err != nil {
47 return nil, fmt.Errorf("%s: %w", f.Name(), err)
48 }
49 if ok, err := seeds.ReleaseAtOrAbove(fl.MinRelease, fl.MinRelease); !ok || err != nil {
50 return nil, fmt.Errorf("%s: min_release %q is not a release", f.Name(), fl.MinRelease)
51 }
52 return &fl, nil
53}
54
55// withSeedsFields sets the edges file's seeds fields from this box's own
56// files, read at each write, as withMovesFields does for the moves floor.
57func withSeedsFields(f edgesFile, dataDir, optDir string) edgesFile {
58 if _, err := os.Lstat(rootSeedsFloorDir(optDir)); f.MovesFloorState != "" || !errors.Is(err, os.ErrNotExist) {
59 return withRootSeedsFields(f, optDir)
60 }
61 f.SeedsProtocol = seeds.ProtocolVersion
62 f.SeedsFloorMinRelease = ""
63 if fl, err := readLocalSeedsFloor(dataDir); err == nil && fl != nil {
64 f.SeedsFloorMinRelease = fl.MinRelease
65 }
66 f.PrevSeedsAllowedFor, _ = readLine(seedsPrevAllowedPath(optDir))
67 return f
68}
69
70// seedsFloorArmed: a local seeds floor that reads, or one that is there and
71// cannot be read.
72func seedsFloorArmed(dataDir string) bool {
73 f, err := readLocalSeedsFloor(dataDir)
74 return err != nil || f != nil
75}
76
77// writeSeedsFloorFiles writes the min-release line FIRST and the JSON LAST,
78// for writeFloorFiles' reason: after a crash between them the line can only
79// be ahead of the JSON, so the units never pick a carlos.prev the Go checks
80// would refuse.
81func writeSeedsFloorFiles(d *movesDir, f seeds.Floor) error {
82 body, err := json.Marshal(f)
83 if err != nil {
84 return err
85 }
86 if err := d.WriteDurable(filepath.Base(seeds.FloorMinReleaseFile), []byte(f.MinRelease+"\n"), 0o644); err != nil {
87 return err
88 }
89 return d.WriteDurable(filepath.Base(seeds.FloorFile), append(body, '\n'), 0o644)
90}
91
92// syncLocalSeedsFloor is syncLocalFloor's rule for the seeds floor: a
93// published, usable floor that is not lower is written under the floor
94// lock; absent leaves what is on disk; a lower one is refused; an
95// unreadable store leaves what is on disk and returns the error.
96func syncLocalSeedsFloor(ctx context.Context, s release.Store, dataDir string) (*seeds.Floor, error) {
97 if s == nil {
98 local, lerr := readLocalSeedsFloor(dataDir)
99 return local, errors.Join(errors.New("no deployment store to read the seeds floor from"), lerr)
100 }
101 remote, rerr := seeds.ReadFloor(ctx, s)
102 switch {
103 case rerr != nil:
104 local, lerr := readLocalSeedsFloor(dataDir)
105 return local, errors.Join(rerr, lerr)
106 case remote == nil:
107 return readLocalSeedsFloor(dataDir)
108 }
109 if ok, err := seeds.ReleaseAtOrAbove(remote.MinRelease, remote.MinRelease); !ok || err != nil {
110 local, lerr := readLocalSeedsFloor(dataDir)
111 return local, errors.Join(fmt.Errorf("the published seeds min_release %q is not a release; the seeds floor on this box stays as it was", remote.MinRelease), lerr)
112 }
113 var out *seeds.Floor
114 ran := false
115 err := withFloorLock(dataDir, true, func(d *movesDir) error {
116 ran = true
117 local, lerr := readLocalSeedsFloor(dataDir)
118 if lerr == nil && local != nil {
119 if remote.Min < local.Min {
120 out = local
121 return fmt.Errorf("the published seeds floor's protocol minimum %d is below this box's %d", remote.Min, local.Min)
122 }
123 if ok, _ := seeds.ReleaseAtOrAbove(remote.MinRelease, local.MinRelease); !ok {
124 out = local
125 return fmt.Errorf("the published seeds min_release %q is below this box's %s", remote.MinRelease, local.MinRelease)
126 }
127 if *local == *remote {
128 if line, ok := readBeneathLine(dataDir, seeds.FloorMinReleaseFile); ok && line == remote.MinRelease {
129 out = local
130 return nil
131 }
132 }
133 }
134 if err := writeSeedsFloorFiles(d, *remote); err != nil {
135 out, _ = readLocalSeedsFloor(dataDir)
136 return fmt.Errorf("%w: %v", errFloorWrite, err)
137 }
138 out = remote
139 return nil
140 })
141 if !ran {
142 out, _ = readLocalSeedsFloor(dataDir)
143 return out, fmt.Errorf("%w: %v", errFloorWrite, err)
144 }
145 return out, err
146}
147
148// judgeSeedsPrev writes the seeds verdict when carlos.prev is at or above
149// the armed seeds floor, and removes it otherwise (no carlos.prev, no
150// floor, no version, below). Unlike the moves verdict it does not read the
151// registry: a seed has no hold rows.
152func judgeSeedsPrev(optDir, dataDir string) error {
153 deny := func(cause error) error {
154 if err := removeDurable(seedsPrevAllowedPath(optDir)); err != nil {
155 return errors.Join(cause, fmt.Errorf("remove the seeds verdict on carlos.prev: %w", err))
156 }
157 return cause
158 }
159 if _, err := os.Stat(systemPrevPath(optDir)); err != nil {
160 return deny(nil)
161 }
162 ran := false
163 err := withFloorLock(dataDir, false, func(*movesDir) error {
164 ran = true
165 floor, err := readLocalSeedsFloor(dataDir)
166 if err != nil {
167 return deny(fmt.Errorf("seeds protocol floor: the floor on this box cannot be read: %v", err))
168 }
169 if floor == nil {
170 return deny(nil)
171 }
172 version, ok := readLine(prevVersionPath(optDir))
173 if !ok {
174 return deny(nil)
175 }
176 if at, _ := seeds.ReleaseAtOrAbove(version, floor.MinRelease); !at {
177 return deny(nil)
178 }
179 return writeFileDurable(seedsPrevAllowedPath(optDir), []byte(floor.MinRelease+"\n"), 0o644)
180 })
181 if !ran {
182 return deny(fmt.Errorf("seeds protocol floor: the floor on this box cannot be read: %v", err))
183 }
184 return err
185}
186
187// seedsActivationRefusal is the seeds half of the activation rule: version
188// below the armed seeds floor is refused (SD-N1). The floor is read from
189// the local file alone (boot recovery runs before the network). Unarmed:
190// nil. A version of "" (a carlos.prev that records none) is below any
191// floor: seeds.ReleaseAtOrAbove("", ...) is false.
192func seedsActivationRefusal(version, dataDir string) error {
193 floor, err := readLocalSeedsFloor(dataDir)
194 if err != nil {
195 return refuse("seeds protocol floor: the floor on this box cannot be read: %v", err)
196 }
197 if floor == nil {
198 return nil
199 }
200 if ok, _ := seeds.ReleaseAtOrAbove(version, floor.MinRelease); !ok {
201 return refuse("seeds protocol floor: %s is below %s", version, floor.MinRelease)
202 }
203 return nil
204}
205
206// armSeedsFloor is armFloor's seeds half, with the same rule: a published
207// floor this box could not write down defers the apply; anything else is a
208// log line.
209func (a *systemApplier) armSeedsFloor(ctx context.Context) error {
210 if id, required, err := a.rootMoves.read(); err != nil {
211 return err
212 } else if required {
213 return refreshRootSeedsFloor(ctx, a.store, a.optDir, a.dataDir, id)
214 }
215 f, err := syncLocalSeedsFloor(ctx, a.store, a.dataDir)
216 if err == nil {
217 return nil
218 }
219 if errors.Is(err, errFloorWrite) {
220 return fmt.Errorf("%w: seeds protocol floor: %v", errSystemDeferred, err)
221 }
222 onDisk := "none"
223 if f != nil {
224 onDisk = f.MinRelease
225 }
226 a.ops.logf("system apply: seeds protocol floor: %v (the seeds floor on this box stays %s)", err, onDisk)
227 return nil
228}
229
230// rootJudgeSeedsPrev is the root adopt pass's call: nothing to do on a box
231// that has neither a seeds floor nor a seeds verdict.
232func rootJudgeSeedsPrev() error {
233 optDir, dd := release.OptDir(), dataDir()
234 if !seedsFloorArmed(dd) {
235 if _, err := os.Stat(seedsPrevAllowedPath(optDir)); errors.Is(err, os.ErrNotExist) {
236 return nil
237 }
238 }
239 return withPrevLock(optDir, func() error { return judgeSeedsPrev(optDir, dd) })
240}
241
242// seedsFloorRefresh is the agent's hourly refresh of the seeds floor.
243func seedsFloorRefresh(ctx context.Context, s release.Store, dataDir string, logf func(string, ...any)) {
244 if _, err := syncLocalSeedsFloor(ctx, s, dataDir); err != nil {
245 logf("agent: seeds protocol floor refresh: %v", err)
246 }
247}
248