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:
@@ -78,7 +78,7 @@ func (h *Hub) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
c.SetReadLimit(readLimit)
|
||||
h.register(agent.ID, c)
|
||||
h.register(agent, r.RemoteAddr, c)
|
||||
defer h.unregister(agent.ID, c)
|
||||
|
||||
// Revoking commits revoked_at before calling Disconnect. Re-checking after
|
||||
@@ -114,12 +114,16 @@ func (h *Hub) Disconnect(agentID int64) {
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Hub) register(id int64, c *websocket.Conn) {
|
||||
// register makes c the agent's live connection, closing any older one. A
|
||||
// replacement is logged: it is normal after a network blip, but repeated
|
||||
// replacements mean two agents share a token.
|
||||
func (h *Hub) register(agent model.Agent, remoteAddr string, c *websocket.Conn) {
|
||||
h.mu.Lock()
|
||||
old := h.conns[id]
|
||||
h.conns[id] = c
|
||||
old := h.conns[agent.ID]
|
||||
h.conns[agent.ID] = c
|
||||
h.mu.Unlock()
|
||||
if old != nil {
|
||||
h.log.Warn("agent_replaced", "agent_id", agent.ID, "agent", agent.Name, "remote_addr", remoteAddr)
|
||||
old.CloseNow()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package hub
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"log/slog"
|
||||
@@ -320,9 +321,31 @@ func TestHub_ClosesConnectionThatSkipsHello(t *testing.T) {
|
||||
assert.Equal(t, websocket.StatusPolicyViolation, websocket.CloseStatus(err))
|
||||
}
|
||||
|
||||
// lockedBuffer is a bytes.Buffer safe for the hub's handler goroutines to
|
||||
// write while the test reads it.
|
||||
type lockedBuffer struct {
|
||||
mu sync.Mutex
|
||||
buf bytes.Buffer
|
||||
}
|
||||
|
||||
func (b *lockedBuffer) Write(p []byte) (int, error) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
return b.buf.Write(p)
|
||||
}
|
||||
|
||||
func (b *lockedBuffer) String() string {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
return b.buf.String()
|
||||
}
|
||||
|
||||
func TestHub_NewConnectionReplacesOld(t *testing.T) {
|
||||
store := newFakeStore(node2)
|
||||
h, _, srv := newHub(t, store)
|
||||
logs := &lockedBuffer{}
|
||||
h := New(store, &fakePub{}, slog.New(slog.NewTextHandler(logs, nil)))
|
||||
srv := httptest.NewServer(h)
|
||||
t.Cleanup(srv.Close)
|
||||
first, _, err := dial(t, srv, "pua_node-2")
|
||||
require.NoError(t, err)
|
||||
defer first.CloseNow()
|
||||
@@ -336,6 +359,9 @@ func TestHub_NewConnectionReplacesOld(t *testing.T) {
|
||||
_, _, err = first.Read(context.Background())
|
||||
assert.Error(t, err, "the replaced connection must be closed")
|
||||
assert.True(t, h.Online(node2.ID), "the agent stays online on its new connection")
|
||||
assert.Contains(t, logs.String(), "msg=agent_replaced")
|
||||
assert.Contains(t, logs.String(), "agent=node-2")
|
||||
assert.NotContains(t, logs.String(), "pua_", "tokens are never logged")
|
||||
}
|
||||
|
||||
func TestHub_DisconnectCutsLiveConnection(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user