feat(master): check the registry now from the service page (#35) (#36)

This commit was merged in pull request #36.
This commit is contained in:
2026-10-14 12:24:59 +00:00
parent 2989fa224d
commit 7671221343
11 changed files with 367 additions and 18 deletions
+42 -2
View File
@@ -18,6 +18,7 @@ import (
"github.com/shcizo/package-updater/internal/agenttoken"
"github.com/shcizo/package-updater/internal/master/events"
"github.com/shcizo/package-updater/internal/master/model"
"github.com/shcizo/package-updater/internal/master/poller"
"github.com/shcizo/package-updater/internal/master/web/icons"
"github.com/shcizo/package-updater/internal/master/webauth"
"github.com/shcizo/package-updater/internal/protocol"
@@ -85,6 +86,13 @@ type Jobs interface {
RestoreDesired(ctx context.Context, serviceID int64, requestedBy string) (model.Job, error)
}
// Poller checks the registry for one service. *poller.Poller satisfies it.
type Poller interface {
// PollService returns *poller.NotPolledError when the service is not
// polled and model.ErrNotFound for an unknown or removed one.
PollService(ctx context.Context, id int64) error
}
// Broker delivers change signals to open streams and accepts signals from
// UI actions. *events.Broker satisfies it.
type Broker interface {
@@ -108,6 +116,7 @@ type Server struct {
hub Hub
jobs Jobs
broker Broker
poller Poller
cfg Config
log *slog.Logger
now func() time.Time
@@ -116,7 +125,7 @@ type Server struct {
// New parses the templates. Each page is its own template set so every page
// can define its own "content" block.
func New(store Store, hub Hub, jobs Jobs, broker Broker, cfg Config, log *slog.Logger) (*Server, error) {
func New(store Store, hub Hub, jobs Jobs, broker Broker, poller Poller, cfg Config, log *slog.Logger) (*Server, error) {
funcs := template.FuncMap{"shortRef": shortRef, "duration": jobDuration, "followsNoTag": followsNoTag, "middle": middle}
pages := map[string]*template.Template{}
for _, page := range []string{"overview.html", "agents.html", "service.html", "jobs.html", "job.html"} {
@@ -126,7 +135,7 @@ func New(store Store, hub Hub, jobs Jobs, broker Broker, cfg Config, log *slog.L
}
pages[page] = t
}
return &Server{store: store, hub: hub, jobs: jobs, broker: broker, cfg: cfg, log: log, now: time.Now, pages: pages}, nil
return &Server{store: store, hub: hub, jobs: jobs, broker: broker, poller: poller, cfg: cfg, log: log, now: time.Now, pages: pages}, nil
}
// Register adds the UI routes to mux. protect wraps every route except
@@ -140,6 +149,7 @@ func (s *Server) Register(mux *http.ServeMux, protect func(http.Handler) http.Ha
route("GET /fragments/services/{id}/jobs", s.serviceJobsFragment)
route("POST /services/{id}/update", s.updateService)
route("POST /services/{id}/mode", s.setMode)
route("POST /services/{id}/poll", s.pollService)
route("POST /services/{id}/restore", s.restoreDesired)
route("POST /services/{id}/accept", s.acceptRunning)
route("POST /services/{id}/merge", s.mergeService)
@@ -204,6 +214,7 @@ type pageData struct {
ConfigFiles []string
Reason string // why Service cannot be updated; "" when it can
AutoBlocked string // why Service cannot be set to auto; "" when it can
PollBlocked string // why Service cannot be checked against the registry; "" when it can
Versions []versionRow
NewerTag string // Service's newer tag while it still holds; "" otherwise
Polling string // what the service page says about registry polling
@@ -380,6 +391,7 @@ func (s *Server) serviceDetail(r *http.Request, id int64) (pageData, error) {
return data, err
}
data.AutoBlocked = s.autoBlocked(svc)
data.PollBlocked = pollBlocked(svc)
if err := s.jobs.Restorable(svc); errors.As(err, &nu) {
data.RestoreBlocked = nu.Reason
} else if err != nil {
@@ -605,6 +617,34 @@ func (s *Server) renderServiceError(w http.ResponseWriter, r *http.Request, id i
s.page(w, r, status, "service.html", data)
}
// pollService checks the registry for one service now and waits for the
// answer. A registry error is not a failure of the request: it shows on the
// service page as the last check failing. The poller publishes the change
// itself when it writes.
func (s *Server) pollService(w http.ResponseWriter, r *http.Request) {
id, ok := pathID(w, r)
if !ok {
return
}
err := s.poller.PollService(r.Context(), id)
var np *poller.NotPolledError
switch {
case errors.As(err, &np):
s.renderServiceError(w, r, id, http.StatusConflict, "Cannot check: "+np.Reason+".")
return
case r.Context().Err() != nil:
// The client is gone or the master is shutting down. Redirect anyway:
// a browser still waiting gets the page, not an empty 200.
case err != nil:
s.failLookup(w, err)
return
default:
sess, _ := webauth.SessionFrom(r.Context())
s.log.Info("poll_requested", "service_id", id, "subject", sess.Subject)
}
http.Redirect(w, r, "/services/"+strconv.FormatInt(id, 10), http.StatusSeeOther)
}
// setMode switches a service between manual and auto. Manual is always
// allowed; auto only when jobs.AutoAllowed says so.
func (s *Server) setMode(w http.ResponseWriter, r *http.Request) {