mirror of
https://github.com/acepanel/panel.git
synced 2026-02-04 07:57:21 +08:00
feat: 容器网络使用cli实现
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
|
||||
49
pkg/types/container_network.go
Normal file
49
pkg/types/container_network.go
Normal file
@@ -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"`
|
||||
}
|
||||
Reference in New Issue
Block a user