Files
GoClaw/gateway/internal/docker/client.go
Manus 0dcae37a78 Checkpoint: Phase 12: Real-time Docker Swarm monitoring for /nodes page
Реализовано:
- gateway/internal/docker/client.go: Docker API клиент через unix socket (/var/run/docker.sock)
  - IsSwarmActive(), GetSwarmInfo(), ListNodes(), ListContainers(), GetContainerStats()
  - CalcCPUPercent() для расчёта CPU%
- gateway/internal/api/handlers.go: новые endpoints
  - GET /api/nodes: список Swarm нод или standalone Docker хост
  - GET /api/nodes/stats: live CPU/RAM статистика контейнеров
  - POST /api/tools/execute: выполнение инструментов
- gateway/cmd/gateway/main.go: зарегистрированы новые маршруты
- server/gateway-proxy.ts: добавлены getGatewayNodes() и getGatewayNodeStats()
- server/routers.ts: добавлен nodes router (nodes.list, nodes.stats)
- client/src/pages/Nodes.tsx: полностью переписан на реальные данные
  - Auto-refresh: 10s для нод, 15s для статистики контейнеров
  - Swarm mode: показывает все ноды кластера
  - Standalone mode: показывает локальный Docker хост + контейнеры
  - CPU/RAM gauges из реальных docker stats
  - Error state при недоступном Gateway
  - Loading skeleton
- server/nodes.test.ts: 14 новых vitest тестов
- Все 51 тест пройдены
2026-03-20 20:12:57 -04:00

201 lines
5.5 KiB
Go

package docker
import (
"context"
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"time"
)
// DockerClient communicates with the Docker daemon via Unix socket or TCP.
type DockerClient struct {
httpClient *http.Client
baseURL string
}
// NewDockerClient creates a client that talks to /var/run/docker.sock.
func NewDockerClient() *DockerClient {
transport := &http.Transport{
DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
return (&net.Dialer{Timeout: 5 * time.Second}).DialContext(ctx, "unix", "/var/run/docker.sock")
},
}
return &DockerClient{
httpClient: &http.Client{Transport: transport, Timeout: 10 * time.Second},
baseURL: "http://localhost", // host is ignored for unix socket
}
}
func (c *DockerClient) get(path string, out interface{}) error {
resp, err := c.httpClient.Get(c.baseURL + path)
if err != nil {
return fmt.Errorf("docker GET %s: %w", path, err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
if resp.StatusCode >= 400 {
return fmt.Errorf("docker GET %s: status %d: %s", path, resp.StatusCode, string(body))
}
return json.Unmarshal(body, out)
}
// ---- Types ----------------------------------------------------------------
type SwarmNode struct {
ID string `json:"ID"`
Description NodeDescription `json:"Description"`
Status NodeStatus `json:"Status"`
ManagerStatus *ManagerStatus `json:"ManagerStatus,omitempty"`
Spec NodeSpec `json:"Spec"`
UpdatedAt time.Time `json:"UpdatedAt"`
CreatedAt time.Time `json:"CreatedAt"`
}
type NodeDescription struct {
Hostname string `json:"Hostname"`
Platform Platform `json:"Platform"`
Resources Resources `json:"Resources"`
Engine Engine `json:"Engine"`
}
type Platform struct {
Architecture string `json:"Architecture"`
OS string `json:"OS"`
}
type Resources struct {
NanoCPUs int64 `json:"NanoCPUs"`
MemoryBytes int64 `json:"MemoryBytes"`
}
type Engine struct {
EngineVersion string `json:"EngineVersion"`
}
type NodeStatus struct {
State string `json:"State"`
Addr string `json:"Addr"`
Message string `json:"Message"`
}
type ManagerStatus struct {
Addr string `json:"Addr"`
Leader bool `json:"Leader"`
Reachability string `json:"Reachability"`
}
type NodeSpec struct {
Role string `json:"Role"`
Availability string `json:"Availability"`
Labels map[string]string `json:"Labels"`
}
type Container struct {
ID string `json:"Id"`
Names []string `json:"Names"`
Image string `json:"Image"`
State string `json:"State"`
Status string `json:"Status"`
Labels map[string]string `json:"Labels"`
}
type ContainerStats struct {
CPUStats CPUStats `json:"cpu_stats"`
PreCPUStats CPUStats `json:"precpu_stats"`
MemoryStats MemoryStats `json:"memory_stats"`
}
type CPUStats struct {
CPUUsage CPUUsage `json:"cpu_usage"`
SystemCPUUsage int64 `json:"system_cpu_usage"`
OnlineCPUs int `json:"online_cpus"`
}
type CPUUsage struct {
TotalUsage int64 `json:"total_usage"`
PercpuUsage []int64 `json:"percpu_usage"`
}
type MemoryStats struct {
Usage int64 `json:"usage"`
MaxUsage int64 `json:"max_usage"`
Limit int64 `json:"limit"`
Stats map[string]int64 `json:"stats"`
}
type DockerInfo struct {
Swarm SwarmInfo `json:"Swarm"`
}
type SwarmInfo struct {
NodeID string `json:"NodeID"`
LocalNodeState string `json:"LocalNodeState"`
ControlAvailable bool `json:"ControlAvailable"`
Managers int `json:"Managers"`
Nodes int `json:"Nodes"`
}
// ---- Methods ---------------------------------------------------------------
// IsSwarmActive checks if Docker Swarm is initialized.
func (c *DockerClient) IsSwarmActive() bool {
var info DockerInfo
if err := c.get("/v1.41/info", &info); err != nil {
return false
}
return info.Swarm.LocalNodeState == "active"
}
// GetSwarmInfo returns basic swarm info.
func (c *DockerClient) GetSwarmInfo() (*DockerInfo, error) {
var info DockerInfo
if err := c.get("/v1.41/info", &info); err != nil {
return nil, err
}
return &info, nil
}
// ListNodes returns all Swarm nodes (requires manager node).
func (c *DockerClient) ListNodes() ([]SwarmNode, error) {
var nodes []SwarmNode
if err := c.get("/v1.41/nodes", &nodes); err != nil {
return nil, err
}
return nodes, nil
}
// ListContainers returns all running containers on this host.
func (c *DockerClient) ListContainers() ([]Container, error) {
var containers []Container
if err := c.get("/v1.41/containers/json?all=false", &containers); err != nil {
return nil, err
}
return containers, nil
}
// GetContainerStats returns one-shot stats for a container (no streaming).
func (c *DockerClient) GetContainerStats(containerID string) (*ContainerStats, error) {
var stats ContainerStats
if err := c.get(fmt.Sprintf("/v1.41/containers/%s/stats?stream=false", containerID), &stats); err != nil {
return nil, err
}
return &stats, nil
}
// CalcCPUPercent computes CPU usage % from two consecutive stats snapshots.
func CalcCPUPercent(stats *ContainerStats) float64 {
cpuDelta := float64(stats.CPUStats.CPUUsage.TotalUsage) - float64(stats.PreCPUStats.CPUUsage.TotalUsage)
systemDelta := float64(stats.CPUStats.SystemCPUUsage) - float64(stats.PreCPUStats.SystemCPUUsage)
numCPU := float64(stats.CPUStats.OnlineCPUs)
if numCPU == 0 {
numCPU = float64(len(stats.CPUStats.CPUUsage.PercpuUsage))
}
if systemDelta > 0 && cpuDelta > 0 {
return (cpuDelta / systemDelta) * numCPU * 100.0
}
return 0
}