From eb18b1080a12967fc0207d53aacb801a6e0464e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=80=97=E5=AD=90?= Date: Sun, 27 Oct 2024 22:52:42 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E4=BD=BF=E7=94=A8api=E8=8E=B7?= =?UTF-8?q?=E5=8F=96=E5=AE=B9=E5=99=A8=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/data/container.go | 152 +++++++++------------- internal/data/container_image.go | 78 ++++++----- internal/data/container_network.go | 118 ++++++----------- internal/data/container_volume.go | 103 +++++++-------- internal/service/container_image.go | 3 +- pkg/types/common.go | 6 +- pkg/types/container.go | 10 +- pkg/types/container_image.go | 12 +- pkg/types/container_network.go | 6 +- pkg/types/container_volume.go | 6 +- pkg/types/docker/container/container.go | 71 ++++++++++ pkg/types/docker/image/image.go | 80 ++++++++++++ pkg/types/docker/network/endpoint.go | 79 +++++++++++ pkg/types/docker/network/ipam.go | 23 ++++ pkg/types/docker/network/network.go | 63 +++++++++ pkg/types/docker/port.go | 19 +++ pkg/types/docker/swarm/meta.go | 23 ++++ pkg/types/docker/volume/list_response.go | 18 +++ pkg/types/docker/volume/volume.go | 74 +++++++++++ web/src/views/container/ContainerView.vue | 33 +++-- web/src/views/container/ImageView.vue | 7 +- web/src/views/container/NetworkView.vue | 9 +- web/src/views/container/VolumeView.vue | 11 +- 23 files changed, 705 insertions(+), 299 deletions(-) create mode 100644 pkg/types/docker/container/container.go create mode 100644 pkg/types/docker/image/image.go create mode 100644 pkg/types/docker/network/endpoint.go create mode 100644 pkg/types/docker/network/ipam.go create mode 100644 pkg/types/docker/network/network.go create mode 100644 pkg/types/docker/port.go create mode 100644 pkg/types/docker/swarm/meta.go create mode 100644 pkg/types/docker/volume/list_response.go create mode 100644 pkg/types/docker/volume/volume.go diff --git a/internal/data/container.go b/internal/data/container.go index b30d9a63..3a877f7f 100644 --- a/internal/data/container.go +++ b/internal/data/container.go @@ -1,79 +1,78 @@ package data import ( - "encoding/json" + "context" "fmt" - "regexp" + "net" + "net/http" "slices" "strings" "time" - "github.com/spf13/cast" + "github.com/go-resty/resty/v2" "github.com/TheTNB/panel/internal/biz" "github.com/TheTNB/panel/internal/http/request" "github.com/TheTNB/panel/pkg/shell" "github.com/TheTNB/panel/pkg/types" + "github.com/TheTNB/panel/pkg/types/docker/container" ) type containerRepo struct { - cmd string + client *resty.Client } -func NewContainerRepo(cmd ...string) biz.ContainerRepo { - if len(cmd) == 0 { - cmd = append(cmd, "docker") +func NewContainerRepo(sock ...string) biz.ContainerRepo { + if len(sock) == 0 { + sock = append(sock, "/var/run/docker.sock") } + client := resty.New() + client.SetTimeout(1 * time.Minute) + client.SetRetryCount(2) + client.SetTransport(&http.Transport{ + DialContext: func(ctx context.Context, _ string, _ string) (net.Conn, error) { + return (&net.Dialer{}).DialContext(ctx, "unix", sock[0]) + }, + }) + client.SetBaseURL("http://d/v1.40") + return &containerRepo{ - cmd: cmd[0], + client: client, } } // ListAll 列出所有容器 func (r *containerRepo) ListAll() ([]types.Container, error) { - output, err := shell.ExecfWithTimeout(10*time.Second, "%s ps -a --format json", r.cmd) + var resp []container.Container + _, err := r.client.R().SetResult(&resp).SetQueryParam("all", "true").Get("/containers/json") if err != nil { return nil, err } - lines := strings.Split(output, "\n") var containers []types.Container - for _, line := range lines { - if line == "" { - continue + for _, item := range resp { + ports := make([]types.ContainerPort, 0) + for _, port := range item.Ports { + ports = append(ports, types.ContainerPort{ + ContainerStart: uint(port.PrivatePort), + ContainerEnd: uint(port.PublicPort), + HostStart: uint(port.PublicPort), + HostEnd: uint(port.PublicPort), + Protocol: port.Type, + Host: port.IP, + }) } - - 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, fmt.Errorf("unmarshal failed: %w", err) - } - - createdAt, _ := time.Parse("2006-01-02 15:04:05 -0700 MST", item.CreatedAt) containers = append(containers, types.Container{ ID: item.ID, - Name: item.Names, + Name: item.Names[0], Image: item.Image, + ImageID: item.ImageID, Command: item.Command, - CreatedAt: createdAt, - Ports: r.parsePorts(item.Ports), - Labels: types.SliceToKV(strings.Split(item.Labels, ",")), + CreatedAt: time.Unix(item.Created, 0), State: item.State, Status: item.Status, + Ports: ports, + Labels: types.MapToKV(item.Labels), }) } @@ -97,10 +96,21 @@ func (r *containerRepo) ListByName(names string) ([]types.Container, error) { // Create 创建容器 func (r *containerRepo) Create(req *request.ContainerCreate) (string, error) { var sb strings.Builder - sb.WriteString(fmt.Sprintf("%s create --name %s --image %s", r.cmd, req.Name, req.Image)) - - for _, port := range req.Ports { - sb.WriteString(fmt.Sprintf(" -p %s:%d-%d:%d-%d/%s", port.Host, port.HostStart, port.HostEnd, port.ContainerStart, port.ContainerEnd, port.Protocol)) + sb.WriteString(fmt.Sprintf("docker run --name %s", req.Name)) + if req.PublishAllPorts { + sb.WriteString(" -P") + } else { + for _, port := range req.Ports { + sb.WriteString(" -p ") + if port.Host != "" { + sb.WriteString(fmt.Sprintf("%s:", port.Host)) + } + if port.HostStart == port.HostEnd || port.ContainerStart == port.ContainerEnd { + sb.WriteString(fmt.Sprintf("%d:%d/%s", port.HostStart, port.ContainerStart, port.Protocol)) + } else { + sb.WriteString(fmt.Sprintf("%d-%d:%d-%d/%s", port.HostStart, port.HostEnd, port.ContainerStart, port.ContainerEnd, port.Protocol)) + } + } } if req.Network != "" { sb.WriteString(fmt.Sprintf(" --network %s", req.Network)) @@ -132,9 +142,6 @@ func (r *containerRepo) Create(req *request.ContainerCreate) (string, error) { if req.OpenStdin { sb.WriteString(" -i") } - if req.PublishAllPorts { - sb.WriteString(" -P") - } if req.Tty { sb.WriteString(" -t") } @@ -148,94 +155,65 @@ func (r *containerRepo) Create(req *request.ContainerCreate) (string, error) { sb.WriteString(fmt.Sprintf(" --memory %d", req.Memory)) } - return shell.ExecfWithTimeout(10*time.Second, sb.String()) // nolint: govet + sb.WriteString(" %s") + return shell.Execf(sb.String(), req.Image) } // Remove 移除容器 func (r *containerRepo) Remove(id string) error { - _, err := shell.ExecfWithTimeout(10*time.Second, "%s rm -f %s", r.cmd, id) + _, err := shell.ExecfWithTimeout(30*time.Second, "docker rm -f %s", id) return err } // Start 启动容器 func (r *containerRepo) Start(id string) error { - _, err := shell.ExecfWithTimeout(10*time.Second, "%s start %s", r.cmd, id) + _, err := shell.ExecfWithTimeout(30*time.Second, "docker start %s", id) return err } // Stop 停止容器 func (r *containerRepo) Stop(id string) error { - _, err := shell.ExecfWithTimeout(10*time.Second, "%s stop %s", r.cmd, id) + _, err := shell.ExecfWithTimeout(30*time.Second, "docker stop %s", id) return err } // Restart 重启容器 func (r *containerRepo) Restart(id string) error { - _, err := shell.ExecfWithTimeout(10*time.Second, "%s restart %s", r.cmd, id) + _, err := shell.ExecfWithTimeout(30*time.Second, "docker restart %s", id) return err } // Pause 暂停容器 func (r *containerRepo) Pause(id string) error { - _, err := shell.ExecfWithTimeout(10*time.Second, "%s pause %s", r.cmd, id) + _, err := shell.ExecfWithTimeout(30*time.Second, "docker pause %s", id) return err } // Unpause 恢复容器 func (r *containerRepo) Unpause(id string) error { - _, err := shell.ExecfWithTimeout(10*time.Second, "%s unpause %s", r.cmd, id) + _, err := shell.ExecfWithTimeout(30*time.Second, "docker unpause %s", id) return err } // Kill 杀死容器 func (r *containerRepo) Kill(id string) error { - _, err := shell.ExecfWithTimeout(10*time.Second, "%s kill %s", r.cmd, id) + _, err := shell.ExecfWithTimeout(30*time.Second, "docker kill %s", id) return err } // Rename 重命名容器 func (r *containerRepo) Rename(id string, newName string) error { - _, err := shell.ExecfWithTimeout(10*time.Second, "%s rename %s %s", r.cmd, id, newName) + _, err := shell.ExecfWithTimeout(30*time.Second, "docker rename %s %s", id, newName) return err } // Logs 查看容器日志 func (r *containerRepo) Logs(id string) (string, error) { - return shell.ExecfWithTimeout(10*time.Second, "%s logs %s", r.cmd, id) + return shell.ExecfWithTimeout(30*time.Second, "docker logs %s", id) } // Prune 清理未使用的容器 func (r *containerRepo) Prune() error { - _, err := shell.ExecfWithTimeout(10*time.Second, "%s container prune -f", r.cmd) + _, err := shell.ExecfWithTimeout(30*time.Second, "docker container prune -f") return err } - -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{ - Host: host, - HostStart: cast.ToUint(public), - HostEnd: cast.ToUint(public), - ContainerStart: cast.ToUint(private), - ContainerEnd: cast.ToUint(private), - Protocol: protocol, - }) - } - - return portList -} diff --git a/internal/data/container_image.go b/internal/data/container_image.go index 4528d058..8f6de5a8 100644 --- a/internal/data/container_image.go +++ b/internal/data/container_image.go @@ -1,68 +1,64 @@ package data import ( - "encoding/json" + "context" "fmt" + "net" + "net/http" "strings" "time" - "github.com/spf13/cast" + "github.com/go-resty/resty/v2" "github.com/TheTNB/panel/internal/biz" "github.com/TheTNB/panel/internal/http/request" "github.com/TheTNB/panel/pkg/shell" + "github.com/TheTNB/panel/pkg/str" "github.com/TheTNB/panel/pkg/types" + "github.com/TheTNB/panel/pkg/types/docker/image" ) type containerImageRepo struct { - cmd string + client *resty.Client } -func NewContainerImageRepo(cmd ...string) biz.ContainerImageRepo { - if len(cmd) == 0 { - cmd = append(cmd, "docker") +func NewContainerImageRepo(sock ...string) biz.ContainerImageRepo { + if len(sock) == 0 { + sock = append(sock, "/var/run/docker.sock") } + client := resty.New() + client.SetTimeout(1 * time.Minute) + client.SetRetryCount(2) + client.SetTransport(&http.Transport{ + DialContext: func(ctx context.Context, _ string, _ string) (net.Conn, error) { + return (&net.Dialer{}).DialContext(ctx, "unix", sock[0]) + }, + }) + client.SetBaseURL("http://d/v1.40") + return &containerImageRepo{ - cmd: cmd[0], + client: client, } } // List 列出镜像 func (r *containerImageRepo) List() ([]types.ContainerImage, error) { - output, err := shell.ExecfWithTimeout(10*time.Second, "%s images -a --format json", r.cmd) + var resp []image.Image + _, err := r.client.R().SetResult(&resp).SetQueryParam("all", "true").Get("/images/json") if err != nil { return nil, err } - lines := strings.Split(output, "\n") var images []types.ContainerImage - for _, line := range lines { - if line == "" { - continue - } - - var item struct { - ID string `json:"ID"` - Containers string `json:"Containers"` - Repository string `json:"Repository"` - Tag string `json:"Tag"` - Digest string `json:"Digest"` - CreatedAt string `json:"CreatedAt"` - Size string `json:"Size"` - SharedSize string `json:"SharedSize"` - VirtualSize string `json:"VirtualSize"` - } - if err = json.Unmarshal([]byte(line), &item); err != nil { - return nil, fmt.Errorf("unmarshal failed: %w", err) - } - - createdAt, _ := time.Parse("2006-01-02 15:04:05 -0700 MST", item.CreatedAt) + for _, item := range resp { images = append(images, types.ContainerImage{ - ID: item.ID, - Containers: cast.ToInt64(item.Containers), - Tag: item.Tag, - CreatedAt: createdAt, - Size: item.Size, + ID: item.ID, + Containers: item.Containers, + RepoTags: item.RepoTags, + RepoDigests: item.RepoDigests, + Size: str.FormatBytes(float64(item.Size)), + Labels: types.MapToKV(item.Labels), + CreatedAt: time.Unix(item.Created, 0), }) } @@ -74,16 +70,16 @@ func (r *containerImageRepo) Pull(req *request.ContainerImagePull) error { var sb strings.Builder if req.Auth { - sb.WriteString(fmt.Sprintf("%s login -u %s -p %s", r.cmd, req.Username, req.Password)) - if _, err := shell.ExecfWithTimeout(1*time.Minute, sb.String()); err != nil { + sb.WriteString(fmt.Sprintf("docker login -u %s -p %s", req.Username, req.Password)) + if _, err := shell.ExecfWithTimeout(1*time.Minute, sb.String()); err != nil { // nolint: govet return fmt.Errorf("login failed: %w", err) } sb.Reset() } - sb.WriteString(fmt.Sprintf("%s pull %s", r.cmd, req.Name)) + sb.WriteString(fmt.Sprintf("docker pull %s", req.Name)) - if _, err := shell.ExecfWithTimeout(20*time.Minute, sb.String()); err != nil { // nolint: govet + if _, err := shell.Execf(sb.String()); err != nil { // nolint: govet return fmt.Errorf("pull failed: %w", err) } @@ -92,12 +88,12 @@ func (r *containerImageRepo) Pull(req *request.ContainerImagePull) error { // Remove 删除镜像 func (r *containerImageRepo) Remove(id string) error { - _, err := shell.ExecfWithTimeout(30*time.Second, "%s rmi %s", r.cmd, id) + _, err := shell.ExecfWithTimeout(30*time.Second, "docker rmi %s", id) return err } // Prune 清理未使用的镜像 func (r *containerImageRepo) Prune() error { - _, err := shell.ExecfWithTimeout(30*time.Second, "%s image prune -f", r.cmd) + _, err := shell.ExecfWithTimeout(30*time.Second, "docker image prune -f") return err } diff --git a/internal/data/container_network.go b/internal/data/container_network.go index f62681d9..c06757a5 100644 --- a/internal/data/container_network.go +++ b/internal/data/container_network.go @@ -1,97 +1,57 @@ package data import ( - "encoding/json" + "context" "fmt" + "net" + "net/http" "strings" "time" - "github.com/spf13/cast" + "github.com/go-resty/resty/v2" "github.com/TheTNB/panel/internal/biz" "github.com/TheTNB/panel/internal/http/request" "github.com/TheTNB/panel/pkg/shell" "github.com/TheTNB/panel/pkg/types" + "github.com/TheTNB/panel/pkg/types/docker/network" ) type containerNetworkRepo struct { - cmd string + client *resty.Client } -func NewContainerNetworkRepo(cmd ...string) biz.ContainerNetworkRepo { - if len(cmd) == 0 { - cmd = append(cmd, "docker") +func NewContainerNetworkRepo(sock ...string) biz.ContainerNetworkRepo { + if len(sock) == 0 { + sock = append(sock, "/var/run/docker.sock") } + client := resty.New() + client.SetTimeout(1 * time.Minute) + client.SetRetryCount(2) + client.SetTransport(&http.Transport{ + DialContext: func(ctx context.Context, _ string, _ string) (net.Conn, error) { + return (&net.Dialer{}).DialContext(ctx, "unix", sock[0]) + }, + }) + client.SetBaseURL("http://d/v1.40") + return &containerNetworkRepo{ - cmd: cmd[0], + client: client, } } // List 列出网络 func (r *containerNetworkRepo) List() ([]types.ContainerNetwork, error) { - output, err := shell.ExecfWithTimeout(10*time.Second, "%s network ls --format json", r.cmd) + var resp []network.Network + _, err := r.client.R().SetResult(&resp).Get("/networks") if err != nil { return nil, err } - lines := strings.Split(output, "\n") var networks []types.ContainerNetwork - for _, line := range lines { - if line == "" { - continue - } - - var item struct { - CreatedAt string `json:"CreatedAt"` - Driver string `json:"Driver"` - ID string `json:"ID"` - IPv6 string `json:"IPv6"` - Internal string `json:"Internal"` - Labels string `json:"Labels"` - Name string `json:"Name"` - Scope string `json:"Scope"` - } - if err = json.Unmarshal([]byte(line), &item); err != nil { - return nil, fmt.Errorf("unmarshal failed: %w", err) - } - - output, err = shell.ExecfWithTimeout(10*time.Second, "%s network inspect %s", r.cmd, item.ID) - if err != nil { - return nil, fmt.Errorf("inspect failed: %w", err) - } - var inspect []struct { - Name string `json:"Name"` - Id string `json:"Id"` - Created time.Time `json:"Created"` - Scope string `json:"Scope"` - Driver string `json:"Driver"` - EnableIPv6 bool `json:"EnableIPv6"` - IPAM struct { - Driver string `json:"Driver"` - Options map[string]string `json:"Options"` - Config []struct { - Subnet string `json:"Subnet"` - IPRange string `json:"IPRange"` - Gateway string `json:"Gateway"` - AuxAddress map[string]string `json:"AuxiliaryAddresses"` - } `json:"Config"` - } `json:"IPAM"` - Internal bool `json:"Internal"` - Attachable bool `json:"Attachable"` - Ingress bool `json:"Ingress"` - ConfigOnly bool `json:"ConfigOnly"` - Options map[string]string `json:"Options"` - Labels map[string]string `json:"Labels"` - } - if err = json.Unmarshal([]byte(output), &inspect); err != nil { - return nil, fmt.Errorf("unmarshal inspect failed: %w", err) - } - if len(inspect) == 0 { - return nil, fmt.Errorf("inspect empty") - } - - var ipamConfigs []types.ContainerNetworkIPAMConfig - for _, ipam := range inspect[0].IPAM.Config { + for _, item := range resp { + ipamConfigs := make([]types.ContainerNetworkIPAMConfig, 0) + for _, ipam := range item.IPAM.Config { ipamConfigs = append(ipamConfigs, types.ContainerNetworkIPAMConfig{ Subnet: ipam.Subnet, IPRange: ipam.IPRange, @@ -99,25 +59,23 @@ func (r *containerNetworkRepo) List() ([]types.ContainerNetwork, error) { AuxAddress: ipam.AuxAddress, }) } - - createdAt, _ := time.Parse("2006-01-02 15:04:05 -0700 MST", item.CreatedAt) networks = append(networks, types.ContainerNetwork{ ID: item.ID, Name: item.Name, Driver: item.Driver, - IPv6: cast.ToBool(item.IPv6), - Internal: cast.ToBool(item.Internal), - Attachable: cast.ToBool(inspect[0].Attachable), - Ingress: cast.ToBool(inspect[0].Ingress), + IPv6: item.EnableIPv6, + Internal: item.Internal, + Attachable: item.Attachable, + Ingress: item.Ingress, Scope: item.Scope, - CreatedAt: createdAt, + CreatedAt: item.Created, IPAM: types.ContainerNetworkIPAM{ - Driver: inspect[0].IPAM.Driver, - Options: types.MapToKV(inspect[0].IPAM.Options), + Driver: item.IPAM.Driver, + Options: types.MapToKV(item.IPAM.Options), Config: ipamConfigs, }, - Options: types.MapToKV(inspect[0].Options), - Labels: types.SliceToKV(strings.Split(item.Labels, ",")), + Options: types.MapToKV(item.Options), + Labels: types.MapToKV(item.Labels), }) } @@ -128,7 +86,7 @@ func (r *containerNetworkRepo) List() ([]types.ContainerNetwork, error) { func (r *containerNetworkRepo) Create(req *request.ContainerNetworkCreate) (string, error) { var sb strings.Builder - sb.WriteString(fmt.Sprintf("%s network create --driver %s", r.cmd, req.Driver)) + sb.WriteString(fmt.Sprintf("docker network create --driver %s", req.Driver)) sb.WriteString(fmt.Sprintf(" %s", req.Name)) if req.Ipv4.Enabled { @@ -152,17 +110,17 @@ func (r *containerNetworkRepo) Create(req *request.ContainerNetworkCreate) (stri sb.WriteString(fmt.Sprintf(" --opt %s=%s", option.Key, option.Value)) } - return shell.ExecfWithTimeout(10*time.Second, "%s", sb.String()) // nolint: govet + return shell.ExecfWithTimeout(30*time.Second, sb.String()) // nolint: govet } // Remove 删除网络 func (r *containerNetworkRepo) Remove(id string) error { - _, err := shell.ExecfWithTimeout(10*time.Second, "%s network rm -f %s", r.cmd, id) + _, err := shell.ExecfWithTimeout(30*time.Second, "docker network rm -f %s", id) return err } // Prune 清理未使用的网络 func (r *containerNetworkRepo) Prune() error { - _, err := shell.ExecfWithTimeout(10*time.Second, "%s network prune -f", r.cmd) + _, err := shell.ExecfWithTimeout(30*time.Second, "docker network prune -f") return err } diff --git a/internal/data/container_volume.go b/internal/data/container_volume.go index 75989a15..bee8bcbe 100644 --- a/internal/data/container_volume.go +++ b/internal/data/container_volume.go @@ -1,88 +1,66 @@ package data import ( - "encoding/json" + "context" "fmt" + "net" + "net/http" "strings" "time" + "github.com/go-resty/resty/v2" + "github.com/TheTNB/panel/internal/biz" "github.com/TheTNB/panel/internal/http/request" "github.com/TheTNB/panel/pkg/shell" + "github.com/TheTNB/panel/pkg/str" "github.com/TheTNB/panel/pkg/types" + "github.com/TheTNB/panel/pkg/types/docker/volume" ) type containerVolumeRepo struct { - cmd string + client *resty.Client } -func NewContainerVolumeRepo(cmd ...string) biz.ContainerVolumeRepo { - if len(cmd) == 0 { - cmd = append(cmd, "docker") +func NewContainerVolumeRepo(sock ...string) biz.ContainerVolumeRepo { + if len(sock) == 0 { + sock = append(sock, "/var/run/docker.sock") } + client := resty.New() + client.SetTimeout(1 * time.Minute) + client.SetRetryCount(2) + client.SetTransport(&http.Transport{ + DialContext: func(ctx context.Context, _ string, _ string) (net.Conn, error) { + return (&net.Dialer{}).DialContext(ctx, "unix", sock[0]) + }, + }) + client.SetBaseURL("http://d/v1.40") + return &containerVolumeRepo{ - cmd: cmd[0], + client: client, } } // List 列出存储卷 func (r *containerVolumeRepo) List() ([]types.ContainerVolume, error) { - output, err := shell.ExecfWithTimeout(10*time.Second, "%s volume ls --format json", r.cmd) + var resp volume.ListResponse + _, err := r.client.R().SetResult(&resp).Get("/volumes") if err != nil { return nil, err } - lines := strings.Split(output, "\n") var volumes []types.ContainerVolume - for _, line := range lines { - if line == "" { - continue - } - - var item struct { - Availability string `json:"Availability"` - Driver string `json:"Driver"` - Group string `json:"Group"` - Labels string `json:"Labels"` - Links string `json:"Links"` - Mountpoint string `json:"Mountpoint"` - Name string `json:"Name"` - Scope string `json:"Scope"` - Size string `json:"Size"` - Status string `json:"Status"` - } - if err = json.Unmarshal([]byte(line), &item); err != nil { - return nil, fmt.Errorf("unmarshal failed: %w", err) - } - - output, err = shell.ExecfWithTimeout(10*time.Second, "%s volume inspect %s", r.cmd, item.Name) - if err != nil { - return nil, fmt.Errorf("inspect failed: %w", err) - } - var inspect []struct { - CreatedAt time.Time `json:"CreatedAt"` - Driver string `json:"Driver"` - Labels map[string]string `json:"Labels"` - Mountpoint string `json:"Mountpoint"` - Name string `json:"Name"` - Options map[string]string `json:"Options"` - Scope string `json:"Scope"` - } - if err = json.Unmarshal([]byte(output), &inspect); err != nil { - return nil, fmt.Errorf("unmarshal inspect failed: %w", err) - } - if len(inspect) == 0 { - return nil, fmt.Errorf("inspect empty") - } - + for _, item := range resp.Volumes { volumes = append(volumes, types.ContainerVolume{ Name: item.Name, Driver: item.Driver, Scope: item.Scope, MountPoint: item.Mountpoint, - CreatedAt: inspect[0].CreatedAt, - Options: types.MapToKV(inspect[0].Options), - Labels: types.SliceToKV(strings.Split(item.Labels, ",")), + CreatedAt: item.CreatedAt, + Labels: types.MapToKV(item.Labels), + Options: types.MapToKV(item.Options), + RefCount: item.UsageData.RefCount, + Size: str.FormatBytes(float64(item.UsageData.Size)), }) } @@ -91,17 +69,32 @@ func (r *containerVolumeRepo) List() ([]types.ContainerVolume, error) { // Create 创建存储卷 func (r *containerVolumeRepo) Create(req *request.ContainerVolumeCreate) (string, error) { - return "", nil + var sb strings.Builder + sb.WriteString("docker volume create") + sb.WriteString(fmt.Sprintf(" %s", req.Name)) + + if req.Driver != "" { + sb.WriteString(fmt.Sprintf(" --driver %s", req.Driver)) + } + for _, label := range req.Labels { + sb.WriteString(fmt.Sprintf(" --label %s=%s", label.Key, label.Value)) + } + + for _, option := range req.Options { + sb.WriteString(fmt.Sprintf(" --opt %s=%s", option.Key, option.Value)) + } + + return shell.ExecfWithTimeout(30*time.Second, sb.String()) // nolint: govet } // Remove 删除存储卷 func (r *containerVolumeRepo) Remove(id string) error { - _, err := shell.ExecfWithTimeout(10*time.Second, "%s volume rm -f %s", r.cmd, id) + _, err := shell.ExecfWithTimeout(30*time.Second, "docker volume rm -f %s", id) return err } // Prune 清理未使用的存储卷 func (r *containerVolumeRepo) Prune() error { - _, err := shell.ExecfWithTimeout(10*time.Second, "%s volume prune -f", r.cmd) + _, err := shell.ExecfWithTimeout(30*time.Second, "docker volume prune -f") return err } diff --git a/internal/service/container_image.go b/internal/service/container_image.go index f3db54e8..bf8f2c87 100644 --- a/internal/service/container_image.go +++ b/internal/service/container_image.go @@ -1,9 +1,10 @@ package service import ( - "github.com/go-rat/chix" "net/http" + "github.com/go-rat/chix" + "github.com/TheTNB/panel/internal/biz" "github.com/TheTNB/panel/internal/data" "github.com/TheTNB/panel/internal/http/request" diff --git a/pkg/types/common.go b/pkg/types/common.go index d5aa44e6..dc6d8f6f 100644 --- a/pkg/types/common.go +++ b/pkg/types/common.go @@ -34,7 +34,7 @@ func KVToMap(kvs []KV) map[string]string { // MapToKV 将 map 转换为 key-value 切片 func MapToKV(m map[string]string) []KV { - var kvs []KV + kvs := make([]KV, 0) for k, v := range m { kvs = append(kvs, KV{Key: k, Value: v}) } @@ -44,7 +44,7 @@ func MapToKV(m map[string]string) []KV { // KVToSlice 将 key-value 切片转换为 key=value 切片 func KVToSlice(kvs []KV) []string { - var s []string + s := make([]string, 0) for _, item := range kvs { s = append(s, item.Key+"="+item.Value) } @@ -54,7 +54,7 @@ func KVToSlice(kvs []KV) []string { // SliceToKV 将 key=value 切片转换为 key-value 切片 func SliceToKV(s []string) []KV { - var kvs []KV + kvs := make([]KV, 0) for _, item := range s { kv := strings.SplitN(item, "=", 2) if len(kv) == 2 { diff --git a/pkg/types/container.go b/pkg/types/container.go index 8afc11af..55093866 100644 --- a/pkg/types/container.go +++ b/pkg/types/container.go @@ -1,6 +1,8 @@ package types -import "time" +import ( + "time" +) type Container struct { ID string `json:"id"` @@ -8,11 +10,11 @@ type Container struct { Image string `json:"image"` ImageID string `json:"image_id"` Command string `json:"command"` + State string `json:"state"` + Status string `json:"status"` CreatedAt time.Time `json:"created_at"` Ports []ContainerPort `json:"ports"` - Labels []KV - State string - Status string + Labels []KV `json:"labels"` } type ContainerPort struct { diff --git a/pkg/types/container_image.go b/pkg/types/container_image.go index 1ec86c82..139e9e0f 100644 --- a/pkg/types/container_image.go +++ b/pkg/types/container_image.go @@ -5,9 +5,11 @@ import ( ) type ContainerImage struct { - ID string `json:"id"` - Containers int64 `json:"containers"` - Tag string `json:"tag"` - Size string `json:"size"` - CreatedAt time.Time `json:"created_at"` + ID string `json:"id"` + Containers int64 `json:"containers"` + RepoTags []string `json:"repo_tags"` + RepoDigests []string `json:"repo_digests"` + Size string `json:"size"` + Labels []KV `json:"labels"` + CreatedAt time.Time `json:"created_at"` } diff --git a/pkg/types/container_network.go b/pkg/types/container_network.go index e927ef8a..bcca9811 100644 --- a/pkg/types/container_network.go +++ b/pkg/types/container_network.go @@ -19,9 +19,9 @@ type ContainerNetwork struct { // ContainerNetworkIPAM represents IP Address Management type ContainerNetworkIPAM struct { - Driver string - Options []KV - Config []ContainerNetworkIPAMConfig + Driver string `json:"driver"` + Options []KV `json:"options"` + Config []ContainerNetworkIPAMConfig `json:"config"` } // ContainerNetworkIPAMConfig represents IPAM configurations diff --git a/pkg/types/container_volume.go b/pkg/types/container_volume.go index 738b6ecd..e6d7efbd 100644 --- a/pkg/types/container_volume.go +++ b/pkg/types/container_volume.go @@ -1,6 +1,8 @@ package types -import "time" +import ( + "time" +) type ContainerVolume struct { Name string `json:"name"` @@ -10,4 +12,6 @@ type ContainerVolume struct { CreatedAt time.Time `json:"created_at"` Labels []KV `json:"labels"` Options []KV `json:"options"` + RefCount int64 `json:"ref_count"` + Size string `json:"size"` } diff --git a/pkg/types/docker/container/container.go b/pkg/types/docker/container/container.go new file mode 100644 index 00000000..486e3c2e --- /dev/null +++ b/pkg/types/docker/container/container.go @@ -0,0 +1,71 @@ +package container + +import ( + "github.com/TheTNB/panel/pkg/types/docker" +) + +// Container contains response of Engine API: +// GET "/containers/json" +type Container struct { + ID string `json:"Id"` + Names []string + Image string + ImageID string + Command string + Created int64 + Ports []docker.Port + SizeRw int64 `json:",omitempty"` + SizeRootFs int64 `json:",omitempty"` + Labels map[string]string + State string + Status string + HostConfig struct { + NetworkMode string `json:",omitempty"` + Annotations map[string]string `json:",omitempty"` + } + Mounts []MountPoint +} + +// MountPoint represents a mount point configuration inside the container. +// This is used for reporting the mountpoints in use by a container. +type MountPoint struct { + // Type is the type of mount, see `Type` definitions in + // github.com/docker/docker/api/types/mount.Type + Type string `json:",omitempty"` + + // Name is the name reference to the underlying data defined by `Source` + // e.g., the volume name. + Name string `json:",omitempty"` + + // Source is the source location of the mount. + // + // For volumes, this contains the storage location of the volume (within + // `/var/lib/docker/volumes/`). For bind-mounts, and `npipe`, this contains + // the source (host) part of the bind-mount. For `tmpfs` mount points, this + // field is empty. + Source string + + // Destination is the path relative to the container root (`/`) where the + // Source is mounted inside the container. + Destination string + + // Driver is the volume driver used to create the volume (if it is a volume). + Driver string `json:",omitempty"` + + // Mode is a comma separated list of options supplied by the user when + // creating the bind/volume mount. + // + // The default is platform-specific (`"z"` on Linux, empty on Windows). + Mode string + + // RW indicates whether the mount is mounted writable (read-write). + RW bool + + // Propagation describes how mounts are propagated from the host into the + // mount point, and vice-versa. Refer to the Linux kernel documentation + // for details: + // https://www.kernel.org/doc/Documentation/filesystems/sharedsubtree.txt + // + // This field is not used on Windows. + Propagation string +} diff --git a/pkg/types/docker/image/image.go b/pkg/types/docker/image/image.go new file mode 100644 index 00000000..6a9dbc34 --- /dev/null +++ b/pkg/types/docker/image/image.go @@ -0,0 +1,80 @@ +package image + +// Image summary +type Image struct { + + // Number of containers using this image. Includes both stopped and running + // containers. + // + // This size is not calculated by default, and depends on which API endpoint + // is used. `-1` indicates that the value has not been set / calculated. + // + // Required: true + Containers int64 `json:"Containers"` + + // Date and time at which the image was created as a Unix timestamp + // (number of seconds sinds EPOCH). + // + // Required: true + Created int64 `json:"Created"` + + // ID is the content-addressable ID of an image. + // + // This identifier is a content-addressable digest calculated from the + // image's configuration (which includes the digests of layers used by + // the image). + // + // Note that this digest differs from the `RepoDigests` below, which + // holds digests of image manifests that reference the image. + // + // Required: true + ID string `json:"Id"` + + // User-defined key/value metadata. + // Required: true + Labels map[string]string `json:"Labels"` + + // ID of the parent image. + // + // Depending on how the image was created, this field may be empty and + // is only set for images that were built/created locally. This field + // is empty if the image was pulled from an image registry. + // + // Required: true + ParentID string `json:"ParentId"` + + // List of content-addressable digests of locally available image manifests + // that the image is referenced from. Multiple manifests can refer to the + // same image. + // + // These digests are usually only available if the image was either pulled + // from a registry, or if the image was pushed to a registry, which is when + // the manifest is generated and its digest calculated. + // + // Required: true + RepoDigests []string `json:"RepoDigests"` + + // List of image names/tags in the local image cache that reference this + // image. + // + // Multiple image tags can refer to the same image, and this list may be + // empty if no tags reference the image, in which case the image is + // "untagged", in which case it can still be referenced by its ID. + // + // Required: true + RepoTags []string `json:"RepoTags"` + + // Total size of image layers that are shared between this image and other + // images. + // + // This size is not calculated by default. `-1` indicates that the value + // has not been set / calculated. + // + // Required: true + SharedSize int64 `json:"SharedSize"` + + // Total size of the image including all layers it is composed of. + // + // Required: true + Size int64 `json:"Size"` +} diff --git a/pkg/types/docker/network/endpoint.go b/pkg/types/docker/network/endpoint.go new file mode 100644 index 00000000..242b223a --- /dev/null +++ b/pkg/types/docker/network/endpoint.go @@ -0,0 +1,79 @@ +package network + +import ( + "net" +) + +// EndpointSettings stores the network endpoint details +type EndpointSettings struct { + // Configurations + IPAMConfig *EndpointIPAMConfig + Links []string + Aliases []string // Aliases holds the list of extra, user-specified DNS names for this endpoint. + // MacAddress may be used to specify a MAC address when the container is created. + // Once the container is running, it becomes operational data (it may contain a + // generated address). + MacAddress string + DriverOpts map[string]string + // Operational data + NetworkID string + EndpointID string + Gateway string + IPAddress string + IPPrefixLen int + IPv6Gateway string + GlobalIPv6Address string + GlobalIPv6PrefixLen int + // DNSNames holds all the (non fully qualified) DNS names associated to this endpoint. First entry is used to + // generate PTR records. + DNSNames []string +} + +// Copy makes a deep copy of `EndpointSettings` +func (es *EndpointSettings) Copy() *EndpointSettings { + epCopy := *es + if es.IPAMConfig != nil { + epCopy.IPAMConfig = es.IPAMConfig.Copy() + } + + if es.Links != nil { + links := make([]string, 0, len(es.Links)) + epCopy.Links = append(links, es.Links...) + } + + if es.Aliases != nil { + aliases := make([]string, 0, len(es.Aliases)) + epCopy.Aliases = append(aliases, es.Aliases...) + } + + if len(es.DNSNames) > 0 { + epCopy.DNSNames = make([]string, len(es.DNSNames)) + copy(epCopy.DNSNames, es.DNSNames) + } + + return &epCopy +} + +// EndpointIPAMConfig represents IPAM configurations for the endpoint +type EndpointIPAMConfig struct { + IPv4Address string `json:",omitempty"` + IPv6Address string `json:",omitempty"` + LinkLocalIPs []string `json:",omitempty"` +} + +// Copy makes a copy of the endpoint ipam config +func (cfg *EndpointIPAMConfig) Copy() *EndpointIPAMConfig { + cfgCopy := *cfg + cfgCopy.LinkLocalIPs = make([]string, 0, len(cfg.LinkLocalIPs)) + cfgCopy.LinkLocalIPs = append(cfgCopy.LinkLocalIPs, cfg.LinkLocalIPs...) + return &cfgCopy +} + +// NetworkSubnet describes a user-defined subnet for a specific network. It's only used to validate if an +// EndpointIPAMConfig is valid for a specific network. +type NetworkSubnet interface { + // Contains checks whether the NetworkSubnet contains [addr]. + Contains(addr net.IP) bool + // IsStatic checks whether the subnet was statically allocated (ie. user-defined). + IsStatic() bool +} diff --git a/pkg/types/docker/network/ipam.go b/pkg/types/docker/network/ipam.go new file mode 100644 index 00000000..b52f14b5 --- /dev/null +++ b/pkg/types/docker/network/ipam.go @@ -0,0 +1,23 @@ +package network + +// IPAM represents IP Address Management +type IPAM struct { + Driver string + Options map[string]string // Per network IPAM driver options + Config []IPAMConfig +} + +// IPAMConfig represents IPAM configurations +type IPAMConfig struct { + Subnet string `json:",omitempty"` + IPRange string `json:",omitempty"` + Gateway string `json:",omitempty"` + AuxAddress map[string]string `json:"AuxiliaryAddresses,omitempty"` +} + +type ipFamily string + +const ( + ip4 ipFamily = "IPv4" + ip6 ipFamily = "IPv6" +) diff --git a/pkg/types/docker/network/network.go b/pkg/types/docker/network/network.go new file mode 100644 index 00000000..cbfc4708 --- /dev/null +++ b/pkg/types/docker/network/network.go @@ -0,0 +1,63 @@ +package network // import "github.com/docker/docker/api/types/network" + +import ( + "time" +) + +// Network is the body of the "get network" http response message. +type Network struct { + Name string // Name is the name of the network + ID string `json:"Id"` // ID uniquely identifies a network on a single machine + Created time.Time // Created is the time the network created + Scope string // Scope describes the level at which the network exists (e.g. `swarm` for cluster-wide or `local` for machine level) + Driver string // Driver is the Driver name used to create the network (e.g. `bridge`, `overlay`) + EnableIPv6 bool // EnableIPv6 represents whether to enable IPv6 + IPAM IPAM // IPAM is the network's IP Address Management + Internal bool // Internal represents if the network is used internal only + Attachable bool // Attachable represents if the global scope is manually attachable by regular containers from workers in swarm mode. + Ingress bool // Ingress indicates the network is providing the routing-mesh for the swarm cluster. + ConfigFrom ConfigReference // ConfigFrom specifies the source which will provide the configuration for this network. + ConfigOnly bool // ConfigOnly networks are place-holder networks for network configurations to be used by other networks. ConfigOnly networks cannot be used directly to run containers or services. + Containers map[string]EndpointResource // Containers contains endpoints belonging to the network + Options map[string]string // Options holds the network specific options to use for when creating the network + Labels map[string]string // Labels holds metadata specific to the network being created + Peers []PeerInfo `json:",omitempty"` // List of peer nodes for an overlay network + Services map[string]ServiceInfo `json:",omitempty"` +} + +// PeerInfo represents one peer of an overlay network +type PeerInfo struct { + Name string + IP string +} + +// Task carries the information about one backend task +type Task struct { + Name string + EndpointID string + EndpointIP string + Info map[string]string +} + +// ServiceInfo represents service parameters with the list of service's tasks +type ServiceInfo struct { + VIP string + Ports []string + LocalLBIndex int + Tasks []Task +} + +// EndpointResource contains network resources allocated and used for a +// container in a network. +type EndpointResource struct { + Name string + EndpointID string + MacAddress string + IPv4Address string + IPv6Address string +} + +// ConfigReference specifies the source which provides a network's configuration +type ConfigReference struct { + Network string +} diff --git a/pkg/types/docker/port.go b/pkg/types/docker/port.go new file mode 100644 index 00000000..c2c2ec06 --- /dev/null +++ b/pkg/types/docker/port.go @@ -0,0 +1,19 @@ +package docker + +// Port An open port on a container +type Port struct { + + // Host IP address that the container's port is mapped to + IP string `json:"IP,omitempty"` + + // Port on the container + // Required: true + PrivatePort uint16 `json:"PrivatePort"` + + // Port exposed on the host + PublicPort uint16 `json:"PublicPort,omitempty"` + + // type + // Required: true + Type string `json:"Type"` +} diff --git a/pkg/types/docker/swarm/meta.go b/pkg/types/docker/swarm/meta.go new file mode 100644 index 00000000..2b365465 --- /dev/null +++ b/pkg/types/docker/swarm/meta.go @@ -0,0 +1,23 @@ +package swarm + +import ( + "strconv" + "time" +) + +// Version represents the internal object version. +type Version struct { + Index uint64 `json:",omitempty"` +} + +// String implements fmt.Stringer interface. +func (v Version) String() string { + return strconv.FormatUint(v.Index, 10) +} + +// Meta is a base object inherited by most of the other once. +type Meta struct { + Version Version `json:",omitempty"` + CreatedAt time.Time `json:",omitempty"` + UpdatedAt time.Time `json:",omitempty"` +} diff --git a/pkg/types/docker/volume/list_response.go b/pkg/types/docker/volume/list_response.go new file mode 100644 index 00000000..ca5192a2 --- /dev/null +++ b/pkg/types/docker/volume/list_response.go @@ -0,0 +1,18 @@ +package volume + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +// ListResponse VolumeListResponse +// +// Volume list response +// swagger:model ListResponse +type ListResponse struct { + + // List of volumes + Volumes []*Volume `json:"Volumes"` + + // Warnings that occurred when fetching the list of volumes. + // + Warnings []string `json:"Warnings"` +} diff --git a/pkg/types/docker/volume/volume.go b/pkg/types/docker/volume/volume.go new file mode 100644 index 00000000..13e2470e --- /dev/null +++ b/pkg/types/docker/volume/volume.go @@ -0,0 +1,74 @@ +package volume + +import "time" + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +// Volume volume +// swagger:model Volume +type Volume struct { + + // Date/Time the volume was created. + CreatedAt time.Time `json:"CreatedAt,omitempty"` + + // Name of the volume driver used by the volume. + // Required: true + Driver string `json:"Driver"` + + // User-defined key/value metadata. + // Required: true + Labels map[string]string `json:"Labels"` + + // Mount path of the volume on the host. + // Required: true + Mountpoint string `json:"Mountpoint"` + + // Name of the volume. + // Required: true + Name string `json:"Name"` + + // The driver specific options used when creating the volume. + // + // Required: true + Options map[string]string `json:"Options"` + + // The level at which the volume exists. Either `global` for cluster-wide, + // or `local` for machine level. + // + // Required: true + Scope string `json:"Scope"` + + // Low-level details about the volume, provided by the volume driver. + // Details are returned as a map with key/value pairs: + // `{"key":"value","key2":"value2"}`. + // + // The `Status` field is optional, and is omitted if the volume driver + // does not support this feature. + // + Status map[string]interface{} `json:"Status,omitempty"` + + // usage data + UsageData UsageData `json:"UsageData,omitempty"` +} + +// UsageData Usage details about the volume. This information is used by the +// `GET /system/df` endpoint, and omitted in other endpoints. +// +// swagger:model UsageData +type UsageData struct { + + // The number of containers referencing this volume. This field + // is set to `-1` if the reference-count is not available. + // + // Required: true + RefCount int64 `json:"RefCount"` + + // Amount of disk space used by the volume (in bytes). This information + // is only available for volumes created with the `"local"` volume + // driver. For volumes created with other volume drivers, this field + // is set to `-1` ("not available") + // + // Required: true + Size int64 `json:"Size"` +} diff --git a/web/src/views/container/ContainerView.vue b/web/src/views/container/ContainerView.vue index e54ddaf3..f043c0ea 100644 --- a/web/src/views/container/ContainerView.vue +++ b/web/src/views/container/ContainerView.vue @@ -1,6 +1,6 @@