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
3// The account-level Fleet tab: GET /a/{sqid}/fleet, the page that answers
4// "where does this account's work actually run?".
5//
6// It is a DIFFERENT page from the two that already carry the word fleet:
7// handleFleet (/fleet) is the operator's global platform status page, and
8// handleFleetPage (/a/{sqid}/fleets/{fleet}) is one of this account's own
9// fleets in detail. This one sits between them — member-readable, account
10// scoped, and the only place the shared platform fleet and the account's own
11// boxes are shown as two halves of one question.
12//
13// Two rules shape everything below:
14//
15// D2 — the shared fleet is a PRESENTATION, not a registry object. There is
16// no "shared fleet" row anywhere; the card is assembled from
17// status/fleet.json, the same PUBLIC aggregate the status page renders. The
18// raw status/boxes/* snapshots are deliberately NOT read here: they carry
19// per-box operational detail (release ids, load, memory) that the operator
20// page is gated to show and a tenant's page is not.
21//
22// Soft-fail everywhere — the overview.go/loadFleetPage posture. This page
23// makes three independent reads (the aggregate, the account's routes, its
24// fleets); any one of them having a bad moment costs that section and a
25// flash, never the page.
26
27import (
28 "context"
29 "log"
30 "net/http"
31 "sort"
32
33 "github.com/carlosframework/platform/internal/registry"
34)
35
36// sharedFleetCard is the platform's own fleet as this account sees it:
37// how many boxes are reporting, where they are, and whether any of THIS
38// account's apps are running on them.
39//
40// Boxes/Regions come from the public aggregate and are the same for every
41// viewer; Active/PlatformApps are this account's own placement split and are
42// not. Nothing here is per-box: a tenant learns that the shared fleet exists
43// and roughly where it is, never which machine is which.
44type sharedFleetCard struct {
45 Title string
46 Active bool // at least one of this account's apps runs here
47 Regions []string // distinct, sorted; a box with no region is simply absent
48 Boxes int
49 PlatformApps int // this account's distinct apps NOT placed on a fleet
50 // PlacementKnown is false when the read that decides Active/PlatformApps
51 // failed. Both fields then hold their zero values, which read as the
52 // POSITIVE claim "none of your apps run on the shared fleet" — a claim
53 // this account's routes were never actually consulted for. The card
54 // renders neither verdict when this is false: "we could not check" and
55 // "we checked, and no" are different sentences, and only one of them is
56 // true after a registry outage.
57 PlacementKnown bool
58}
59
60// accountFleetCard is one of the account's own fleets as the index row it
61// links through to the detail page with. Counts come from loadFleetPage —
62// the same function the detail page itself uses — so a row and the page it
63// clicks through to can never disagree about how many boxes a fleet has.
64type accountFleetCard struct {
65 Name string
66 Boxes int
67 Reachable int
68 Apps int
69}
70
71// assembleAccountFleet builds the tab's two halves: the shared platform
72// fleet from status/fleet.json, and the account's own fleets from the
73// registry. warn carries the FIRST read that failed, for a flash banner —
74// every failure degrades its own section to empty and nothing else.
75//
76// fleetsKnown is the fleets index's twin of sharedFleetCard.PlacementKnown,
77// and exists for the identical reason: a nil slice is the SAME value for
78// "this account has no fleets" and "the read that would have listed them
79// failed", and the first of those is a positive claim ("No fleets of your
80// own yet") that a failed registry read has not earned. False means the
81// section renders "unknown", never "none".
82func (s *Server) assembleAccountFleet(ctx context.Context, acct *Account) (card *sharedFleetCard, fleets []accountFleetCard, fleetsKnown bool, warn string) {
83 card = &sharedFleetCard{Title: "Shared fleet"}
84
85 // A console with no deployment bucket configured has no aggregate to
86 // read at all — that is the empty card, not an error.
87 if s.cfg.Releases != nil {
88 f, err := FleetView(ctx, s.cfg.Releases)
89 switch {
90 case err != nil:
91 // A read that FAILED is not the same claim as "no box is
92 // reporting" — the card stays empty either way, but only this
93 // branch admits the difference, rather than telling a tenant the
94 // platform has no boxes because S3 was slow. (FleetView already
95 // folds a not-yet-published object into (nil, nil), so reaching
96 // here means a real failure.)
97 log.Printf("console: account fleet %s: fleet view: %v", acct.Sqid, err)
98 warn = "Could not read the shared fleet's status."
99 case f != nil:
100 card.Boxes = len(f.Boxes)
101 seen := map[string]bool{}
102 for _, b := range f.Boxes {
103 if b.Region != "" && !seen[b.Region] {
104 seen[b.Region] = true
105 card.Regions = append(card.Regions, b.Region)
106 }
107 }
108 sort.Strings(card.Regions)
109 }
110 }
111
112 reg, err := openRegistry(s.cfg.Registry)
113 if err != nil {
114 log.Printf("console: account fleet %s: open registry: %v", acct.Sqid, err)
115 if warn == "" {
116 warn = "Could not reach the fleet registry."
117 }
118 return card, nil, false, warn
119 }
120
121 // The placement split. A route whose Region parses as a fleet placement
122 // runs on that fleet; everything else — "" and any future residency code
123 // FleetPlacement declines — runs on the shared fleet, which is exactly
124 // the fallback the platform itself applies. Counted by distinct APP, not
125 // by route, because "3 of your apps run here" is the sentence a tenant
126 // can act on; a host count is noise on this page.
127 //
128 // rt.Account is compared strictly, the same test loadFleetPage and
129 // overview.go make: an unstamped row is not this account's to claim on a
130 // page that goes on to say "of YOUR apps".
131 routes, err := reg.List()
132 if err != nil {
133 log.Printf("console: account fleet %s: list routes: %v", acct.Sqid, err)
134 if warn == "" {
135 warn = "Could not read where your apps are running."
136 }
137 } else {
138 platformApps := map[string]bool{}
139 for _, rt := range routes {
140 if rt.Account != acct.Sqid {
141 continue
142 }
143 if _, placed := registry.FleetPlacement(rt.Region); !placed {
144 platformApps[rt.App] = true
145 }
146 }
147 card.PlatformApps = len(platformApps)
148 card.Active = card.PlatformApps > 0
149 card.PlacementKnown = true
150 }
151
152 rows, err := reg.ListFleets()
153 if err != nil {
154 log.Printf("console: account fleet %s: list fleets: %v", acct.Sqid, err)
155 if warn == "" {
156 warn = "Could not list your fleets."
157 }
158 return card, nil, false, warn
159 }
160 for i := range rows {
161 // Fleet names are one flat, cross-account namespace (fleets.go's
162 // package doc comment), so this filter is the ONLY thing standing
163 // between this index and another account's fleet names. &fleets[i]
164 // rather than a ranged copy's address: loadFleetPage takes a pointer,
165 // and one that outlived a loop variable would be a bug waiting for
166 // the day this function keeps it.
167 f := &rows[i]
168 if f.Account != acct.Sqid {
169 continue
170 }
171 boxes, apps, fleetWarn := s.loadFleetPage(ctx, reg, acct, f)
172 if fleetWarn != "" && warn == "" {
173 // loadFleetPage's own wording says "this fleet", which has no
174 // referent on an index of several; say what it costs HERE.
175 warn = "Some fleet details could not be read — the counts below may be incomplete."
176 }
177 c := accountFleetCard{Name: f.Name, Boxes: len(boxes), Apps: len(apps)}
178 for _, b := range boxes {
179 if b.Reachable {
180 c.Reachable++
181 }
182 }
183 fleets = append(fleets, c)
184 }
185 return card, fleets, true, warn
186}
187
188// handleAccountFleetTab serves GET /a/{sqid}/fleet. Membership in the
189// account is the whole gate: the page is a read, so a member sees exactly
190// what an owner does (there is no mutation on it to withhold).
191func (s *Server) handleAccountFleetTab(w http.ResponseWriter, r *http.Request) {
192 addr, raw, acct, role, ok := s.account(w, r)
193 if !ok {
194 return
195 }
196 p := s.accountPage(w, r, addr, raw, acct, role)
197 p.Title = "Fleet"
198 p.ActiveNav = "fleet"
199 shared, own, ownKnown, warn := s.assembleAccountFleet(r.Context(), acct)
200 p.SharedFleet = shared
201 p.AccountFleets = own
202 p.FleetsKnown = ownKnown
203 if warn != "" && p.FlashErr == "" {
204 p.FlashErr = warn
205 }
206 s.render(w, http.StatusOK, "account_fleet", p)
207}
208
209// handleFleetBYO serves GET /a/{sqid}/fleet/byo: the "how to add and harden
210// a box" page the Fleet tab's button links to. It is static, per-deployment
211// content — the account only personalizes the eyebrow and header, same as
212// every other account page — so the handler is handleAccountFleetTab minus
213// the shared-fleet/own-fleets assembly: gate, page scaffold, render.
214func (s *Server) handleFleetBYO(w http.ResponseWriter, r *http.Request) {
215 addr, raw, acct, role, ok := s.account(w, r)
216 if !ok {
217 return
218 }
219 p := s.accountPage(w, r, addr, raw, acct, role)
220 p.Title = "Bring your own fleet"
221 p.ActiveNav = "fleet"
222 s.render(w, http.StatusOK, "fleet_byo", p)
223}
224