2
0
mirror of https://github.com/acepanel/panel.git synced 2026-02-04 07:57:21 +08:00

feat: 容器网络使用cli实现

This commit is contained in:
耗子
2024-10-27 18:51:53 +08:00
parent beb271b3b9
commit f3a578380a
10 changed files with 200 additions and 156 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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