2
0
mirror of https://github.com/acepanel/panel.git synced 2026-02-04 05:31:44 +08:00

refactor: 使用api获取容器信息

This commit is contained in:
耗子
2024-10-27 22:52:42 +08:00
parent ffe7b95b9c
commit eb18b1080a
23 changed files with 705 additions and 299 deletions

View File

@@ -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<host>[\d.:]+)?:(?P<public>\d+)->(?P<private>\d+)/(?P<protocol>\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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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"

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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"`
}

View File

@@ -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

View File

@@ -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"`
}

View File

@@ -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<foo>` 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
}

View File

@@ -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"`
}

View File

@@ -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
}

View File

@@ -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"
)

View File

@@ -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
}

19
pkg/types/docker/port.go Normal file
View File

@@ -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"`
}

View File

@@ -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"`
}

View File

@@ -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"`
}

View File

@@ -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"`
}

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
import Editor from '@guolao/vue-monaco-editor'
import { NButton, NDataTable, NDropdown, NInput, NSwitch } from 'naive-ui'
import { NButton, NDataTable, NDropdown, NFlex, NInput, NSwitch, NTag } from 'naive-ui'
import container from '@/api/panel/container'
import ContainerCreate from '@/views/container/ContainerCreate.vue'
@@ -52,19 +52,32 @@ const columns: any = [
})
}
},
{ title: '镜像', key: 'image', minWidth: 300, resizable: true, ellipsis: { tooltip: true } },
{
title: '镜像',
key: 'image',
minWidth: 300,
resizable: true,
render(row: any): any {
return h(NTag, null, {
default: () => row.image
})
}
},
{
title: '端口(主机->容器)',
key: 'ports',
width: 100,
minWidth: 200,
resizable: true,
ellipsis: { tooltip: true },
render(row: any) {
return row.ports
.map((port: any) => {
return `${port.IP ? port.IP + ':' : ''}${port.PublicPort}->${port.PrivatePort}/${port.Type}`
})
.join(', ')
render(row: any): any {
return h(NFlex, null, {
default: () =>
row.ports.map((port: any) =>
h(NTag, null, {
default: () =>
`${port.host ? port.host + ':' : ''}${port.container_start}->${port.host_start}/${port.protocol}`
})
)
})
}
},
{

View File

@@ -2,6 +2,7 @@
import { NButton, NDataTable, NFlex, NInput, NPopconfirm, NTag } from 'naive-ui'
import container from '@/api/panel/container'
import { formatDateTime } from '@/utils'
import type { ImageList } from '@/views/container/types'
const pullModel = ref({
@@ -62,10 +63,12 @@ const columns: any = [
},
{
title: '创建时间',
key: 'created',
key: 'created_at',
width: 200,
resizable: true,
ellipsis: { tooltip: true }
render(row: any) {
return formatDateTime(row.created_at)
}
},
{
title: '操作',

View File

@@ -2,13 +2,14 @@
import { NButton, NDataTable, NFlex, NInput, NPopconfirm, NTag } from 'naive-ui'
import container from '@/api/panel/container'
import { formatDateTime } from '@/utils'
import type { NetworkList } from '@/views/container/types'
const createModel = ref({
name: '',
driver: 'bridge',
ipv4: {
enabled: true,
enabled: false,
subnet: '',
gateway: '',
ip_range: ''
@@ -101,10 +102,12 @@ const columns: any = [
},
{
title: '创建时间',
key: 'created',
key: 'created_at',
width: 200,
resizable: true,
ellipsis: { tooltip: true }
render(row: any) {
return formatDateTime(row.created_at)
}
},
{
title: '操作',

View File

@@ -2,6 +2,7 @@
import { NButton, NDataTable, NInput, NPopconfirm } from 'naive-ui'
import container from '@/api/panel/container'
import { formatDateTime } from '@/utils'
import type { VolumeList } from '@/views/container/types'
const createModel = ref({
@@ -27,7 +28,7 @@ const columns: any = [
{ type: 'selection', fixed: 'left' },
{
title: '名称',
key: 'id',
key: 'name',
minWidth: 150,
resizable: true,
ellipsis: { tooltip: true }
@@ -48,17 +49,19 @@ const columns: any = [
},
{
title: '挂载点',
key: 'mount',
key: 'mount_point',
resizable: true,
minWidth: 150,
ellipsis: { tooltip: true }
},
{
title: '创建时间',
key: 'created',
key: 'created_at',
width: 200,
resizable: true,
ellipsis: { tooltip: true }
render(row: any) {
return formatDateTime(row.created_at)
}
},
{
title: '操作',