feat: add volume, images and networks
This commit is contained in:
@ -559,6 +559,188 @@ func TestDeleteAgent_NonExistent(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// ── ListImages ────────────────────────────────────────────────────────────────
|
||||
|
||||
func TestListImages_Empty(t *testing.T) {
|
||||
h, _, _, _ := newTestHandler(t)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/images", nil)
|
||||
w := httptest.NewRecorder()
|
||||
h.ListImages(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", w.Code)
|
||||
}
|
||||
|
||||
var out []map[string]any
|
||||
json.NewDecoder(w.Body).Decode(&out)
|
||||
if len(out) != 0 {
|
||||
t.Errorf("expected empty list, got %d", len(out))
|
||||
}
|
||||
}
|
||||
|
||||
func TestListImages_WithData(t *testing.T) {
|
||||
h, _, reg, _ := newTestHandler(t)
|
||||
|
||||
reg.Register("a1", "host1", "alias1", "10.0.0.1", "amd64", "linux")
|
||||
reg.UpdateResources("a1",
|
||||
nil,
|
||||
[]*agentv1.ImageInfo{
|
||||
{Id: "sha256:abc", Tags: []string{"nginx:latest"}, Size: 50000000, CreatedAt: 1700000000},
|
||||
{Id: "sha256:def", Tags: []string{"redis:7"}, Size: 30000000, CreatedAt: 1700000001},
|
||||
},
|
||||
nil,
|
||||
nil,
|
||||
)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/images", nil)
|
||||
w := httptest.NewRecorder()
|
||||
h.ListImages(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", w.Code)
|
||||
}
|
||||
|
||||
var out []struct {
|
||||
AgentID string `json:"agent_id"`
|
||||
Hostname string `json:"hostname"`
|
||||
ID string `json:"id"`
|
||||
Tags []string `json:"tags"`
|
||||
Size int64 `json:"size"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
}
|
||||
json.NewDecoder(w.Body).Decode(&out)
|
||||
|
||||
if len(out) != 2 {
|
||||
t.Fatalf("expected 2 images, got %d", len(out))
|
||||
}
|
||||
if out[0].AgentID != "a1" {
|
||||
t.Errorf("expected agent_id 'a1', got %q", out[0].AgentID)
|
||||
}
|
||||
if out[0].ID != "sha256:abc" && out[1].ID != "sha256:abc" {
|
||||
t.Error("expected sha256:abc in results")
|
||||
}
|
||||
}
|
||||
|
||||
// ── ListVolumes ───────────────────────────────────────────────────────────────
|
||||
|
||||
func TestListVolumes_Empty(t *testing.T) {
|
||||
h, _, _, _ := newTestHandler(t)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/volumes", nil)
|
||||
w := httptest.NewRecorder()
|
||||
h.ListVolumes(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", w.Code)
|
||||
}
|
||||
|
||||
var out []map[string]any
|
||||
json.NewDecoder(w.Body).Decode(&out)
|
||||
if len(out) != 0 {
|
||||
t.Errorf("expected empty list, got %d", len(out))
|
||||
}
|
||||
}
|
||||
|
||||
func TestListVolumes_WithData(t *testing.T) {
|
||||
h, _, reg, _ := newTestHandler(t)
|
||||
|
||||
reg.Register("a1", "host1", "alias1", "10.0.0.1", "amd64", "linux")
|
||||
reg.UpdateResources("a1",
|
||||
nil,
|
||||
nil,
|
||||
[]*agentv1.VolumeInfo{
|
||||
{Name: "data", Driver: "local", Mountpoint: "/var/lib/docker/volumes/data/_data"},
|
||||
{Name: "cache", Driver: "local", Mountpoint: "/var/lib/docker/volumes/cache/_data"},
|
||||
},
|
||||
nil,
|
||||
)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/volumes", nil)
|
||||
w := httptest.NewRecorder()
|
||||
h.ListVolumes(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", w.Code)
|
||||
}
|
||||
|
||||
var out []struct {
|
||||
AgentID string `json:"agent_id"`
|
||||
Name string `json:"name"`
|
||||
Driver string `json:"driver"`
|
||||
Mountpoint string `json:"mountpoint"`
|
||||
}
|
||||
json.NewDecoder(w.Body).Decode(&out)
|
||||
|
||||
if len(out) != 2 {
|
||||
t.Fatalf("expected 2 volumes, got %d", len(out))
|
||||
}
|
||||
names := map[string]bool{out[0].Name: true, out[1].Name: true}
|
||||
if !names["data"] || !names["cache"] {
|
||||
t.Errorf("expected volumes 'data' and 'cache', got %v", names)
|
||||
}
|
||||
}
|
||||
|
||||
// ── ListNetworks ──────────────────────────────────────────────────────────────
|
||||
|
||||
func TestListNetworks_Empty(t *testing.T) {
|
||||
h, _, _, _ := newTestHandler(t)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/networks", nil)
|
||||
w := httptest.NewRecorder()
|
||||
h.ListNetworks(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", w.Code)
|
||||
}
|
||||
|
||||
var out []map[string]any
|
||||
json.NewDecoder(w.Body).Decode(&out)
|
||||
if len(out) != 0 {
|
||||
t.Errorf("expected empty list, got %d", len(out))
|
||||
}
|
||||
}
|
||||
|
||||
func TestListNetworks_WithData(t *testing.T) {
|
||||
h, _, reg, _ := newTestHandler(t)
|
||||
|
||||
reg.Register("a1", "host1", "alias1", "10.0.0.1", "amd64", "linux")
|
||||
reg.UpdateResources("a1",
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
[]*agentv1.NetworkInfo{
|
||||
{Id: "net1", Name: "bridge", Driver: "bridge", Scope: "local"},
|
||||
{Id: "net2", Name: "host", Driver: "host", Scope: "local"},
|
||||
},
|
||||
)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/networks", nil)
|
||||
w := httptest.NewRecorder()
|
||||
h.ListNetworks(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", w.Code)
|
||||
}
|
||||
|
||||
var out []struct {
|
||||
AgentID string `json:"agent_id"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Driver string `json:"driver"`
|
||||
Scope string `json:"scope"`
|
||||
}
|
||||
json.NewDecoder(w.Body).Decode(&out)
|
||||
|
||||
if len(out) != 2 {
|
||||
t.Fatalf("expected 2 networks, got %d", len(out))
|
||||
}
|
||||
ids := map[string]bool{out[0].ID: true, out[1].ID: true}
|
||||
if !ids["net1"] || !ids["net2"] {
|
||||
t.Errorf("expected net1 and net2, got %v", ids)
|
||||
}
|
||||
}
|
||||
|
||||
// ── ContainerAction ───────────────────────────────────────────────────────────
|
||||
|
||||
func TestContainerAction_AgentNotConnected(t *testing.T) {
|
||||
|
||||
@ -178,6 +178,91 @@ func (h *Handler) ContainerAction(w http.ResponseWriter, r *http.Request) {
|
||||
jsonOK(w, map[string]string{"command_id": cmdID})
|
||||
}
|
||||
|
||||
// ── Images ────────────────────────────────────────────────────────────────────
|
||||
|
||||
func (h *Handler) ListImages(w http.ResponseWriter, r *http.Request) {
|
||||
type imageDTO struct {
|
||||
AgentID string `json:"agent_id"`
|
||||
Hostname string `json:"hostname"`
|
||||
Alias string `json:"alias"`
|
||||
ID string `json:"id"`
|
||||
Tags []string `json:"tags"`
|
||||
Size int64 `json:"size"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
}
|
||||
var out []imageDTO
|
||||
for _, agent := range h.registry.List() {
|
||||
for _, img := range agent.Images {
|
||||
out = append(out, imageDTO{
|
||||
AgentID: agent.ID,
|
||||
Hostname: agent.Hostname,
|
||||
Alias: agent.Alias,
|
||||
ID: img.GetId(),
|
||||
Tags: img.GetTags(),
|
||||
Size: img.GetSize(),
|
||||
CreatedAt: img.GetCreatedAt(),
|
||||
})
|
||||
}
|
||||
}
|
||||
jsonOK(w, out)
|
||||
}
|
||||
|
||||
// ── Volumes ───────────────────────────────────────────────────────────────────
|
||||
|
||||
func (h *Handler) ListVolumes(w http.ResponseWriter, r *http.Request) {
|
||||
type volumeDTO struct {
|
||||
AgentID string `json:"agent_id"`
|
||||
Hostname string `json:"hostname"`
|
||||
Alias string `json:"alias"`
|
||||
Name string `json:"name"`
|
||||
Driver string `json:"driver"`
|
||||
Mountpoint string `json:"mountpoint"`
|
||||
}
|
||||
var out []volumeDTO
|
||||
for _, agent := range h.registry.List() {
|
||||
for _, vol := range agent.Volumes {
|
||||
out = append(out, volumeDTO{
|
||||
AgentID: agent.ID,
|
||||
Hostname: agent.Hostname,
|
||||
Alias: agent.Alias,
|
||||
Name: vol.GetName(),
|
||||
Driver: vol.GetDriver(),
|
||||
Mountpoint: vol.GetMountpoint(),
|
||||
})
|
||||
}
|
||||
}
|
||||
jsonOK(w, out)
|
||||
}
|
||||
|
||||
// ── Networks ──────────────────────────────────────────────────────────────────
|
||||
|
||||
func (h *Handler) ListNetworks(w http.ResponseWriter, r *http.Request) {
|
||||
type networkDTO struct {
|
||||
AgentID string `json:"agent_id"`
|
||||
Hostname string `json:"hostname"`
|
||||
Alias string `json:"alias"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Driver string `json:"driver"`
|
||||
Scope string `json:"scope"`
|
||||
}
|
||||
var out []networkDTO
|
||||
for _, agent := range h.registry.List() {
|
||||
for _, net := range agent.Networks {
|
||||
out = append(out, networkDTO{
|
||||
AgentID: agent.ID,
|
||||
Hostname: agent.Hostname,
|
||||
Alias: agent.Alias,
|
||||
ID: net.GetId(),
|
||||
Name: net.GetName(),
|
||||
Driver: net.GetDriver(),
|
||||
Scope: net.GetScope(),
|
||||
})
|
||||
}
|
||||
}
|
||||
jsonOK(w, out)
|
||||
}
|
||||
|
||||
// ── Agent token provisioning ──────────────────────────────────────────────────
|
||||
|
||||
func (h *Handler) CreateAgentToken(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@ -46,6 +46,9 @@ func NewRouter(h *Handler) http.Handler {
|
||||
r.Patch("/agents/{agentID}", h.UpdateAgent)
|
||||
r.Delete("/agents/{agentID}", h.DeleteAgent)
|
||||
r.Get("/containers", h.ListContainers)
|
||||
r.Get("/images", h.ListImages)
|
||||
r.Get("/volumes", h.ListVolumes)
|
||||
r.Get("/networks", h.ListNetworks)
|
||||
r.Post("/agents/{agentID}/containers/{containerID}/action", h.ContainerAction)
|
||||
r.Get("/agents/{agentID}/containers/{containerID}/logs", h.LogsWS)
|
||||
r.Get("/events", h.EventsWS)
|
||||
|
||||
Reference in New Issue
Block a user