feat: add one pull plug and play feature

This commit is contained in:
2026-05-18 11:28:38 +02:00
parent ad64766cd6
commit fc498379f6
11 changed files with 290 additions and 42 deletions

View File

@ -488,6 +488,77 @@ func TestListContainers_WithData(t *testing.T) {
}
}
// ── DeleteAgent ───────────────────────────────────────────────────────────────
func TestDeleteAgent_Success(t *testing.T) {
h, s, reg, _ := newTestHandler(t)
_ = s.CreateAgentToken("a1", "t1", "host1")
reg.Register("a1", "host1", "", "ip", "arch", "os")
router := chi.NewRouter()
router.Delete("/api/v1/agents/{agentID}", h.DeleteAgent)
req, _ := http.NewRequest(http.MethodDelete, "/api/v1/agents/a1", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != http.StatusNoContent {
t.Fatalf("expected 204, got %d — body: %s", w.Code, w.Body.String())
}
// Agent must be gone from store
_, err := s.GetAgent("a1")
if err == nil {
t.Error("expected store error after deletion, got nil")
}
// Agent must be gone from registry
_, ok := reg.Get("a1")
if ok {
t.Error("expected agent to be deregistered from registry")
}
}
func TestDeleteAgent_NotInRegistry(t *testing.T) {
h, s, _, _ := newTestHandler(t)
_ = s.CreateAgentToken("a1", "t1", "host1")
// Agent not registered in registry (offline agent)
router := chi.NewRouter()
router.Delete("/api/v1/agents/{agentID}", h.DeleteAgent)
req, _ := http.NewRequest(http.MethodDelete, "/api/v1/agents/a1", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != http.StatusNoContent {
t.Fatalf("expected 204 even when not in registry, got %d — body: %s", w.Code, w.Body.String())
}
_, err := s.GetAgent("a1")
if err == nil {
t.Error("expected store error after deletion, got nil")
}
}
func TestDeleteAgent_NonExistent(t *testing.T) {
h, _, _, _ := newTestHandler(t)
router := chi.NewRouter()
router.Delete("/api/v1/agents/{agentID}", h.DeleteAgent)
req, _ := http.NewRequest(http.MethodDelete, "/api/v1/agents/ghost", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
// DELETE on a non-existent ID is still 204 (idempotent)
if w.Code != http.StatusNoContent {
t.Fatalf("expected 204 for non-existent agent, got %d — body: %s", w.Code, w.Body.String())
}
}
// ── ContainerAction ───────────────────────────────────────────────────────────
func TestContainerAction_AgentNotConnected(t *testing.T) {

View File

@ -72,6 +72,16 @@ func (h *Handler) ListAgents(w http.ResponseWriter, r *http.Request) {
jsonOK(w, out)
}
func (h *Handler) DeleteAgent(w http.ResponseWriter, r *http.Request) {
agentID := chi.URLParam(r, "agentID")
if err := h.store.DeleteAgent(agentID); err != nil {
http.Error(w, "store error", http.StatusInternalServerError)
return
}
h.registry.Deregister(agentID)
w.WriteHeader(http.StatusNoContent)
}
func (h *Handler) UpdateAgent(w http.ResponseWriter, r *http.Request) {
agentID := chi.URLParam(r, "agentID")
var body struct {

View File

@ -44,6 +44,7 @@ func NewRouter(h *Handler) http.Handler {
r.Get("/agents", h.ListAgents)
r.Post("/agents/token", h.CreateAgentToken)
r.Patch("/agents/{agentID}", h.UpdateAgent)
r.Delete("/agents/{agentID}", h.DeleteAgent)
r.Get("/containers", h.ListContainers)
r.Post("/agents/{agentID}/containers/{containerID}/action", h.ContainerAction)
r.Get("/agents/{agentID}/containers/{containerID}/logs", h.LogsWS)

View File

@ -138,6 +138,11 @@ func (s *Store) UpdateAgentAlias(id, alias string) error {
return err
}
func (s *Store) DeleteAgent(id string) error {
_, err := s.db.Exec(`DELETE FROM agents WHERE id = ?`, id)
return err
}
// ── Users ─────────────────────────────────────────────────────────────────────
func (s *Store) GetUserHash(username string) (string, error) {

View File

@ -195,6 +195,54 @@ func TestUpdateAgentAlias(t *testing.T) {
}
}
func TestDeleteAgent(t *testing.T) {
s := newTestStore(t)
if err := s.CreateAgentToken("a1", "t1", "host1"); err != nil {
t.Fatalf("CreateAgentToken: %v", err)
}
if err := s.DeleteAgent("a1"); err != nil {
t.Fatalf("DeleteAgent: %v", err)
}
_, err := s.GetAgent("a1")
if err == nil {
t.Error("expected error after deletion, got nil")
}
}
func TestDeleteAgent_NotFound(t *testing.T) {
s := newTestStore(t)
// Deleting a non-existent agent should not error (DELETE is idempotent at SQL level)
if err := s.DeleteAgent("nonexistent"); err != nil {
t.Fatalf("DeleteAgent on missing id: %v", err)
}
}
func TestDeleteAgent_RemovesFromList(t *testing.T) {
s := newTestStore(t)
_ = s.CreateAgentToken("a1", "t1", "host1")
_ = s.CreateAgentToken("a2", "t2", "host2")
if err := s.DeleteAgent("a1"); err != nil {
t.Fatalf("DeleteAgent: %v", err)
}
agents, err := s.ListAgents()
if err != nil {
t.Fatalf("ListAgents: %v", err)
}
if len(agents) != 1 {
t.Fatalf("expected 1 agent after deletion, got %d", len(agents))
}
if agents[0].ID != "a2" {
t.Errorf("expected remaining agent to be a2, got %q", agents[0].ID)
}
}
func TestCreateAgentToken_IdempotentIgnore(t *testing.T) {
s := newTestStore(t)