Add fleet groups, agent screenshots, deploy guards, and Crucible polish.
This commit is contained in:
@@ -310,8 +310,22 @@ func (f *FleetHandler) PostAgentCommand(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
f.ws.BroadcastAgentCommand(req.Action, args)
|
||||
} else {
|
||||
if !f.ws.isAgentConnected(id) {
|
||||
writeJSON(w, map[string]interface{}{
|
||||
"success": false,
|
||||
"error": "agent not connected",
|
||||
"agent_id": id,
|
||||
"action": req.Action,
|
||||
})
|
||||
return
|
||||
}
|
||||
if err := f.ws.SendAgentCommand(id, req.Action, args); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
writeJSON(w, map[string]interface{}{
|
||||
"success": false,
|
||||
"error": err.Error(),
|
||||
"agent_id": id,
|
||||
"action": req.Action,
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -588,8 +588,15 @@ func TestFleetPostAgentCommandErrors(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodPost, "/agents/offline-agent/command",
|
||||
strings.NewReader(`{"action":"pause"}`))
|
||||
fleetChiRoute(http.MethodPost, "/agents/{id}/command", fh.PostAgentCommand).ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusBadRequest {
|
||||
t.Fatalf("expected 400, got %d body %s", rec.Code, rec.Body.String())
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d body %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
var body map[string]interface{}
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if body["success"] != false || body["error"] != "agent not connected" {
|
||||
t.Fatalf("unexpected body: %v", body)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -337,8 +337,17 @@ func TestIntegrationAgentCommandOffline(t *testing.T) {
|
||||
router, _, _, _ := newTestRouter(t)
|
||||
rec := serveAuthed(t, router, http.MethodPost, "/api/v1/agents/offline-agent/command",
|
||||
[]byte(`{"action":"pause"}`))
|
||||
if rec.Code != http.StatusBadRequest {
|
||||
t.Fatalf("expected 400 for offline agent, got %d body=%s", rec.Code, rec.Body.String())
|
||||
// Command for a non-connected agent returns 200 with success:false (not a 4xx),
|
||||
// so the caller can inspect the error without tripping HTTP error handling.
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200 for offline agent, got %d body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
var body map[string]interface{}
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if body["success"] != false {
|
||||
t.Fatalf("expected success=false, got %v", body)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -47,8 +47,8 @@ func checkDashboardWSToken(r *http.Request) bool {
|
||||
}
|
||||
|
||||
var upgrader = websocket.Upgrader{
|
||||
ReadBufferSize: 4096,
|
||||
WriteBufferSize: 4096,
|
||||
ReadBufferSize: 512 * 1024,
|
||||
WriteBufferSize: 512 * 1024,
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
return true // Allow all origins for local use
|
||||
},
|
||||
@@ -246,6 +246,17 @@ func (h *WSHub) isAgentConnected(agentID string) bool {
|
||||
return ok
|
||||
}
|
||||
|
||||
// writeAgentJSON sends a message to a connected agent using the per-connection
|
||||
// write mutex. All post-auth outbound JSON must use this — never conn.WriteJSON
|
||||
// from the read loop, or commands and new_job messages can corrupt each other.
|
||||
func (h *WSHub) writeAgentJSON(agentID string, msg Message) error {
|
||||
ac := h.getAgentConn(agentID)
|
||||
if ac == nil {
|
||||
return fmt.Errorf("agent %s not connected", agentID)
|
||||
}
|
||||
return ac.SendJSON(msg)
|
||||
}
|
||||
|
||||
func (h *WSHub) SetPoolManager(manager *pool.Manager, defaultCfg pool.Config) {
|
||||
h.mu.Lock()
|
||||
h.poolManager = manager
|
||||
@@ -826,12 +837,12 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) {
|
||||
if proxy != nil {
|
||||
job := proxy.GetCurrentJob()
|
||||
if job != nil {
|
||||
conn.WriteJSON(Message{Type: "new_job", Payload: mustMarshal(job)})
|
||||
_ = h.writeAgentJSON(agentID, Message{Type: "new_job", Payload: mustMarshal(job)})
|
||||
} else {
|
||||
conn.WriteJSON(Message{Type: "new_job", Payload: mustMarshal(map[string]string{"error": "no job available — pool connecting"})})
|
||||
_ = h.writeAgentJSON(agentID, Message{Type: "new_job", Payload: mustMarshal(map[string]string{"error": "no job available — pool connecting"})})
|
||||
}
|
||||
} else {
|
||||
conn.WriteJSON(Message{Type: "new_job", Payload: mustMarshal(map[string]string{"error": "pool connecting — retry shortly"})})
|
||||
_ = h.writeAgentJSON(agentID, Message{Type: "new_job", Payload: mustMarshal(map[string]string{"error": "pool connecting — retry shortly"})})
|
||||
}
|
||||
|
||||
case "log_tail":
|
||||
@@ -899,6 +910,11 @@ func (h *WSHub) HandleDashboardWS(w http.ResponseWriter, r *http.Request) {
|
||||
// Send initial data
|
||||
agents, _ := h.db.ListAgents()
|
||||
h.enrichAgentsCapabilities(agents)
|
||||
for _, a := range agents {
|
||||
if a != nil && h.isAgentConnected(a.ID) {
|
||||
a.Status = "online"
|
||||
}
|
||||
}
|
||||
stats, _ := h.db.GetFleetStats()
|
||||
|
||||
_ = dc.WriteJSON(Message{Type: "init", Payload: mustMarshal(map[string]interface{}{
|
||||
|
||||
@@ -46,8 +46,9 @@ func TestShortAgentID(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAgentDisplayNameFallback(t *testing.T) {
|
||||
// No hostname, no worker name — falls back to "agent-<shortID>"
|
||||
name := agentDisplayName("", "", "", "12345678-abcd")
|
||||
if name != "12345678" {
|
||||
t.Fatalf("expected id prefix, got %q", name)
|
||||
if name != "agent-12345678" {
|
||||
t.Fatalf("expected agent-12345678, got %q", name)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user