Show edge and site versions with Update buttons; Update now for Hammerhead itself
Edges and sites report their version and whether auto-update is on. Hammerhead can ask a machine that has auto-update on to install the newest release, and shows the outcome. Requests carry an ID so a stale result can't answer a new one. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
abf352e6c9
commit
579ad1521a
11 files changed
+326
-23
No files matched your search
@@ -2,30 +2,160 @@ package hammerhead
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.veillax.com/Veillax/reef/selfupdate"
|
||||
)
|
||||
|
||||
// checkUpdate asks the release server whether a newer Hammerhead exists. It only looks: updating is
|
||||
// left to auto-update or to the person running the server.
|
||||
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
|
||||
cfg := selfupdate.Config{Server: o.UpdateServer, Repo: o.UpdateRepo, App: "hammerhead", Current: o.Version}
|
||||
if cfg.Server == "" {
|
||||
cfg.Server = "https://git.veillax.com"
|
||||
}
|
||||
if cfg.Repo == "" {
|
||||
cfg.Repo = "Veillax/hammerhead"
|
||||
}
|
||||
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, cfg)
|
||||
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
|
||||
}
|
||||
Reference in new issue
Block a user