diff --git a/internal/biz/container_network.go b/internal/biz/container_network.go index ddbea711..65ae31e0 100644 --- a/internal/biz/container_network.go +++ b/internal/biz/container_network.go @@ -1,13 +1,12 @@ package biz import ( - "github.com/docker/docker/api/types/network" - "github.com/TheTNB/panel/internal/http/request" + "github.com/TheTNB/panel/pkg/types" ) type ContainerNetworkRepo interface { - List() ([]network.Inspect, error) + List() ([]types.ContainerNetwork, error) Create(req *request.ContainerNetworkCreate) (string, error) Remove(id string) error Prune() error diff --git a/internal/data/container.go b/internal/data/container.go index a6640894..b30d9a63 100644 --- a/internal/data/container.go +++ b/internal/data/container.go @@ -31,7 +31,7 @@ func NewContainerRepo(cmd ...string) biz.ContainerRepo { // ListAll 列出所有容器 func (r *containerRepo) ListAll() ([]types.Container, error) { - output, err := shell.ExecfWithTimeout(10*time.Second, "%s ps -a --format '{{json .}}'", r.cmd) + output, err := shell.ExecfWithTimeout(10*time.Second, "%s ps -a --format json", r.cmd) if err != nil { return nil, err } @@ -71,7 +71,7 @@ func (r *containerRepo) ListAll() ([]types.Container, error) { Command: item.Command, CreatedAt: createdAt, Ports: r.parsePorts(item.Ports), - Labels: r.parseLabels(item.Labels), + Labels: types.SliceToKV(strings.Split(item.Labels, ",")), State: item.State, Status: item.Status, }) @@ -210,25 +210,6 @@ func (r *containerRepo) Prune() error { 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 diff --git a/internal/data/container_image.go b/internal/data/container_image.go index a80c9f5f..4528d058 100644 --- a/internal/data/container_image.go +++ b/internal/data/container_image.go @@ -29,7 +29,7 @@ func NewContainerImageRepo(cmd ...string) biz.ContainerImageRepo { // List 列出镜像 func (r *containerImageRepo) List() ([]types.ContainerImage, error) { - output, err := shell.ExecfWithTimeout(10*time.Second, "%s images -a --format '{{json .}}'", r.cmd) + output, err := shell.ExecfWithTimeout(10*time.Second, "%s images -a --format json", r.cmd) if err != nil { return nil, err } diff --git a/internal/data/container_network.go b/internal/data/container_network.go index b44e9985..4644f721 100644 --- a/internal/data/container_network.go +++ b/internal/data/container_network.go @@ -1,104 +1,131 @@ package data import ( - "context" + "encoding/json" + "fmt" + "strings" + "time" - "github.com/docker/docker/api/types/filters" - "github.com/docker/docker/api/types/network" - "github.com/docker/docker/client" + "github.com/spf13/cast" "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" ) type containerNetworkRepo struct { - client *client.Client + cmd string } -func NewContainerNetworkRepo(sock ...string) biz.ContainerNetworkRepo { - if len(sock) == 0 { - sock = append(sock, "/run/podman/podman.sock") +func NewContainerNetworkRepo(cmd ...string) biz.ContainerNetworkRepo { + if len(cmd) == 0 { + cmd = append(cmd, "docker") } - cli, _ := client.NewClientWithOpts(client.WithHost("unix://"+sock[0]), client.WithAPIVersionNegotiation()) return &containerNetworkRepo{ - client: cli, + cmd: cmd[0], } } // List 列出网络 -func (r *containerNetworkRepo) List() ([]network.Inspect, error) { - return r.client.NetworkList(context.Background(), network.ListOptions{}) +func (r *containerNetworkRepo) List() ([]types.ContainerNetwork, error) { + output, err := shell.ExecfWithTimeout(10*time.Second, "%s network ls --format json", r.cmd) + 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 []types.ContainerNetworkInspect + 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") + } + + 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), + Scope: item.Scope, + CreatedAt: createdAt, + IPAM: inspect[0].IPAM, + Options: types.MapToKV(inspect[0].Options), + Labels: types.SliceToKV(strings.Split(item.Labels, ",")), + }) + } + + return networks, nil } // Create 创建网络 func (r *containerNetworkRepo) Create(req *request.ContainerNetworkCreate) (string, error) { - var ipamConfigs []network.IPAMConfig - if req.Ipv4.Enabled { - ipamConfigs = append(ipamConfigs, network.IPAMConfig{ - Subnet: req.Ipv4.Subnet, - Gateway: req.Ipv4.Gateway, - IPRange: req.Ipv4.IPRange, - }) - } - if req.Ipv6.Enabled { - ipamConfigs = append(ipamConfigs, network.IPAMConfig{ - Subnet: req.Ipv6.Subnet, - Gateway: req.Ipv6.Gateway, - IPRange: req.Ipv6.IPRange, - }) - } + var sb strings.Builder - options := network.CreateOptions{ - EnableIPv6: &req.Ipv6.Enabled, - Driver: req.Driver, - Options: types.KVToMap(req.Options), - Labels: types.KVToMap(req.Labels), - } - if len(ipamConfigs) > 0 { - options.IPAM = &network.IPAM{ - Config: ipamConfigs, + sb.WriteString(fmt.Sprintf("%s network create --driver %s", r.cmd, req.Driver)) + sb.WriteString(fmt.Sprintf(" %s", req.Name)) + + if req.Ipv4.Enabled { + sb.WriteString(fmt.Sprintf(" --subnet %s", req.Ipv4.Subnet)) + sb.WriteString(fmt.Sprintf(" --gateway %s", req.Ipv4.Gateway)) + if req.Ipv4.IPRange != "" { + sb.WriteString(fmt.Sprintf(" --ip-range %s", req.Ipv4.IPRange)) } } + if req.Ipv6.Enabled { + sb.WriteString(fmt.Sprintf(" --subnet %s", req.Ipv6.Subnet)) + sb.WriteString(fmt.Sprintf(" --gateway %s", req.Ipv6.Gateway)) + if req.Ipv6.IPRange != "" { + sb.WriteString(fmt.Sprintf(" --ip-range %s", req.Ipv6.IPRange)) + } + } + 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)) + } - resp, err := r.client.NetworkCreate(context.Background(), req.Name, options) - return resp.ID, err + return shell.ExecfWithTimeout(10*time.Second, "%s", sb.String()) // nolint: govet } // Remove 删除网络 func (r *containerNetworkRepo) Remove(id string) error { - return r.client.NetworkRemove(context.Background(), id) -} - -// Exist 判断网络是否存在 -func (r *containerNetworkRepo) Exist(name string) (bool, error) { - var options network.ListOptions - options.Filters = filters.NewArgs(filters.Arg("name", name)) - networks, err := r.client.NetworkList(context.Background(), options) - if err != nil { - return false, err - } - - return len(networks) > 0, nil -} - -// Inspect 查看网络 -func (r *containerNetworkRepo) Inspect(id string) (network.Inspect, error) { - return r.client.NetworkInspect(context.Background(), id, network.InspectOptions{}) -} - -// Connect 连接网络 -func (r *containerNetworkRepo) Connect(networkID string, containerID string) error { - return r.client.NetworkConnect(context.Background(), networkID, containerID, nil) -} - -// Disconnect 断开网络 -func (r *containerNetworkRepo) Disconnect(networkID string, containerID string) error { - return r.client.NetworkDisconnect(context.Background(), networkID, containerID, true) + _, err := shell.ExecfWithTimeout(10*time.Second, "%s network rm %s", r.cmd, id) + return err } // Prune 清理未使用的网络 func (r *containerNetworkRepo) Prune() error { - _, err := r.client.NetworksPrune(context.Background(), filters.NewArgs()) + _, err := shell.ExecfWithTimeout(10*time.Second, "%s network prune -f", r.cmd) return err } diff --git a/internal/http/request/container.go b/internal/http/request/container.go index db17ede3..88e61f1e 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.ContainerPort `form:"ports" json:"ports"` + Network string `form:"network" json:"network"` + Volumes []types.ContainerContainerVolume `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/http/request/container_network.go b/internal/http/request/container_network.go index f0c8a288..10b6ac3a 100644 --- a/internal/http/request/container_network.go +++ b/internal/http/request/container_network.go @@ -7,15 +7,10 @@ type ContainerNetworkID struct { } type ContainerNetworkCreate struct { - Name string `form:"name" json:"name" validate:"required"` - Driver string `form:"driver" json:"driver"` - Ipv4 types.ContainerNetwork `form:"ipv4" json:"ipv4"` - Ipv6 types.ContainerNetwork `form:"ipv6" json:"ipv6"` - Labels []types.KV `form:"labels" json:"labels"` - Options []types.KV `form:"options" json:"options"` -} - -type ContainerNetworkConnect struct { - Network string `form:"network" json:"network" validate:"required"` - Container string `form:"container" json:"container" validate:"required"` + Name string `form:"name" json:"name" validate:"required"` + Driver string `form:"driver" json:"driver"` + Ipv4 types.ContainerContainerNetwork `form:"ipv4" json:"ipv4"` + Ipv6 types.ContainerContainerNetwork `form:"ipv6" json:"ipv6"` + Labels []types.KV `form:"labels" json:"labels"` + Options []types.KV `form:"options" json:"options"` } diff --git a/internal/service/container_network.go b/internal/service/container_network.go index 4b5517bd..867c1815 100644 --- a/internal/service/container_network.go +++ b/internal/service/container_network.go @@ -2,7 +2,6 @@ package service import ( "net/http" - "time" "github.com/go-rat/chix" @@ -30,40 +29,9 @@ func (s *ContainerNetworkService) List(w http.ResponseWriter, r *http.Request) { paged, total := Paginate(r, networks) - items := make([]any, 0) - for _, item := range paged { - var ipamConfig []any - for _, v := range item.IPAM.Config { - ipamConfig = append(ipamConfig, map[string]any{ - "subnet": v.Subnet, - "gateway": v.Gateway, - "ip_range": v.IPRange, - "aux_address": v.AuxAddress, - }) - } - items = append(items, map[string]any{ - "id": item.ID, - "name": item.Name, - "driver": item.Driver, - "ipv6": item.EnableIPv6, - "scope": item.Scope, - "internal": item.Internal, - "attachable": item.Attachable, - "ingress": item.Ingress, - "labels": item.Labels, - "options": item.Options, - "ipam": map[string]any{ - "config": ipamConfig, - "driver": item.IPAM.Driver, - "options": item.IPAM.Options, - }, - "created": item.Created.Format(time.DateTime), - }) - } - Success(w, chix.M{ "total": total, - "items": items, + "items": paged, }) } diff --git a/pkg/types/common.go b/pkg/types/common.go index 7728b67b..d5aa44e6 100644 --- a/pkg/types/common.go +++ b/pkg/types/common.go @@ -1,5 +1,7 @@ package types +import "strings" + type NV struct { Name string `json:"name"` Value string `json:"value"` @@ -30,6 +32,16 @@ func KVToMap(kvs []KV) map[string]string { return m } +// MapToKV 将 map 转换为 key-value 切片 +func MapToKV(m map[string]string) []KV { + var kvs []KV + for k, v := range m { + kvs = append(kvs, KV{Key: k, Value: v}) + } + + return kvs +} + // KVToSlice 将 key-value 切片转换为 key=value 切片 func KVToSlice(kvs []KV) []string { var s []string @@ -39,3 +51,16 @@ func KVToSlice(kvs []KV) []string { return s } + +// SliceToKV 将 key=value 切片转换为 key-value 切片 +func SliceToKV(s []string) []KV { + var kvs []KV + for _, item := range s { + kv := strings.SplitN(item, "=", 2) + if len(kv) == 2 { + kvs = append(kvs, KV{Key: kv[0], Value: kv[1]}) + } + } + + return kvs +} diff --git a/pkg/types/container.go b/pkg/types/container.go index c2a6430a..8afc11af 100644 --- a/pkg/types/container.go +++ b/pkg/types/container.go @@ -24,13 +24,13 @@ type ContainerPort struct { Protocol string `form:"protocol" json:"protocol"` } -type ContainerVolume struct { +type ContainerContainerVolume struct { Host string `form:"host" json:"host"` Container string `form:"container" json:"container"` Mode string `form:"mode" json:"mode"` } -type ContainerNetwork struct { +type ContainerContainerNetwork struct { Enabled bool `form:"enabled" json:"enabled"` Gateway string `form:"gateway" json:"gateway"` IPRange string `form:"ip_range" json:"ip_range"` diff --git a/pkg/types/container_network.go b/pkg/types/container_network.go new file mode 100644 index 00000000..0fdf2925 --- /dev/null +++ b/pkg/types/container_network.go @@ -0,0 +1,49 @@ +package types + +import "time" + +type ContainerNetwork struct { + ID string `json:"id"` + Name string `json:"name"` + Driver string `json:"driver"` + IPv6 bool `json:"ipv6"` + Internal bool `json:"internal"` + Attachable bool `json:"attachable"` + Ingress bool `json:"ingress"` + Scope string `json:"scope"` + CreatedAt time.Time `json:"created_at"` + IPAM ContainerNetworkIPAM `json:"ipam"` + Options []KV `json:"options"` + Labels []KV `json:"labels"` +} + +// ContainerNetworkIPAM represents IP Address Management +type ContainerNetworkIPAM struct { + Driver string + Options map[string]string // Per network IPAM driver options + Config []ContainerNetworkIPAMConfig +} + +// ContainerNetworkIPAMConfig represents IPAM configurations +type ContainerNetworkIPAMConfig struct { + Subnet string `json:"subnet"` + IPRange string `json:"ip_range"` + Gateway string `json:"gateway"` + AuxAddress map[string]string `json:"AuxiliaryAddresses,omitempty"` +} + +type ContainerNetworkInspect 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 ContainerNetworkIPAM + 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"` +}