first commit
This commit is contained in:
@@ -0,0 +1,320 @@
|
||||
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])
|
||||
}
|
||||
Reference in New Issue
Block a user