fix: address final review findings for master/agent phase 1 (#6)

- inventory: resolve the image reference via ContainerInspect when a moved
  tag makes ContainerList report the image ID
- store: allow reusing a revoked agent's name (partial unique index) and
  mark the revoked agent's compose/container services removed
- web: publish a services event on revoke; no-store on the one-time token page
- webauth: reject empty OIDC state; accept the CSRF form token from the body only
- agent conn: bound the WebSocket handshake to 30s
- hub: log agent_replaced when a connection replaces an older one
This commit is contained in:
2026-10-12 15:16:29 +02:00
parent 4f092eaf79
commit 0e9e34db06
12 changed files with 211 additions and 35 deletions
+16 -12
View File
@@ -39,9 +39,11 @@ type Hub interface {
Disconnect(agentID int64)
}
// Subscriber delivers change signals. *events.Broker satisfies it.
type Subscriber interface {
// Broker delivers change signals to open streams and accepts signals from
// UI actions. *events.Broker satisfies it.
type Broker interface {
Subscribe() (<-chan events.Topic, func())
Publish(t events.Topic)
}
// Config holds values shown in the "add agent" snippet.
@@ -53,18 +55,18 @@ type Config struct {
// Server renders the UI.
type Server struct {
store Store
hub Hub
sub Subscriber
cfg Config
log *slog.Logger
now func() time.Time
pages map[string]*template.Template
store Store
hub Hub
broker Broker
cfg Config
log *slog.Logger
now func() time.Time
pages map[string]*template.Template
}
// 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, sub Subscriber, cfg Config, log *slog.Logger) (*Server, error) {
func New(store Store, hub Hub, broker Broker, cfg Config, log *slog.Logger) (*Server, error) {
funcs := template.FuncMap{"shortRef": shortRef}
pages := map[string]*template.Template{}
for _, page := range []string{"overview.html", "agents.html", "agent_created.html"} {
@@ -74,7 +76,7 @@ func New(store Store, hub Hub, sub Subscriber, cfg Config, log *slog.Logger) (*S
}
pages[page] = t
}
return &Server{store: store, hub: hub, sub: sub, cfg: cfg, log: log, now: time.Now, pages: pages}, nil
return &Server{store: store, hub: hub, broker: broker, cfg: cfg, log: log, now: time.Now, pages: pages}, nil
}
// Register adds the UI routes to mux. protect wraps every route except
@@ -195,6 +197,7 @@ func (s *Server) createAgent(w http.ResponseWriter, r *http.Request) {
data := s.base(r, "Agent created")
data.Created = agent
data.Snippet = agentSnippet(s.cfg, token)
w.Header().Set("Cache-Control", "no-store") // the page shows the one-time token
s.render(w, http.StatusOK, "agent_created.html", "layout", data)
}
@@ -219,6 +222,7 @@ func (s *Server) revokeAgent(w http.ResponseWriter, r *http.Request) {
return
}
s.hub.Disconnect(id)
s.broker.Publish(events.Services) // the agent's services were marked removed
http.Redirect(w, r, "/agents", http.StatusSeeOther)
}
@@ -231,7 +235,7 @@ func (s *Server) events(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("X-Accel-Buffering", "no") // stop nginx from buffering the stream
ch, cancel := s.sub.Subscribe()
ch, cancel := s.broker.Subscribe()
defer cancel()
if _, err := fmt.Fprint(w, ": connected\n\n"); err != nil {