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 "errors"
6 "fmt"
7 "log"
8 "net/http"
9 "sort"
10 "strings"
11 "time"
12
13 "github.com/carlosframework/platform/internal/seeds"
14 "github.com/carlosframework/platform/internal/status"
15)
16
17// seedRegionReady is the check both seed doors make before writing any seed
18// state, even awaiting-upload (seeded-instances spec §3.3, §6.3): this
19// console runs without claim control, the seeds floor is published, and
20// every box in region has armed it, excluded or vouched for its
21// carlos.prev, runs nested, and has no claim control. region is the record
22// vocabulary ("" is the home region); the home region's name is accepted
23// too. nil means go ahead.
24//
25// oneBox is the import door's extra rule: the swap, its journal and the
26// rollback live on the one box holding the legacy row, and a second box in
27// the region could finish an aborted import without its journal while the
28// swapping box is still rolling back. Until a multi-box design exists, an
29// import refuses a region with more than one box.
30//
31// Every missing, zero or unparseable field is NOT ready. An old box's edges
32// file reads as zeros, so its protocol is checked before anything else: a
33// box whose own local floor is unreadable publishes "" for its floor, and
34// must never be read as ready.
35func (s *Server) seedRegionReady(ctx context.Context, region string, oneBox bool) *doorRefusal {
36 region = s.homeRegionIn(region)
37 if s.cfg.HostClaim {
38 return seedClaimConsoleRefusal()
39 }
40 floor, err := seeds.ReadFloor(ctx, s.cfg.Releases)
41 if err == nil && floor != nil {
42 // A floor PublishFloor would never write (a hand-edited or
43 // truncated object) is as good as unreadable: an empty min_release
44 // would match every box that could not read its own floor.
45 if ok, perr := seeds.ReleaseAtOrAbove(floor.MinRelease, floor.MinRelease); !ok || perr != nil {
46 err = fmt.Errorf("published floor has no usable min_release %q", floor.MinRelease)
47 }
48 }
49 if err != nil {
50 log.Printf("console: seeds floor: %v", err)
51 return &doorRefusal{http.StatusServiceUnavailable, "could not read the seeds floor. Try again."}
52 }
53 if floor == nil {
54 return &doorRefusal{http.StatusConflict, "This deployment has no seeds floor yet. An operator publishes it with carlos system seeds-floor publish."}
55 }
56 minProtocol := max(seeds.RequiredProtocol, floor.Min)
57 name := s.homeRegionOut(region)
58 if name == "" {
59 name = "the home region"
60 }
61 unconfirmed := &doorRefusal{http.StatusServiceUnavailable, fmt.Sprintf("could not confirm every box in %s. Try again in a minute.", name)}
62 snap, err := s.liveEdges(ctx)
63 if err != nil || !snap.EvidenceComplete() {
64 if err != nil {
65 log.Printf("console: seeds readiness: edges: %v", err)
66 }
67 return unconfirmed
68 }
69 var inRegion, below []string
70 for _, f := range snap.Files {
71 if s.homeRegionIn(f.Placement) != region {
72 continue
73 }
74 inRegion = append(inRegion, f.Box)
75 if f.SeedsProtocol < minProtocol {
76 below = append(below, f.Box)
77 continue
78 }
79 prevOK := f.PrevRelease == "none" || f.PrevSeedsAllowedFor == floor.MinRelease
80 rootRefused := (f.MovesFloorState != "" || f.MovesFloorGeneration != 0 || f.SeedsFloorState != "" || f.SeedsFloorGeneration != 0) && (f.SeedsFloorState != "accepted" || f.SeedsFloorGeneration == 0)
81 if rootRefused || f.SeedsFloorMinRelease != floor.MinRelease || !prevOK {
82 below = append(below, f.Box)
83 }
84 }
85 if len(inRegion) == 0 {
86 return unconfirmed
87 }
88 if oneBox && len(inRegion) > 1 {
89 return &doorRefusal{http.StatusConflict, fmt.Sprintf("%s has more than one box. An import needs a region with one box for now.", name)}
90 }
91 if len(below) > 0 {
92 return &doorRefusal{http.StatusConflict, fmt.Sprintf("%s in %s are below the seeds floor. Roll the platform there, then publish the seeds floor again.", boxList(below), name)}
93 }
94 // The platform's own heartbeats, the set complete() vouched for. Never
95 // systemBoxes: it appends every customer fleet's heartbeats, and a fleet
96 // box sharing a label could stand in for a platform box. The join is
97 // the edges file's box against the heartbeat's Label, the pair
98 // readLiveEdgesSnapshot joins on. A label two heartbeats claim is
99 // ambiguous, so unconfirmed.
100 boxes, err := status.LoadBoxes(ctx, s.cfg.Releases)
101 if err != nil {
102 log.Printf("console: seeds readiness: boxes: %v", err)
103 return unconfirmed
104 }
105 byLabel := map[string]status.Box{}
106 dup := map[string]bool{}
107 for _, b := range boxes {
108 if _, seen := byLabel[b.Label]; seen {
109 dup[b.Label] = true
110 }
111 byLabel[b.Label] = b
112 }
113 // A region box with no heartbeat, a stale one, or one with no layout
114 // report is unconfirmed, never nested. The refusal stays generic; the
115 // log names the box, since a retired box whose edges file nobody
116 // deleted refuses here until someone does.
117 now := time.Now()
118 var flat, claimed []string
119 for _, label := range inRegion {
120 b, ok := byLabel[label]
121 why := ""
122 switch {
123 case !ok:
124 why = "no heartbeat"
125 case dup[label]:
126 why = "more than one heartbeat with this label"
127 case status.BoxStale(b, now, status.SnapshotPeriod):
128 why = "stale heartbeat, generated " + b.GeneratedAt.UTC().Format(time.RFC3339)
129 case b.Metrics == nil || b.Metrics.Layout == nil:
130 why = "no layout report"
131 }
132 if why != "" {
133 log.Printf("console: seeds readiness: box %q in region %q is unconfirmed: %s", label, name, why)
134 return unconfirmed
135 }
136 if b.Metrics.Layout.Mode != "nested" {
137 flat = append(flat, label)
138 }
139 if b.Metrics.HostClaimEnabled {
140 claimed = append(claimed, label)
141 }
142 }
143 if len(flat) > 0 {
144 return &doorRefusal{http.StatusConflict, fmt.Sprintf("%s in %s do not run instances nested yet. A seeded instance needs every box there nested.", boxList(flat), name)}
145 }
146 if len(claimed) > 0 {
147 return &doorRefusal{http.StatusConflict, fmt.Sprintf("%s in %s run with claim control on. Seeded instances are not supported there yet.", boxList(claimed), name)}
148 }
149 return nil
150}
151
152// seedClaimConsoleRefusal is the seed doors' answer on a console with claim
153// control on. The --from door also gives it for a source whose claim could
154// not be read, which seedRegionReady's own check makes unreachable today.
155func seedClaimConsoleRefusal() *doorRefusal {
156 return &doorRefusal{http.StatusConflict, "This console runs with claim control on. Seeded instances are not supported here yet."}
157}
158
159// boxList is the refusals' box names: sorted, comma-joined.
160func boxList(boxes []string) string {
161 sort.Strings(boxes)
162 return strings.Join(boxes, ", ")
163}
164
165// handleAPISystemSeedsFloor publishes the seeds floor (spec §3.3, §10 step
166// 2). The update doors' authority: an ops account owner or an address on
167// CARLOS_SYSTEM_OPERATORS (apiSystemRoller).
168func (s *Server) handleAPISystemSeedsFloor(w http.ResponseWriter, r *http.Request) {
169 addr, _, ok := s.apiSystemRoller(w, r)
170 if !ok {
171 return
172 }
173 // A console with no deployment bucket has nowhere to publish: the
174 // handler's own failure sentence, not a panic (final review carry item).
175 if s.cfg.Releases == nil {
176 jsonError(w, http.StatusServiceUnavailable, "could not publish the seeds floor. Try again.")
177 return
178 }
179 var req struct {
180 MinRelease string `json:"min_release"`
181 }
182 if !readJSON(w, r, &req) {
183 return
184 }
185 // The v prefix is required here: ReleaseAtOrAbove tolerates its absence,
186 // but the floor is stored and compared as typed, so "0.27.0" would sit in
187 // the record beside every "v0.27.0" the boxes report.
188 if ok, err := seeds.ReleaseAtOrAbove(req.MinRelease, req.MinRelease); !strings.HasPrefix(req.MinRelease, "v") || !ok || err != nil {
189 jsonError(w, http.StatusBadRequest, "min_release must be a release such as v0.27.0")
190 return
191 }
192 err := seeds.PublishFloor(r.Context(), s.cfg.Releases, seeds.Floor{Min: seeds.RequiredProtocol, MinRelease: req.MinRelease})
193 var lower *seeds.FloorLowerError
194 switch {
195 case errors.As(err, &lower):
196 jsonError(w, http.StatusConflict, fmt.Sprintf("the seeds floor is already %s. A floor only rises.", lower.Current))
197 return
198 case err != nil:
199 log.Printf("console: publish seeds floor: %v", err)
200 jsonError(w, http.StatusBadGateway, "could not publish the seeds floor. Try again.")
201 return
202 }
203 s.auditSystem(addr, "seeds-floor-publish", fmt.Sprintf("published the seeds floor at %s", req.MinRelease))
204 writeJSON(w, http.StatusOK, map[string]string{"min_release": req.MinRelease})
205}
206