package web import ( "encoding/json" "fmt" "net/http" "net/url" "strconv" "strings" ) // handleOSAdmin serves the operator's OS-update controls (behind the operator login, like every route here): // // POST /os/ring/ ring=0|1 // POST /os/enabled/ on=1|0 // POST /os/approve-now approve the current ring-0 set at once (an operator event) // POST /os/approve-docker approve the Docker engine set ring 0 ran 2 healthy nights (`11` ยง5.8) // (a form field return=/system makes any POST answer with a redirect to the System page) // GET /os/fleet one line per box (JSON) func (s *Server) handleOSAdmin(w http.ResponseWriter, r *http.Request, path string) { if s.osUpdates == nil { http.Error(w, "os updates not configured", http.StatusServiceUnavailable) return } // A button on the System page posts return=/system: answer with a redirect and a flash, never JSON. fromPage := r.Method == http.MethodPost && r.FormValue("return") == "/system" reply := func(v any, err error) { if fromPage { q := "flash=done" if err != nil { q = "err=" + url.QueryEscape(err.Error()) } else if m, ok := v.(map[string]string); ok && m["release_id"] != "" { q = "flash=" + url.QueryEscape("approved "+m["release_id"]) } http.Redirect(w, r, "/system?"+q, http.StatusSeeOther) return } if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(v) } switch { case r.Method == http.MethodGet && path == "/os/fleet": reply(s.osUpdates.FleetJSON()) case r.Method == http.MethodPost && strings.HasPrefix(path, "/os/ring/"): n, err := strconv.Atoi(r.FormValue("ring")) if err != nil { http.Error(w, "ring must be 0 or 1", http.StatusBadRequest) return } reply(map[string]bool{"ok": true}, s.osUpdates.SetRing(strings.TrimPrefix(path, "/os/ring/"), n)) case r.Method == http.MethodPost && strings.HasPrefix(path, "/os/enabled/"): on := r.FormValue("on") if on != "0" && on != "1" { http.Error(w, "on must be 0 or 1", http.StatusBadRequest) return } reply(map[string]bool{"ok": true}, s.osUpdates.SetEnabled(strings.TrimPrefix(path, "/os/enabled/"), on == "1")) case r.Method == http.MethodPost && path == "/os/approve-now": id, err := s.osUpdates.ApproveNow() reply(map[string]string{"release_id": id}, err) case r.Method == http.MethodPost && path == "/os/approve-docker": view, ok := s.osUpdates.(OSSystemView) if !ok { reply(nil, fmt.Errorf("docker approval not available")) return } id, err := view.ApproveDocker() reply(map[string]string{"release_id": id}, err) default: http.Error(w, "not found", http.StatusNotFound) } }