carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1// GET /fleet/architecture — the operator's map of the deployment: how a
2// request reaches a process, and everything the account is carrying that
3// makes that possible.
4//
5// The gate is operatorOnly's (server.go): session -> IsOperator -> bare
6// http.NotFound on EVERY failure including the store error. A styled refusal
7// would confirm the route exists, and this page names hostnames, instance
8// ids and the shape of the estate.
9//
10// Every read soft-fails on its own. The inventory, the fleet aggregate, the
11// per-box snapshots, the registry and the rate card are five independent
12// reads; any one of them having a bad moment costs its own section and a
13// line in the footer, never the page.
14package console
15
16import (
17 "crypto/subtle"
18 "log"
19 "net/http"
20 "time"
21)
22
23func (s *Server) handleArchitecture(w http.ResponseWriter, r *http.Request) {
24 // Method-checked here rather than by a "GET /path" mux pattern: that
25 // pattern answers a POST with 405 + Allow, which tells an unauthenticated
26 // prober this route exists — the one thing operatorOnly's bare 404 is
27 // there to withhold. HEAD is GET without a body and is served as such.
28 if r.Method != http.MethodGet && r.Method != http.MethodHead {
29 http.NotFound(w, r)
30 return
31 }
32 addr, raw, ok := s.operatorOnly(w, r)
33 if !ok {
34 return
35 }
36 in := ArchInput{Bucket: s.cfg.Bucket}
37 inv, missing := s.loadInventory(r.Context())
38 in.Inv = inv
39
40 // A read that failed is SAID, not only logged. An empty Boxes section
41 // with nothing beside it reads as "this deployment has no boxes", which
42 // is the one claim a page whose bucket read just failed has no business
43 // making. failed names them at the foot; boxesUnread is the same fact
44 // carried to the place the false claim would otherwise be printed.
45 var failed []string
46 var boxesUnread bool
47
48 // Reads two, three and five all go through the deployment bucket; a
49 // console configured without one (CARLOS_DEPLOYMENT_DIR-less, legal per
50 // Config.Releases) has no fleet, no host table and no rate card to read,
51 // and asking anyway would panic rather than degrade.
52 if s.cfg.Releases != nil {
53 if f, err := FleetView(r.Context(), s.cfg.Releases); err != nil {
54 log.Printf("console: architecture: fleet view: %v", err)
55 failed = append(failed, "the published fleet status could not be read")
56 boxesUnread = true
57 } else {
58 in.Fleet = f
59 }
60 if hosts, err := HostRows(r.Context(), s.cfg.Releases, time.Now()); err != nil {
61 log.Printf("console: architecture: hosts: %v", err)
62 failed = append(failed, "the per-box snapshots could not be read")
63 boxesUnread = true
64 } else {
65 in.Hosts = hosts
66 }
67 // The rate card comes from the SAME loader /fleet/economics uses
68 // (economicsview.go), so the two pages cannot disagree about what a
69 // box costs. Its warning is already surfaced on that page; here a bad
70 // scenario simply means the zero rate card, and every box then reports
71 // drift — which is the honest answer, not a silent $0.
72 sc, _ := s.loadActiveScenario(r.Context())
73 in.Rates = sc.RateCard
74 }
75 if reg, err := openRegistry(s.cfg.Registry); err != nil {
76 log.Printf("console: architecture: registry: %v", err)
77 failed = append(failed, "the route registry could not be opened — apps are not named on the boxes below")
78 } else if routes, err := reg.List(); err != nil {
79 log.Printf("console: architecture: routes: %v", err)
80 failed = append(failed, "the routes could not be listed — apps are not named on the boxes below")
81 } else {
82 in.Routes = routes
83 }
84
85 v := BuildArch(in)
86 // BuildArch defaults a nil inventory to "never"; loadInventory knows
87 // WHICH of the three states it actually is, and that is the one the page
88 // must name.
89 if missing != "" {
90 v.Missing = missing
91 }
92 // Appended to a COPY: v.Partial aliases the inventory's own slice, and
93 // this page's read failures are not the inventory's.
94 if len(failed) > 0 {
95 v.Partial = append(append([]string{}, v.Partial...), failed...)
96 }
97 // The pictures are NOT rendered here any more: an estate has one band
98 // per region and the count is not known until it is built, so the
99 // template ranges over the bands and each draws itself (ArchBand.SVG).
100 // The SVG is still built in Go, which is what keeps it deterministic
101 // and testable as a whole.
102 p := page{Title: "Architecture", Address: addr, CSRF: raw,
103 Arch: &v, ArchBoxesUnread: boxesUnread}
104 p.Flash, p.FlashErr = s.takeFlash(w, r)
105 s.render(w, http.StatusOK, "architecture", p)
106}
107
108// handleArchitectureRefresh runs one inventory now. Single-flighted
109// (infraRefreshing): a leaned-on button must not stack passes against a
110// rate-limited API. It deliberately does NOT clear the auth latch — a
111// deployment without the IAM grant gets the same honest answer however many
112// times the button is pressed.
113//
114// Method-checked here rather than by a "POST /path" mux pattern, for the
115// reason server.go's economics doors give: a 405 + Allow answer to a GET
116// would leak that this route exists to someone who merely guessed the URL.
117func (s *Server) handleArchitectureRefresh(w http.ResponseWriter, r *http.Request) {
118 if r.Method != http.MethodPost {
119 http.NotFound(w, r)
120 return
121 }
122 _, raw, ok := s.operatorOnly(w, r)
123 if !ok {
124 return
125 }
126 if subtle.ConstantTimeCompare([]byte(r.FormValue("csrf")), []byte(raw)) != 1 {
127 s.renderMessage(w, r, http.StatusForbidden, "Refused", "Cross-site request blocked.")
128 return
129 }
130 if !s.infraRefreshing.CompareAndSwap(false, true) {
131 s.setFlash(w, "err", "An inventory is already running.")
132 http.Redirect(w, r, "/fleet/architecture", http.StatusSeeOther)
133 return
134 }
135 defer s.infraRefreshing.Store(false)
136
137 if err := s.infraPass(r.Context()); err != nil {
138 log.Printf("console: architecture refresh: %v", err)
139 s.setFlash(w, "err", "The inventory could not be refreshed.")
140 } else {
141 s.setFlash(w, "ok", "Inventory refreshed.")
142 }
143 http.Redirect(w, r, "/fleet/architecture", http.StatusSeeOther)
144}
145