Реализовано: - 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 тест пройдены
201 lines
5.5 KiB
Go
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
|
|
}
|