package hammerhead import ( "context" "errors" "net/http" "strings" "sync" "time" "git.veillax.com/Veillax/reef/selfupdate" ) func (s *Server) updateConfig(app, current string) selfupdate.Config { o := s.opts server := o.UpdateServer if server == "" { server = "https://git.veillax.com" } owner := "Veillax" if o.UpdateRepo != "" { owner = strings.SplitN(o.UpdateRepo, "/", 2)[0] } return selfupdate.Config{Server: server, Repo: owner + "/" + app, App: app, Current: current} } // checkUpdate asks the release server whether a newer Hammerhead exists. It only looks. func (s *Server) checkUpdate(w http.ResponseWriter, r *http.Request, p *principal) { o := s.opts ctx, cancel := context.WithTimeout(r.Context(), 20*time.Second) defer cancel() out := map[string]any{"current": o.Version, "autoUpdate": o.AutoUpdate, "checked": time.Now()} latest, newer, err := selfupdate.Check(ctx, s.updateConfig("hammerhead", o.Version)) out["latest"], out["newer"] = latest, newer if err != nil { out["error"] = err.Error() } writeJSON(w, 200, out) } // applyUpdate installs the newest release of Hammerhead itself and restarts into it. Like the // scheduled check it needs auto-update to be on, which is also what makes the binary replaceable. func (s *Server) applyUpdate(w http.ResponseWriter, r *http.Request, p *principal) { if !s.opts.AutoUpdate { fail(w, 400, errors.New("auto-update is off: set HAMMERHEAD_AUTO_UPDATE=1 and restart Hammerhead, or update it by hand")) return } cfg := s.updateConfig("hammerhead", s.opts.Version) cfg.Restart = func(exe string) error { s.Close(); return selfupdate.Restart(exe) } s.audit("", p.User.Email, "server.update", "") go func() { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) defer cancel() selfupdate.CheckOnce(ctx, cfg) // on success the process is replaced and this never returns }() writeJSON(w, 200, map[string]any{"started": true}) } // latestVersions returns the newest release of each program, for "update available" badges. Results // are cached for ten minutes so a busy dashboard doesn't hammer the release server. func (s *Server) latestVersions(w http.ResponseWriter, r *http.Request, p *principal) { s.verMu.Lock() if time.Since(s.verAt) > 10*time.Minute || s.verCache == nil { ctx, cancel := context.WithTimeout(r.Context(), 15*time.Second) out := map[string]string{} var mu sync.Mutex var wg sync.WaitGroup for _, app := range []string{"hammerhead", "blacktip", "bamboo"} { wg.Add(1) go func(app string) { defer wg.Done() if tag, _, err := selfupdate.Check(ctx, s.updateConfig(app, "")); err == nil && tag != "" { mu.Lock() out[app] = tag mu.Unlock() } }(app) } wg.Wait() cancel() s.verCache, s.verAt = out, time.Now() } out := s.verCache s.verMu.Unlock() writeJSON(w, 200, out) } // noteUpdateReport records what an edge or site says about its version and any update that was // requested of it. It reports whether a pending request was answered, which means the config the // agent polls has changed (it no longer carries the request). func noteUpdateReport(version *string, auto *bool, updateID, updateMsg *string, inVersion string, inAuto bool, inID, inMsg string) bool { upgraded := *version != "" && inVersion != "" && inVersion != *version if inVersion != "" { *version = inVersion } *auto = inAuto if *updateID == "" { return false } switch { case inID == *updateID && inMsg != "": *updateMsg = inMsg case upgraded: *updateMsg = "updated to " + inVersion default: return false } *updateID = "" return true } func (s *Server) updateEdge(w http.ResponseWriter, r *http.Request, p *principal) { s.mu.Lock() defer s.mu.Unlock() e := s.findEdge(r.PathValue("id")) if e == nil { fail(w, 404, errors.New("no such edge")) return } if err := requestUpdate(&e.AutoUpdate, &e.UpdateID, &e.UpdateMsg, s.edgeOnline(e), "BLACKTIP"); err != nil { fail(w, 400, err) return } s.bump() s.audit("", p.User.Email, "edge.update", e.Name) ok(w) } func (s *Server) updateSite(w http.ResponseWriter, r *http.Request, c *orgCtx) { s.mu.Lock() defer s.mu.Unlock() st := s.findSite(r.PathValue("id")) if st == nil || !s.siteVisible(c, st) { fail(w, 404, errors.New("no such site")) return } if st.Local { fail(w, 400, errors.New("this site is the edge's own machine; update the edge instead")) return } if err := requestUpdate(&st.AutoUpdate, &st.UpdateID, &st.UpdateMsg, s.siteOnline(st), "BAMBOO"); err != nil { fail(w, 400, err) return } s.bump() s.audit(c.Org.ID, c.Actor, "site.update", st.Name) ok(w) } // requestUpdate marks an update as wanted. The agent installs it only if it has auto-update on, and // the request stays until the agent answers (or reports a new version). Asking again replaces it. func requestUpdate(auto *bool, updateID, updateMsg *string, online bool, prefix string) error { if !online { return errors.New("it is offline right now") } if !*auto { return errors.New("it isn't running with auto-update on, so it won't install updates from here (set " + prefix + "_AUTO_UPDATE=1 and restart it)") } *updateID, *updateMsg = randHex(8), "" return nil }