321 lines
9.5 KiB
Go
321 lines
9.5 KiB
Go
package agent
|
|
|
|
import (
|
|
"context"
|
|
"encoding/binary"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/docker/docker/api/types/container"
|
|
"github.com/docker/docker/api/types/image"
|
|
"github.com/docker/docker/client"
|
|
)
|
|
|
|
// Docker 工具:通过 Docker API 操作本地或远程 Docker。
|
|
// socket 地址通过 DockerConfigStore 动态读取,用户在 Web 页面配置后即时生效。
|
|
|
|
func (t *Toolset) dockerClient() (*client.Client, error) {
|
|
socket := t.dockerSocket()
|
|
cli, err := client.NewClientWithOpts(
|
|
client.WithHost(socket),
|
|
client.WithAPIVersionNegotiation(),
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("创建 Docker 客户端失败: %w", err)
|
|
}
|
|
return cli, nil
|
|
}
|
|
|
|
func (t *Toolset) dockerPS(ctx context.Context, params map[string]any) ToolResult {
|
|
all, _ := params["all"].(bool)
|
|
cli, err := t.dockerClient()
|
|
if err != nil {
|
|
return ToolResult{Success: false, Output: err.Error()}
|
|
}
|
|
defer cli.Close()
|
|
|
|
ctx, cancel := context.WithTimeout(ctx, t.Timeout)
|
|
defer cancel()
|
|
list, err := cli.ContainerList(ctx, container.ListOptions{All: all})
|
|
if err != nil {
|
|
return ToolResult{Success: false, Output: "Docker 操作失败: " + err.Error()}
|
|
}
|
|
if len(list) == 0 {
|
|
return ToolResult{Success: true, Output: "没有找到容器"}
|
|
}
|
|
|
|
sort.Slice(list, func(i, j int) bool {
|
|
return list[i].Created > list[j].Created
|
|
})
|
|
var out strings.Builder
|
|
fmt.Fprintf(&out, "%-14s %-26s %-20s %-12s %s\n", "CONTAINER ID", "NAME", "IMAGE", "STATUS", "PORTS")
|
|
for _, c := range list {
|
|
name := strings.TrimPrefix(strings.Join(c.Names, ","), "/")
|
|
fmt.Fprintf(&out, "%-14s %-26s %-20s %-12s %s\n",
|
|
c.ID[:min(12, len(c.ID))], name, c.Image, c.Status, dockerPorts(c.Ports))
|
|
}
|
|
return ToolResult{Success: true, Output: t.truncate(out.String())}
|
|
}
|
|
|
|
func (t *Toolset) dockerImages(ctx context.Context, params map[string]any) ToolResult {
|
|
cli, err := t.dockerClient()
|
|
if err != nil {
|
|
return ToolResult{Success: false, Output: err.Error()}
|
|
}
|
|
defer cli.Close()
|
|
|
|
ctx, cancel := context.WithTimeout(ctx, t.Timeout)
|
|
defer cancel()
|
|
list, err := cli.ImageList(ctx, image.ListOptions{All: true})
|
|
if err != nil {
|
|
return ToolResult{Success: false, Output: "Docker 操作失败: " + err.Error()}
|
|
}
|
|
if len(list) == 0 {
|
|
return ToolResult{Success: true, Output: "没有找到镜像"}
|
|
}
|
|
|
|
sort.Slice(list, func(i, j int) bool {
|
|
return list[i].Created > list[j].Created
|
|
})
|
|
var out strings.Builder
|
|
fmt.Fprintf(&out, "%-22s %-28s %-14s %s\n", "REPOSITORY", "TAG", "IMAGE ID", "SIZE")
|
|
for _, img := range list {
|
|
if len(img.RepoTags) == 0 {
|
|
continue
|
|
}
|
|
for _, tag := range img.RepoTags {
|
|
repo, imageTag, _ := strings.Cut(tag, ":")
|
|
id := img.ID
|
|
if len(id) > 12 {
|
|
id = id[7:19]
|
|
}
|
|
fmt.Fprintf(&out, "%-22s %-28s %-14s %s\n", repo, imageTag, id, formatBytes(img.Size))
|
|
}
|
|
}
|
|
return ToolResult{Success: true, Output: t.truncate(out.String())}
|
|
}
|
|
|
|
func (t *Toolset) dockerLogs(ctx context.Context, params map[string]any) ToolResult {
|
|
containerID, _ := params["container"].(string)
|
|
containerID = strings.TrimSpace(containerID)
|
|
if containerID == "" {
|
|
return ToolResult{Success: false, Output: "container 不能为空"}
|
|
}
|
|
tail := intParam(params, "tail")
|
|
if tail <= 0 {
|
|
tail = 100
|
|
}
|
|
|
|
cli, err := t.dockerClient()
|
|
if err != nil {
|
|
return ToolResult{Success: false, Output: err.Error()}
|
|
}
|
|
defer cli.Close()
|
|
|
|
ctx, cancel := context.WithTimeout(ctx, t.Timeout)
|
|
defer cancel()
|
|
resp, err := cli.ContainerLogs(ctx, containerID, container.LogsOptions{
|
|
ShowStdout: true,
|
|
ShowStderr: true,
|
|
Tail: strconv.Itoa(tail),
|
|
})
|
|
if err != nil {
|
|
return ToolResult{Success: false, Output: "Docker 操作失败: " + err.Error()}
|
|
}
|
|
defer resp.Close()
|
|
|
|
raw, err := io.ReadAll(resp)
|
|
if err != nil {
|
|
return ToolResult{Success: false, Output: "读取 Docker 日志失败: " + err.Error()}
|
|
}
|
|
return ToolResult{Success: true, Output: t.truncate(demuxDockerLogs(raw))}
|
|
}
|
|
|
|
// demuxDockerLogs 解析 Docker 日志流:多路复用流(stdout/stderr 分帧)按帧解出,
|
|
// 普通文本流(TTY 容器)直接返回。
|
|
func demuxDockerLogs(data []byte) string {
|
|
if len(data) >= 8 && isMultiplexedFrame(data[0:8]) {
|
|
var out strings.Builder
|
|
for i := 0; i+8 <= len(data); {
|
|
frameSize := int(binary.BigEndian.Uint32(data[i+4 : i+8]))
|
|
i += 8
|
|
if frameSize < 0 || i+frameSize > len(data) {
|
|
break
|
|
}
|
|
out.Write(data[i : i+frameSize])
|
|
i += frameSize
|
|
}
|
|
return out.String()
|
|
}
|
|
return string(data)
|
|
}
|
|
|
|
func isMultiplexedFrame(header []byte) bool {
|
|
streamType := header[0]
|
|
if streamType != 0 && streamType != 1 && streamType != 2 {
|
|
return false
|
|
}
|
|
return header[1] == 0 && header[2] == 0 && header[3] == 0
|
|
}
|
|
|
|
func (t *Toolset) dockerInspect(ctx context.Context, params map[string]any) ToolResult {
|
|
containerID, _ := params["container"].(string)
|
|
containerID = strings.TrimSpace(containerID)
|
|
if containerID == "" {
|
|
return ToolResult{Success: false, Output: "container 不能为空"}
|
|
}
|
|
|
|
cli, err := t.dockerClient()
|
|
if err != nil {
|
|
return ToolResult{Success: false, Output: err.Error()}
|
|
}
|
|
defer cli.Close()
|
|
|
|
ctx, cancel := context.WithTimeout(ctx, t.Timeout)
|
|
defer cancel()
|
|
inspect, err := cli.ContainerInspect(ctx, containerID)
|
|
if err != nil {
|
|
return ToolResult{Success: false, Output: "Docker 操作失败: " + err.Error()}
|
|
}
|
|
|
|
state := inspect.State
|
|
if state == nil {
|
|
state = &container.State{}
|
|
}
|
|
summary := map[string]any{
|
|
"id": inspect.ID,
|
|
"name": strings.TrimPrefix(inspect.Name, "/"),
|
|
"image": inspect.Config.Image,
|
|
"status": state.Status,
|
|
"running": state.Running,
|
|
"exit_code": state.ExitCode,
|
|
"restart_count": inspect.RestartCount,
|
|
"created": inspect.Created,
|
|
"command": inspect.Config.Cmd,
|
|
"env_count": len(inspect.Config.Env),
|
|
"ports": inspect.NetworkSettings.Ports,
|
|
"network_mode": string(inspect.HostConfig.NetworkMode),
|
|
"ip_address": inspect.NetworkSettings.IPAddress,
|
|
}
|
|
data, _ := json.MarshalIndent(summary, "", " ")
|
|
return ToolResult{Success: true, Output: t.truncate(string(data))}
|
|
}
|
|
|
|
func (t *Toolset) dockerExec(ctx context.Context, params map[string]any) ToolResult {
|
|
containerID, _ := params["container"].(string)
|
|
containerID = strings.TrimSpace(containerID)
|
|
command, _ := params["command"].(string)
|
|
command = strings.TrimSpace(command)
|
|
if containerID == "" || command == "" {
|
|
return ToolResult{Success: false, Output: "container 和 command 不能为空"}
|
|
}
|
|
|
|
cli, err := t.dockerClient()
|
|
if err != nil {
|
|
return ToolResult{Success: false, Output: err.Error()}
|
|
}
|
|
defer cli.Close()
|
|
|
|
ctx, cancel := context.WithTimeout(ctx, t.Timeout)
|
|
defer cancel()
|
|
execCfg := container.ExecOptions{
|
|
AttachStdout: true,
|
|
AttachStderr: true,
|
|
Cmd: []string{"sh", "-c", command},
|
|
}
|
|
created, err := cli.ContainerExecCreate(ctx, containerID, execCfg)
|
|
if err != nil {
|
|
return ToolResult{Success: false, Output: "Docker 操作失败: " + err.Error()}
|
|
}
|
|
attach, err := cli.ContainerExecAttach(ctx, created.ID, container.ExecAttachOptions{})
|
|
if err != nil {
|
|
return ToolResult{Success: false, Output: "Docker 操作失败: " + err.Error()}
|
|
}
|
|
defer attach.Close()
|
|
|
|
raw, err := io.ReadAll(attach.Reader)
|
|
if err != nil {
|
|
return ToolResult{Success: false, Output: "读取命令输出失败: " + err.Error()}
|
|
}
|
|
return ToolResult{Success: true, Output: t.truncate(demuxDockerLogs(raw))}
|
|
}
|
|
|
|
func (t *Toolset) dockerStart(ctx context.Context, params map[string]any) ToolResult {
|
|
containerID, _ := params["container"].(string)
|
|
containerID = strings.TrimSpace(containerID)
|
|
if containerID == "" {
|
|
return ToolResult{Success: false, Output: "container 不能为空"}
|
|
}
|
|
|
|
cli, err := t.dockerClient()
|
|
if err != nil {
|
|
return ToolResult{Success: false, Output: err.Error()}
|
|
}
|
|
defer cli.Close()
|
|
|
|
ctx, cancel := context.WithTimeout(ctx, t.Timeout)
|
|
defer cancel()
|
|
if err := cli.ContainerStart(ctx, containerID, container.StartOptions{}); err != nil {
|
|
return ToolResult{Success: false, Output: "Docker 操作失败: " + err.Error()}
|
|
}
|
|
return ToolResult{Success: true, Output: "容器 " + containerID + " 已启动"}
|
|
}
|
|
|
|
func (t *Toolset) dockerStop(ctx context.Context, params map[string]any) ToolResult {
|
|
containerID, _ := params["container"].(string)
|
|
containerID = strings.TrimSpace(containerID)
|
|
if containerID == "" {
|
|
return ToolResult{Success: false, Output: "container 不能为空"}
|
|
}
|
|
timeoutSeconds := intParam(params, "timeout")
|
|
|
|
cli, err := t.dockerClient()
|
|
if err != nil {
|
|
return ToolResult{Success: false, Output: err.Error()}
|
|
}
|
|
defer cli.Close()
|
|
|
|
ctx, cancel := context.WithTimeout(ctx, t.Timeout)
|
|
defer cancel()
|
|
var timeout *int
|
|
if timeoutSeconds > 0 {
|
|
timeout = &timeoutSeconds
|
|
}
|
|
if err := cli.ContainerStop(ctx, containerID, container.StopOptions{Timeout: timeout}); err != nil {
|
|
return ToolResult{Success: false, Output: "Docker 操作失败: " + err.Error()}
|
|
}
|
|
return ToolResult{Success: true, Output: "容器 " + containerID + " 已停止"}
|
|
}
|
|
|
|
func dockerPorts(ports []container.Port) string {
|
|
if len(ports) == 0 {
|
|
return ""
|
|
}
|
|
var parts []string
|
|
for _, p := range ports {
|
|
host := ""
|
|
if p.PublicPort > 0 {
|
|
host = fmt.Sprintf("%d->", p.PublicPort)
|
|
}
|
|
parts = append(parts, fmt.Sprintf("%s%d/%s", host, p.PrivatePort, p.Type))
|
|
}
|
|
return strings.Join(parts, ", ")
|
|
}
|
|
|
|
func formatBytes(size int64) string {
|
|
const unit = 1024
|
|
if size < unit {
|
|
return fmt.Sprintf("%dB", size)
|
|
}
|
|
div, exp := int64(unit), 0
|
|
for n := size / unit; n >= unit; n /= unit {
|
|
div *= unit
|
|
exp++
|
|
}
|
|
return fmt.Sprintf("%.1f%cB", float64(size)/float64(div), "KMGTPE"[exp])
|
|
}
|