carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package console
2
3import (
4 "context"
5 "sort"
6 "strings"
7 "time"
8
9 "github.com/carlosframework/platform/internal/release"
10 "github.com/carlosframework/platform/internal/status"
11)
12
13// layoutDowngradeBlocked is design §8.2's rollback rule at the console: a
14// channel pointer must not move below release.LayoutFloor while any box
15// could still hold a nested row. The box-side guard (host/systemapply.go)
16// refuses the install too, but only on the box that runs it; this one
17// stops the pointer moving at all, so no box has to refuse.
18//
19// It judges OBSERVED state, never the intent: a revert refused on a box
20// leaves a nested row under a flat intent. A box that has not reported in
21// time, or whose snapshot carries no layout at all, is unknown and blocks,
22// because the cost of guessing is a database hidden under a mount. The
23// empty string means the move is allowed.
24func (s *Server) layoutDowngradeBlocked(ctx context.Context, target string) string {
25 if !release.PreLayout(target) {
26 return ""
27 }
28 // Every fleet's boxes, the union carlos system status reads. Strict:
29 // a fleet whose heartbeats cannot be read is a fleet whose rows are
30 // unknown, and unknown blocks.
31 boxes, _, err := s.systemBoxes(ctx, true)
32 if err != nil {
33 return target + " predates the instance layout and the fleet's layout is unknown: could not read the boxes' snapshots: " + err.Error()
34 }
35 now := time.Now()
36 var reasons []string
37 for _, b := range boxes {
38 switch {
39 case status.BoxStale(b, now, status.SnapshotPeriod):
40 reasons = append(reasons, b.Label+" is stale")
41 case b.Metrics == nil || b.Metrics.Layout == nil:
42 // A box whose running release parses and is below the floor
43 // cannot have run the layout pass (the pass and its report
44 // arrived together, in the floor release), so its silence is
45 // flat: today's fleet, on v0.21.0 to v0.24.2, keeps its
46 // rollback. Silence at or above the floor, or from a box whose
47 // release is a sha or "dev" (which could be a build with the
48 // pass), is unknown and blocks. A box at or above the floor
49 // with no activator publishes a report of its own (Mode
50 // "unmanaged", host/layoutleg.go), so it is not silent here.
51 if b.Metrics != nil && release.BelowFloor(b.Metrics.Release, release.LayoutFloor) {
52 continue
53 }
54 reasons = append(reasons, b.Label+" reports no layout field")
55 default:
56 for host, row := range b.Metrics.Layout.Rows {
57 switch row.Layout {
58 case "nested", "fenced", "conflict":
59 reasons = append(reasons, b.Label+": "+host+" is "+row.Layout)
60 }
61 }
62 }
63 }
64 if len(reasons) == 0 {
65 return ""
66 }
67 sort.Strings(reasons)
68 return target + " predates the instance layout and the fleet is not flat: " + strings.Join(reasons, "; ") +
69 ". Run `carlos system layout revert`, wait for `carlos status --layout` to show every row flat, then retry"
70}
71
72// layoutMigrateBlocked refuses `layout migrate` while any box in scope
73// (every box for "", else the one labelled box) runs a release below
74// release.LayoutFloor, runs one this console cannot place and reports no
75// layout, or is stale: a nested row minted or moved on such a box would never
76// wake, so nothing may migrate before the floor release is cut and rolled.
77// A named box that no snapshot knows is refused too: an intent for a box
78// nobody can see is one nobody can check.
79func (s *Server) layoutMigrateBlocked(ctx context.Context, box string) string {
80 boxes, _, err := s.systemBoxes(ctx, true)
81 if err != nil {
82 return "layout migrate refused: could not read the boxes' snapshots: " + err.Error()
83 }
84 now := time.Now()
85 var reasons []string
86 seen := false
87 for _, b := range boxes {
88 if box != "" && b.Label != box {
89 continue
90 }
91 seen = true
92 switch {
93 case status.BoxStale(b, now, status.SnapshotPeriod):
94 reasons = append(reasons, b.Label+" is stale")
95 case b.Metrics == nil || b.Metrics.Release == "":
96 reasons = append(reasons, b.Label+" reports no release")
97 case release.BelowFloor(b.Metrics.Release, release.LayoutFloor):
98 reasons = append(reasons, b.Label+" runs "+b.Metrics.Release)
99 case release.PreLayout(b.Metrics.Release) && b.Metrics.Layout == nil:
100 // A sha or "dev" (a dev box tracking main) cannot be placed
101 // against the floor, but a layout report proves the build
102 // carries the pass, which is what the floor stands for; with
103 // no report there is no such proof.
104 reasons = append(reasons, b.Label+" runs "+b.Metrics.Release+", which cannot be placed against v"+release.LayoutFloor.String()+", and reports no layout")
105 }
106 }
107 if box != "" && !seen {
108 reasons = append(reasons, "box "+box+" has not reported")
109 }
110 if len(reasons) == 0 {
111 return ""
112 }
113 sort.Strings(reasons)
114 return "layout migrate refused: every box in scope must run v" + release.LayoutFloor.String() + " or later and be reporting: " + strings.Join(reasons, "; ")
115}
116
117// layoutRevertBlocked is #132's interlock on the layout door: a revert is
118// refused for any box in scope ("" is every box) that has a maintenance
119// worker head whose status is active or revoking. A worker's bearer
120// credential and its private input live on the box as the agent's uid,
121// and a flat tenant shares that uid and the agent's /proc view, so running
122// a tenant flat beside a worker hands it both. A detached head holds no
123// credential and does not block.
124//
125// Heads are read through cfg.WorkerHeads. On main the signed WorkerHead
126// shape exists (release/workercontrol.go) but no object key, store reader
127// or lifecycle writer does yet: #132's codec and store slice landed with
128// "no runtime caller". TODO(#132): wire WorkerHeads to the head store once
129// its key and reader land, and have the box-side revert pass refuse while
130// a durable worker token exists, as #132 also asks. Until then no worker
131// is enrolled anywhere (#132, #126), and a nil seam reads as "no heads".
132func (s *Server) layoutRevertBlocked(ctx context.Context, box string) string {
133 if s.cfg.WorkerHeads == nil {
134 return ""
135 }
136 heads, err := s.cfg.WorkerHeads(ctx)
137 if err != nil {
138 return "could not read the maintenance worker heads, so the revert is refused: " + err.Error()
139 }
140 var reasons []string
141 for _, h := range heads {
142 if box != "" && h.Box != box {
143 continue
144 }
145 if h.Status == "active" || h.Status == "revoking" {
146 reasons = append(reasons, h.Box+" has a maintenance worker ("+h.Status+")")
147 }
148 }
149 if len(reasons) == 0 {
150 return ""
151 }
152 sort.Strings(reasons)
153 return "layout revert refused: " + strings.Join(reasons, "; ") + ". A flat tenant shares the worker's user and can read its credential; detach the worker first"
154}
155