From 7521b0f78836ec89c10fec7c7c32422c69e49b9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=80=97=E5=AD=90?= Date: Sun, 27 Oct 2024 17:33:34 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BD=BF=E7=94=A8cli=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=E5=AE=B9=E5=99=A8=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go.mod | 2 +- internal/biz/container.go | 7 +- internal/data/container.go | 302 +++++++++++++++++------------ internal/http/request/container.go | 36 ++-- internal/service/container.go | 28 +-- pkg/types/container.go | 22 +++ 6 files changed, 227 insertions(+), 170 deletions(-) diff --git a/go.mod b/go.mod index 0b810b025a..cb6ec8562b 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,6 @@ require ( github.com/bddjr/hlfhr v1.1.2 github.com/beevik/ntp v1.4.3 github.com/docker/docker v27.3.1+incompatible - github.com/docker/go-connections v0.5.0 github.com/expr-lang/expr v1.16.9 github.com/glebarez/sqlite v1.11.0 github.com/go-chi/chi/v5 v5.1.0 @@ -63,6 +62,7 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/devhaozi/huaweicloud-sdk-go-v3 v0.0.0-20241018211007-bbebb6de5db7 // indirect github.com/distribution/reference v0.6.0 // indirect + github.com/docker/go-connections v0.5.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect github.com/dustin/go-humanize v1.0.1 // indirect diff --git a/internal/biz/container.go b/internal/biz/container.go index c5616ccbfa..5d943820b3 100644 --- a/internal/biz/container.go +++ b/internal/biz/container.go @@ -1,15 +1,13 @@ package biz import ( - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/container" - "github.com/TheTNB/panel/internal/http/request" + "github.com/TheTNB/panel/pkg/types" ) type ContainerRepo interface { ListAll() ([]types.Container, error) - ListByNames(names []string) ([]types.Container, error) + ListByName(name string) ([]types.Container, error) Create(req *request.ContainerCreate) (string, error) Remove(id string) error Start(id string) error @@ -19,7 +17,6 @@ type ContainerRepo interface { Unpause(id string) error Kill(id string) error Rename(id string, newName string) error - Update(id string, config container.UpdateConfig) error Logs(id string) (string, error) Prune() error } diff --git a/internal/data/container.go b/internal/data/container.go index efe16aecae..f792f04635 100644 --- a/internal/data/container.go +++ b/internal/data/container.go @@ -1,213 +1,259 @@ package data import ( - "context" + "encoding/json" "fmt" - "io" - "strconv" + "regexp" + "slices" + "strings" + "time" - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/api/types/filters" - "github.com/docker/docker/api/types/network" - "github.com/docker/docker/client" - "github.com/docker/go-connections/nat" + "github.com/spf13/cast" "github.com/TheTNB/panel/internal/biz" "github.com/TheTNB/panel/internal/http/request" - paneltypes "github.com/TheTNB/panel/pkg/types" + "github.com/TheTNB/panel/pkg/shell" + "github.com/TheTNB/panel/pkg/types" ) type containerRepo struct { - client *client.Client + cmd string } -func NewContainerRepo(sock ...string) biz.ContainerRepo { - if len(sock) == 0 { - sock = append(sock, "/run/podman/podman.sock") +func NewContainerRepo(cmd ...string) biz.ContainerRepo { + if len(cmd) == 0 { + cmd = append(cmd, "docker") } - cli, _ := client.NewClientWithOpts(client.WithHost("unix://"+sock[0]), client.WithAPIVersionNegotiation()) return &containerRepo{ - client: cli, + cmd: cmd[0], } } // ListAll 列出所有容器 func (r *containerRepo) ListAll() ([]types.Container, error) { - containers, err := r.client.ContainerList(context.Background(), container.ListOptions{ - All: true, - }) + output, err := shell.ExecfWithTimeout(10*time.Second, "%s ps -a --format '{{json .}}'", r.cmd) if err != nil { return nil, err } + lines := strings.Split(output, "\n") - return containers, nil -} + var containers []types.Container + for _, line := range lines { + if line == "" { + continue // 跳过空行 + } -// ListByNames 根据名称列出容器 -func (r *containerRepo) ListByNames(names []string) ([]types.Container, error) { - var options container.ListOptions - options.All = true - if len(names) > 0 { - var array []filters.KeyValuePair - for _, n := range names { - array = append(array, filters.Arg("name", n)) + var item struct { + Command string `json:"Command"` + CreatedAt string `json:"CreatedAt"` + ID string `json:"ID"` + Image string `json:"Image"` + Labels string `json:"Labels"` + LocalVolumes string `json:"LocalVolumes"` + Mounts string `json:"Mounts"` + Names string `json:"Names"` + Networks string `json:"Networks"` + Ports string `json:"Ports"` + RunningFor string `json:"RunningFor"` + Size string `json:"Size"` + State string `json:"State"` + Status string `json:"Status"` + } + if err = json.Unmarshal([]byte(line), &item); err != nil { + return nil, err } - options.Filters = filters.NewArgs(array...) + + createdAt, _ := time.Parse("2006-01-02 15:04:05 -0700 MST", item.CreatedAt) + + containers = append(containers, types.Container{ + ID: item.ID, + Name: item.Names, + Image: item.Image, + Command: item.Command, + CreatedAt: createdAt, + Ports: r.parsePorts(item.Ports), + Labels: r.parseLabels(item.Labels), + State: item.State, + Status: item.Status, + }) } - containers, err := r.client.ContainerList(context.Background(), options) + + return containers, nil +} + +// ListByName 根据名称搜索容器 +func (r *containerRepo) ListByName(names string) ([]types.Container, error) { + containers, err := r.ListAll() if err != nil { return nil, err } + containers = slices.DeleteFunc(containers, func(item types.Container) bool { + return !strings.Contains(item.Name, names) + }) + return containers, nil } // Create 创建容器 func (r *containerRepo) Create(req *request.ContainerCreate) (string, error) { - var hostConf container.HostConfig - var networkConf network.NetworkingConfig + sb := strings.Builder{} + sb.WriteString(fmt.Sprintf("%s create --name %s --image %s", r.cmd, req.Name, req.Image)) - portMap := make(nat.PortMap) for _, port := range req.Ports { - if port.ContainerStart-port.ContainerEnd != port.HostStart-port.HostEnd { - return "", fmt.Errorf("容器端口和主机端口数量不匹配(容器: %d 主机: %d)", port.ContainerStart-port.ContainerEnd, port.HostStart-port.HostEnd) - } - if port.ContainerStart > port.ContainerEnd || port.HostStart > port.HostEnd || port.ContainerStart < 1 || port.HostStart < 1 { - return "", fmt.Errorf("端口范围不正确") - } - - count := 0 - for host := port.HostStart; host <= port.HostEnd; host++ { - bindItem := nat.PortBinding{HostPort: strconv.Itoa(host), HostIP: port.Host} - portMap[nat.Port(fmt.Sprintf("%d/%s", port.ContainerStart+count, port.Protocol))] = []nat.PortBinding{bindItem} - count++ - } + sb.WriteString(fmt.Sprintf(" -p %s:%d", port.Host, port.ContainerStart)) } - - exposed := make(nat.PortSet) - for port := range portMap { - exposed[port] = struct{}{} - } - if req.Network != "" { - switch req.Network { - case "host", "none", "bridge": - hostConf.NetworkMode = container.NetworkMode(req.Network) - } - networkConf.EndpointsConfig = map[string]*network.EndpointSettings{req.Network: {}} - } else { - networkConf = network.NetworkingConfig{} - } - - hostConf.Privileged = req.Privileged - hostConf.AutoRemove = req.AutoRemove - hostConf.CPUShares = req.CPUShares - hostConf.PublishAllPorts = req.PublishAllPorts - hostConf.RestartPolicy = container.RestartPolicy{Name: container.RestartPolicyMode(req.RestartPolicy)} - if req.RestartPolicy == "on-failure" { - hostConf.RestartPolicy.MaximumRetryCount = 5 - } - hostConf.NanoCPUs = req.CPUs * 1000000000 - hostConf.Memory = req.Memory * 1024 * 1024 - hostConf.MemorySwap = 0 - hostConf.PortBindings = portMap - hostConf.Binds = []string{} - - volumes := make(map[string]struct{}) - for _, v := range req.Volumes { - volumes[v.Container] = struct{}{} - hostConf.Binds = append(hostConf.Binds, fmt.Sprintf("%s:%s:%s", v.Host, v.Container, v.Mode)) - } - - resp, err := r.client.ContainerCreate(context.Background(), &container.Config{ - Image: req.Image, - Env: paneltypes.KVToSlice(req.Env), - Entrypoint: req.Entrypoint, - Cmd: req.Command, - Labels: paneltypes.KVToMap(req.Labels), - ExposedPorts: exposed, - OpenStdin: req.OpenStdin, - Tty: req.Tty, - Volumes: volumes, - }, &hostConf, &networkConf, nil, req.Name) - if err != nil { - return "", err + sb.WriteString(fmt.Sprintf(" --network %s", req.Network)) + } + for _, volume := range req.Volumes { + sb.WriteString(fmt.Sprintf(" -v %s:%s:%s", volume.Host, volume.Container, volume.Mode)) + } + for _, label := range req.Labels { + sb.WriteString(fmt.Sprintf(" --label %s=%s", label.Key, label.Value)) + } + for _, env := range req.Env { + sb.WriteString(fmt.Sprintf(" -e %s=%s", env.Key, env.Value)) + } + if len(req.Entrypoint) > 0 { + sb.WriteString(fmt.Sprintf(" --entrypoint '%s'", strings.Join(req.Entrypoint, " "))) + } + if len(req.Command) > 0 { + sb.WriteString(fmt.Sprintf(" '%s'", strings.Join(req.Command, " "))) + } + if req.RestartPolicy != "" { + sb.WriteString(fmt.Sprintf(" --restart %s", req.RestartPolicy)) + } + if req.AutoRemove { + sb.WriteString(" --rm") + } + if req.Privileged { + sb.WriteString(" --privileged") + } + if req.OpenStdin { + sb.WriteString(" -i") + } + if req.PublishAllPorts { + sb.WriteString(" -P") + } + if req.Tty { + sb.WriteString(" -t") + } + if req.CPUShares > 0 { + sb.WriteString(fmt.Sprintf(" --cpu-shares %d", req.CPUShares)) + } + if req.CPUs > 0 { + sb.WriteString(fmt.Sprintf(" --cpus %d", req.CPUs)) + } + if req.Memory > 0 { + sb.WriteString(fmt.Sprintf(" --memory %d", req.Memory)) } - return resp.ID, err + return shell.ExecfWithTimeout(10*time.Second, sb.String()) // nolint: govet } // Remove 移除容器 func (r *containerRepo) Remove(id string) error { - return r.client.ContainerRemove(context.Background(), id, container.RemoveOptions{ - Force: true, - }) + _, err := shell.ExecfWithTimeout(10*time.Second, "%s rm -f %s", r.cmd, id) + return err } // Start 启动容器 func (r *containerRepo) Start(id string) error { - return r.client.ContainerStart(context.Background(), id, container.StartOptions{}) + _, err := shell.ExecfWithTimeout(10*time.Second, "%s start %s", r.cmd, id) + return err } // Stop 停止容器 func (r *containerRepo) Stop(id string) error { - return r.client.ContainerStop(context.Background(), id, container.StopOptions{}) + _, err := shell.ExecfWithTimeout(10*time.Second, "%s stop %s", r.cmd, id) + return err } // Restart 重启容器 func (r *containerRepo) Restart(id string) error { - return r.client.ContainerRestart(context.Background(), id, container.StopOptions{}) + _, err := shell.ExecfWithTimeout(10*time.Second, "%s restart %s", r.cmd, id) + return err } // Pause 暂停容器 func (r *containerRepo) Pause(id string) error { - return r.client.ContainerPause(context.Background(), id) + _, err := shell.ExecfWithTimeout(10*time.Second, "%s pause %s", r.cmd, id) + return err } // Unpause 恢复容器 func (r *containerRepo) Unpause(id string) error { - return r.client.ContainerUnpause(context.Background(), id) + _, err := shell.ExecfWithTimeout(10*time.Second, "%s unpause %s", r.cmd, id) + return err } // Kill 杀死容器 func (r *containerRepo) Kill(id string) error { - return r.client.ContainerKill(context.Background(), id, "KILL") + _, err := shell.ExecfWithTimeout(10*time.Second, "%s kill %s", r.cmd, id) + return err } // Rename 重命名容器 func (r *containerRepo) Rename(id string, newName string) error { - return r.client.ContainerRename(context.Background(), id, newName) -} - -// Update 更新容器 -func (r *containerRepo) Update(id string, config container.UpdateConfig) error { - _, err := r.client.ContainerUpdate(context.Background(), id, config) + _, err := shell.ExecfWithTimeout(10*time.Second, "%s rename %s %s", r.cmd, id, newName) return err } // Logs 查看容器日志 func (r *containerRepo) Logs(id string) (string, error) { - options := container.LogsOptions{ - ShowStdout: true, - ShowStderr: true, - } - reader, err := r.client.ContainerLogs(context.Background(), id, options) - if err != nil { - return "", err - } - defer reader.Close() - - data, err := io.ReadAll(reader) - if err != nil { - return "", err - } - - return string(data), nil + return shell.ExecfWithTimeout(10*time.Second, "%s logs %s", r.cmd, id) } // Prune 清理未使用的容器 func (r *containerRepo) Prune() error { - _, err := r.client.ContainersPrune(context.Background(), filters.NewArgs()) + _, err := shell.ExecfWithTimeout(10*time.Second, "%s container prune -f", r.cmd) return err } + +func (r *containerRepo) parseLabels(labels string) []types.KV { + var result []types.KV + if labels == "" { + return result + } + + pairs := strings.Split(labels, ",") + for _, pair := range pairs { + kv := strings.SplitN(pair, "=", 2) + if len(kv) == 2 { + result = append(result, types.KV{ + Key: strings.TrimSpace(kv[0]), + Value: strings.TrimSpace(kv[1]), + }) + } + } + return result +} + +func (r *containerRepo) parsePorts(ports string) []types.ContainerPort { + var portList []types.ContainerPort + + re := regexp.MustCompile(`(?P[\d.:]+)?:(?P\d+)->(?P\d+)/(?P\w+)`) + + entries := strings.Split(ports, ", ") // 0.0.0.0:3306->3306/tcp, :::3306->3306/tcp, 33060/tcp + for _, entry := range entries { + matches := re.FindStringSubmatch(entry) + if len(matches) == 0 { + continue + } + + host := matches[1] + public := matches[2] + private := matches[3] + protocol := matches[4] + + portList = append(portList, types.ContainerPort{ + IP: host, + PublicPort: cast.ToUint(public), + PrivatePort: cast.ToUint(private), + Type: protocol, + }) + } + + return portList +} diff --git a/internal/http/request/container.go b/internal/http/request/container.go index db17ede336..b34b7157df 100644 --- a/internal/http/request/container.go +++ b/internal/http/request/container.go @@ -12,22 +12,22 @@ type ContainerRename struct { } type ContainerCreate struct { - Name string `form:"name" json:"name" validate:"required"` - Image string `form:"image" json:"image" validate:"required"` - Ports []types.ContainerPort `form:"ports" json:"ports"` - Network string `form:"network" json:"network"` - Volumes []types.ContainerVolume `form:"volumes" json:"volumes"` - Labels []types.KV `form:"labels" json:"labels"` - Env []types.KV `form:"env" json:"env"` - Entrypoint []string `form:"entrypoint" json:"entrypoint"` - Command []string `form:"command" json:"command"` - RestartPolicy string `form:"restart_policy" json:"restart_policy"` - AutoRemove bool `form:"auto_remove" json:"auto_remove"` - Privileged bool `form:"privileged" json:"privileged"` - OpenStdin bool `form:"openStdin" json:"open_stdin"` - PublishAllPorts bool `form:"publish_all_ports" json:"publish_all_ports"` - Tty bool `form:"tty" json:"tty"` - CPUShares int64 `form:"cpu_shares" json:"cpu_shares"` - CPUs int64 `form:"cpus" json:"cpus"` - Memory int64 `form:"memory" json:"memory"` + Name string `form:"name" json:"name" validate:"required"` + Image string `form:"image" json:"image" validate:"required"` + Ports []types.ContainerCreatePort `form:"ports" json:"ports"` + Network string `form:"network" json:"network"` + Volumes []types.ContainerVolume `form:"volumes" json:"volumes"` + Labels []types.KV `form:"labels" json:"labels"` + Env []types.KV `form:"env" json:"env"` + Entrypoint []string `form:"entrypoint" json:"entrypoint"` + Command []string `form:"command" json:"command"` + RestartPolicy string `form:"restart_policy" json:"restart_policy"` + AutoRemove bool `form:"auto_remove" json:"auto_remove"` + Privileged bool `form:"privileged" json:"privileged"` + OpenStdin bool `form:"openStdin" json:"open_stdin"` + PublishAllPorts bool `form:"publish_all_ports" json:"publish_all_ports"` + Tty bool `form:"tty" json:"tty"` + CPUShares int64 `form:"cpu_shares" json:"cpu_shares"` + CPUs int64 `form:"cpus" json:"cpus"` + Memory int64 `form:"memory" json:"memory"` } diff --git a/internal/service/container.go b/internal/service/container.go index 8d8e82b1d1..bd6c76526b 100644 --- a/internal/service/container.go +++ b/internal/service/container.go @@ -2,8 +2,6 @@ package service import ( "net/http" - "strings" - "time" "github.com/go-rat/chix" @@ -31,21 +29,16 @@ func (s *ContainerService) List(w http.ResponseWriter, r *http.Request) { paged, total := Paginate(r, containers) items := make([]any, 0) for _, item := range paged { - var name string - if len(item.Names) > 0 { - name = item.Names[0] - } items = append(items, map[string]any{ - "id": item.ID, - "name": strings.TrimLeft(name, "/"), - "image": item.Image, - "image_id": item.ImageID, - "command": item.Command, - "created": time.Unix(item.Created, 0).Format(time.DateTime), - "ports": item.Ports, - "labels": item.Labels, - "state": item.State, - "status": item.Status, + "id": item.ID, + "name": item.Name, + "image": item.Image, + "command": item.Command, + "created_at": item.CreatedAt, + "ports": item.Ports, + "labels": item.Labels, + "state": item.State, + "status": item.Status, }) } @@ -56,8 +49,7 @@ func (s *ContainerService) List(w http.ResponseWriter, r *http.Request) { } func (s *ContainerService) Search(w http.ResponseWriter, r *http.Request) { - name := strings.Fields(r.FormValue("name")) - containers, err := s.containerRepo.ListByNames(name) + containers, err := s.containerRepo.ListByName(r.FormValue("name")) if err != nil { Error(w, http.StatusInternalServerError, "%v", err) return diff --git a/pkg/types/container.go b/pkg/types/container.go index a10af531d7..76937e35c6 100644 --- a/pkg/types/container.go +++ b/pkg/types/container.go @@ -1,6 +1,28 @@ package types +import "time" + type ContainerPort struct { + IP string `json:"ip,omitempty"` + PrivatePort uint `json:"private_port"` + PublicPort uint `json:"public_port,omitempty"` + Type string `json:"type"` +} + +type Container struct { + ID string `json:"id"` + Name string `json:"name"` + Image string `json:"image"` + ImageID string `json:"image_id"` + Command string `json:"command"` + CreatedAt time.Time `json:"created_at"` + Ports []ContainerPort `json:"ports"` + Labels []KV + State string + Status string +} + +type ContainerCreatePort struct { ContainerStart int `form:"container_start" json:"container_start"` ContainerEnd int `form:"container_end" json:"container_end"` Host string `form:"host" json:"host"`