// GET /fleet/architecture — the operator's map of the deployment: how a // request reaches a process, and everything the account is carrying that // makes that possible. // // The gate is operatorOnly's (server.go): session -> IsOperator -> bare // http.NotFound on EVERY failure including the store error. A styled refusal // would confirm the route exists, and this page names hostnames, instance // ids and the shape of the estate. // // Every read soft-fails on its own. The inventory, the fleet aggregate, the // per-box snapshots, the registry and the rate card are five independent // reads; any one of them having a bad moment costs its own section and a // line in the footer, never the page. package console import ( "crypto/subtle" "log" "net/http" "time" ) func (s *Server) handleArchitecture(w http.ResponseWriter, r *http.Request) { // Method-checked here rather than by a "GET /path" mux pattern: that // pattern answers a POST with 405 + Allow, which tells an unauthenticated // prober this route exists — the one thing operatorOnly's bare 404 is // there to withhold. HEAD is GET without a body and is served as such. if r.Method != http.MethodGet && r.Method != http.MethodHead { http.NotFound(w, r) return } addr, raw, ok := s.operatorOnly(w, r) if !ok { return } in := ArchInput{Bucket: s.cfg.Bucket} inv, missing := s.loadInventory(r.Context()) in.Inv = inv // A read that failed is SAID, not only logged. An empty Boxes section // with nothing beside it reads as "this deployment has no boxes", which // is the one claim a page whose bucket read just failed has no business // making. failed names them at the foot; boxesUnread is the same fact // carried to the place the false claim would otherwise be printed. var failed []string var boxesUnread bool // Reads two, three and five all go through the deployment bucket; a // console configured without one (CARLOS_DEPLOYMENT_DIR-less, legal per // Config.Releases) has no fleet, no host table and no rate card to read, // and asking anyway would panic rather than degrade. if s.cfg.Releases != nil { if f, err := FleetView(r.Context(), s.cfg.Releases); err != nil { log.Printf("console: architecture: fleet view: %v", err) failed = append(failed, "the published fleet status could not be read") boxesUnread = true } else { in.Fleet = f } if hosts, err := HostRows(r.Context(), s.cfg.Releases, time.Now()); err != nil { log.Printf("console: architecture: hosts: %v", err) failed = append(failed, "the per-box snapshots could not be read") boxesUnread = true } else { in.Hosts = hosts } // The rate card comes from the SAME loader /fleet/economics uses // (economicsview.go), so the two pages cannot disagree about what a // box costs. Its warning is already surfaced on that page; here a bad // scenario simply means the zero rate card, and every box then reports // drift — which is the honest answer, not a silent $0. sc, _ := s.loadActiveScenario(r.Context()) in.Rates = sc.RateCard } if reg, err := openRegistry(s.cfg.Registry); err != nil { log.Printf("console: architecture: registry: %v", err) failed = append(failed, "the route registry could not be opened — apps are not named on the boxes below") } else if routes, err := reg.List(); err != nil { log.Printf("console: architecture: routes: %v", err) failed = append(failed, "the routes could not be listed — apps are not named on the boxes below") } else { in.Routes = routes } v := BuildArch(in) // BuildArch defaults a nil inventory to "never"; loadInventory knows // WHICH of the three states it actually is, and that is the one the page // must name. if missing != "" { v.Missing = missing } // Appended to a COPY: v.Partial aliases the inventory's own slice, and // this page's read failures are not the inventory's. if len(failed) > 0 { v.Partial = append(append([]string{}, v.Partial...), failed...) } // The pictures are NOT rendered here any more: an estate has one band // per region and the count is not known until it is built, so the // template ranges over the bands and each draws itself (ArchBand.SVG). // The SVG is still built in Go, which is what keeps it deterministic // and testable as a whole. p := page{Title: "Architecture", Address: addr, CSRF: raw, Arch: &v, ArchBoxesUnread: boxesUnread} p.Flash, p.FlashErr = s.takeFlash(w, r) s.render(w, http.StatusOK, "architecture", p) } // handleArchitectureRefresh runs one inventory now. Single-flighted // (infraRefreshing): a leaned-on button must not stack passes against a // rate-limited API. It deliberately does NOT clear the auth latch — a // deployment without the IAM grant gets the same honest answer however many // times the button is pressed. // // Method-checked here rather than by a "POST /path" mux pattern, for the // reason server.go's economics doors give: a 405 + Allow answer to a GET // would leak that this route exists to someone who merely guessed the URL. func (s *Server) handleArchitectureRefresh(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.NotFound(w, r) return } _, raw, ok := s.operatorOnly(w, r) if !ok { return } if subtle.ConstantTimeCompare([]byte(r.FormValue("csrf")), []byte(raw)) != 1 { s.renderMessage(w, r, http.StatusForbidden, "Refused", "Cross-site request blocked.") return } if !s.infraRefreshing.CompareAndSwap(false, true) { s.setFlash(w, "err", "An inventory is already running.") http.Redirect(w, r, "/fleet/architecture", http.StatusSeeOther) return } defer s.infraRefreshing.Store(false) if err := s.infraPass(r.Context()); err != nil { log.Printf("console: architecture refresh: %v", err) s.setFlash(w, "err", "The inventory could not be refreshed.") } else { s.setFlash(w, "ok", "Inventory refreshed.") } http.Redirect(w, r, "/fleet/architecture", http.StatusSeeOther) }