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>
162 lines
5.1 KiB
Go
162 lines
5.1 KiB
Go
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
|
|
}
|