mirror of
https://github.com/acepanel/panel.git
synced 2026-02-04 13:47:15 +08:00
feat: 使用cli实现容器管理
This commit is contained in:
2
go.mod
2
go.mod
@@ -6,7 +6,6 @@ require (
|
||||
github.com/bddjr/hlfhr v1.1.2
|
||||
github.com/beevik/ntp v1.4.3
|
||||
github.com/docker/docker v27.3.1+incompatible
|
||||
github.com/docker/go-connections v0.5.0
|
||||
github.com/expr-lang/expr v1.16.9
|
||||
github.com/glebarez/sqlite v1.11.0
|
||||
github.com/go-chi/chi/v5 v5.1.0
|
||||
@@ -63,6 +62,7 @@ require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/devhaozi/huaweicloud-sdk-go-v3 v0.0.0-20241018211007-bbebb6de5db7 // indirect
|
||||
github.com/distribution/reference v0.6.0 // indirect
|
||||
github.com/docker/go-connections v0.5.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
package biz
|
||||
|
||||
import (
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
|
||||
"github.com/TheTNB/panel/internal/http/request"
|
||||
"github.com/TheTNB/panel/pkg/types"
|
||||
)
|
||||
|
||||
type ContainerRepo interface {
|
||||
ListAll() ([]types.Container, error)
|
||||
ListByNames(names []string) ([]types.Container, error)
|
||||
ListByName(name string) ([]types.Container, error)
|
||||
Create(req *request.ContainerCreate) (string, error)
|
||||
Remove(id string) error
|
||||
Start(id string) error
|
||||
@@ -19,7 +17,6 @@ type ContainerRepo interface {
|
||||
Unpause(id string) error
|
||||
Kill(id string) error
|
||||
Rename(id string, newName string) error
|
||||
Update(id string, config container.UpdateConfig) error
|
||||
Logs(id string) (string, error)
|
||||
Prune() error
|
||||
}
|
||||
|
||||
@@ -1,213 +1,259 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/api/types/network"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/docker/go-connections/nat"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/TheTNB/panel/internal/biz"
|
||||
"github.com/TheTNB/panel/internal/http/request"
|
||||
paneltypes "github.com/TheTNB/panel/pkg/types"
|
||||
"github.com/TheTNB/panel/pkg/shell"
|
||||
"github.com/TheTNB/panel/pkg/types"
|
||||
)
|
||||
|
||||
type containerRepo struct {
|
||||
client *client.Client
|
||||
cmd string
|
||||
}
|
||||
|
||||
func NewContainerRepo(sock ...string) biz.ContainerRepo {
|
||||
if len(sock) == 0 {
|
||||
sock = append(sock, "/run/podman/podman.sock")
|
||||
func NewContainerRepo(cmd ...string) biz.ContainerRepo {
|
||||
if len(cmd) == 0 {
|
||||
cmd = append(cmd, "docker")
|
||||
}
|
||||
cli, _ := client.NewClientWithOpts(client.WithHost("unix://"+sock[0]), client.WithAPIVersionNegotiation())
|
||||
return &containerRepo{
|
||||
client: cli,
|
||||
cmd: cmd[0],
|
||||
}
|
||||
}
|
||||
|
||||
// ListAll 列出所有容器
|
||||
func (r *containerRepo) ListAll() ([]types.Container, error) {
|
||||
containers, err := r.client.ContainerList(context.Background(), container.ListOptions{
|
||||
All: true,
|
||||
})
|
||||
output, err := shell.ExecfWithTimeout(10*time.Second, "%s ps -a --format '{{json .}}'", r.cmd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
lines := strings.Split(output, "\n")
|
||||
|
||||
var containers []types.Container
|
||||
for _, line := range lines {
|
||||
if line == "" {
|
||||
continue // 跳过空行
|
||||
}
|
||||
|
||||
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, 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,
|
||||
Image: item.Image,
|
||||
Command: item.Command,
|
||||
CreatedAt: createdAt,
|
||||
Ports: r.parsePorts(item.Ports),
|
||||
Labels: r.parseLabels(item.Labels),
|
||||
State: item.State,
|
||||
Status: item.Status,
|
||||
})
|
||||
}
|
||||
|
||||
return containers, nil
|
||||
}
|
||||
|
||||
// ListByNames 根据名称列出容器
|
||||
func (r *containerRepo) ListByNames(names []string) ([]types.Container, error) {
|
||||
var options container.ListOptions
|
||||
options.All = true
|
||||
if len(names) > 0 {
|
||||
var array []filters.KeyValuePair
|
||||
for _, n := range names {
|
||||
array = append(array, filters.Arg("name", n))
|
||||
}
|
||||
options.Filters = filters.NewArgs(array...)
|
||||
}
|
||||
containers, err := r.client.ContainerList(context.Background(), options)
|
||||
// ListByName 根据名称搜索容器
|
||||
func (r *containerRepo) ListByName(names string) ([]types.Container, error) {
|
||||
containers, err := r.ListAll()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
containers = slices.DeleteFunc(containers, func(item types.Container) bool {
|
||||
return !strings.Contains(item.Name, names)
|
||||
})
|
||||
|
||||
return containers, nil
|
||||
}
|
||||
|
||||
// Create 创建容器
|
||||
func (r *containerRepo) Create(req *request.ContainerCreate) (string, error) {
|
||||
var hostConf container.HostConfig
|
||||
var networkConf network.NetworkingConfig
|
||||
sb := strings.Builder{}
|
||||
sb.WriteString(fmt.Sprintf("%s create --name %s --image %s", r.cmd, req.Name, req.Image))
|
||||
|
||||
portMap := make(nat.PortMap)
|
||||
for _, port := range req.Ports {
|
||||
if port.ContainerStart-port.ContainerEnd != port.HostStart-port.HostEnd {
|
||||
return "", fmt.Errorf("容器端口和主机端口数量不匹配(容器: %d 主机: %d)", port.ContainerStart-port.ContainerEnd, port.HostStart-port.HostEnd)
|
||||
}
|
||||
if port.ContainerStart > port.ContainerEnd || port.HostStart > port.HostEnd || port.ContainerStart < 1 || port.HostStart < 1 {
|
||||
return "", fmt.Errorf("端口范围不正确")
|
||||
}
|
||||
|
||||
count := 0
|
||||
for host := port.HostStart; host <= port.HostEnd; host++ {
|
||||
bindItem := nat.PortBinding{HostPort: strconv.Itoa(host), HostIP: port.Host}
|
||||
portMap[nat.Port(fmt.Sprintf("%d/%s", port.ContainerStart+count, port.Protocol))] = []nat.PortBinding{bindItem}
|
||||
count++
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf(" -p %s:%d", port.Host, port.ContainerStart))
|
||||
}
|
||||
|
||||
exposed := make(nat.PortSet)
|
||||
for port := range portMap {
|
||||
exposed[port] = struct{}{}
|
||||
}
|
||||
|
||||
if req.Network != "" {
|
||||
switch req.Network {
|
||||
case "host", "none", "bridge":
|
||||
hostConf.NetworkMode = container.NetworkMode(req.Network)
|
||||
}
|
||||
networkConf.EndpointsConfig = map[string]*network.EndpointSettings{req.Network: {}}
|
||||
} else {
|
||||
networkConf = network.NetworkingConfig{}
|
||||
sb.WriteString(fmt.Sprintf(" --network %s", req.Network))
|
||||
}
|
||||
for _, volume := range req.Volumes {
|
||||
sb.WriteString(fmt.Sprintf(" -v %s:%s:%s", volume.Host, volume.Container, volume.Mode))
|
||||
}
|
||||
for _, label := range req.Labels {
|
||||
sb.WriteString(fmt.Sprintf(" --label %s=%s", label.Key, label.Value))
|
||||
}
|
||||
for _, env := range req.Env {
|
||||
sb.WriteString(fmt.Sprintf(" -e %s=%s", env.Key, env.Value))
|
||||
}
|
||||
if len(req.Entrypoint) > 0 {
|
||||
sb.WriteString(fmt.Sprintf(" --entrypoint '%s'", strings.Join(req.Entrypoint, " ")))
|
||||
}
|
||||
if len(req.Command) > 0 {
|
||||
sb.WriteString(fmt.Sprintf(" '%s'", strings.Join(req.Command, " ")))
|
||||
}
|
||||
if req.RestartPolicy != "" {
|
||||
sb.WriteString(fmt.Sprintf(" --restart %s", req.RestartPolicy))
|
||||
}
|
||||
if req.AutoRemove {
|
||||
sb.WriteString(" --rm")
|
||||
}
|
||||
if req.Privileged {
|
||||
sb.WriteString(" --privileged")
|
||||
}
|
||||
if req.OpenStdin {
|
||||
sb.WriteString(" -i")
|
||||
}
|
||||
if req.PublishAllPorts {
|
||||
sb.WriteString(" -P")
|
||||
}
|
||||
if req.Tty {
|
||||
sb.WriteString(" -t")
|
||||
}
|
||||
if req.CPUShares > 0 {
|
||||
sb.WriteString(fmt.Sprintf(" --cpu-shares %d", req.CPUShares))
|
||||
}
|
||||
if req.CPUs > 0 {
|
||||
sb.WriteString(fmt.Sprintf(" --cpus %d", req.CPUs))
|
||||
}
|
||||
if req.Memory > 0 {
|
||||
sb.WriteString(fmt.Sprintf(" --memory %d", req.Memory))
|
||||
}
|
||||
|
||||
hostConf.Privileged = req.Privileged
|
||||
hostConf.AutoRemove = req.AutoRemove
|
||||
hostConf.CPUShares = req.CPUShares
|
||||
hostConf.PublishAllPorts = req.PublishAllPorts
|
||||
hostConf.RestartPolicy = container.RestartPolicy{Name: container.RestartPolicyMode(req.RestartPolicy)}
|
||||
if req.RestartPolicy == "on-failure" {
|
||||
hostConf.RestartPolicy.MaximumRetryCount = 5
|
||||
}
|
||||
hostConf.NanoCPUs = req.CPUs * 1000000000
|
||||
hostConf.Memory = req.Memory * 1024 * 1024
|
||||
hostConf.MemorySwap = 0
|
||||
hostConf.PortBindings = portMap
|
||||
hostConf.Binds = []string{}
|
||||
|
||||
volumes := make(map[string]struct{})
|
||||
for _, v := range req.Volumes {
|
||||
volumes[v.Container] = struct{}{}
|
||||
hostConf.Binds = append(hostConf.Binds, fmt.Sprintf("%s:%s:%s", v.Host, v.Container, v.Mode))
|
||||
}
|
||||
|
||||
resp, err := r.client.ContainerCreate(context.Background(), &container.Config{
|
||||
Image: req.Image,
|
||||
Env: paneltypes.KVToSlice(req.Env),
|
||||
Entrypoint: req.Entrypoint,
|
||||
Cmd: req.Command,
|
||||
Labels: paneltypes.KVToMap(req.Labels),
|
||||
ExposedPorts: exposed,
|
||||
OpenStdin: req.OpenStdin,
|
||||
Tty: req.Tty,
|
||||
Volumes: volumes,
|
||||
}, &hostConf, &networkConf, nil, req.Name)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return resp.ID, err
|
||||
return shell.ExecfWithTimeout(10*time.Second, sb.String()) // nolint: govet
|
||||
}
|
||||
|
||||
// Remove 移除容器
|
||||
func (r *containerRepo) Remove(id string) error {
|
||||
return r.client.ContainerRemove(context.Background(), id, container.RemoveOptions{
|
||||
Force: true,
|
||||
})
|
||||
_, err := shell.ExecfWithTimeout(10*time.Second, "%s rm -f %s", r.cmd, id)
|
||||
return err
|
||||
}
|
||||
|
||||
// Start 启动容器
|
||||
func (r *containerRepo) Start(id string) error {
|
||||
return r.client.ContainerStart(context.Background(), id, container.StartOptions{})
|
||||
_, err := shell.ExecfWithTimeout(10*time.Second, "%s start %s", r.cmd, id)
|
||||
return err
|
||||
}
|
||||
|
||||
// Stop 停止容器
|
||||
func (r *containerRepo) Stop(id string) error {
|
||||
return r.client.ContainerStop(context.Background(), id, container.StopOptions{})
|
||||
_, err := shell.ExecfWithTimeout(10*time.Second, "%s stop %s", r.cmd, id)
|
||||
return err
|
||||
}
|
||||
|
||||
// Restart 重启容器
|
||||
func (r *containerRepo) Restart(id string) error {
|
||||
return r.client.ContainerRestart(context.Background(), id, container.StopOptions{})
|
||||
_, err := shell.ExecfWithTimeout(10*time.Second, "%s restart %s", r.cmd, id)
|
||||
return err
|
||||
}
|
||||
|
||||
// Pause 暂停容器
|
||||
func (r *containerRepo) Pause(id string) error {
|
||||
return r.client.ContainerPause(context.Background(), id)
|
||||
_, err := shell.ExecfWithTimeout(10*time.Second, "%s pause %s", r.cmd, id)
|
||||
return err
|
||||
}
|
||||
|
||||
// Unpause 恢复容器
|
||||
func (r *containerRepo) Unpause(id string) error {
|
||||
return r.client.ContainerUnpause(context.Background(), id)
|
||||
_, err := shell.ExecfWithTimeout(10*time.Second, "%s unpause %s", r.cmd, id)
|
||||
return err
|
||||
}
|
||||
|
||||
// Kill 杀死容器
|
||||
func (r *containerRepo) Kill(id string) error {
|
||||
return r.client.ContainerKill(context.Background(), id, "KILL")
|
||||
_, err := shell.ExecfWithTimeout(10*time.Second, "%s kill %s", r.cmd, id)
|
||||
return err
|
||||
}
|
||||
|
||||
// Rename 重命名容器
|
||||
func (r *containerRepo) Rename(id string, newName string) error {
|
||||
return r.client.ContainerRename(context.Background(), id, newName)
|
||||
}
|
||||
|
||||
// Update 更新容器
|
||||
func (r *containerRepo) Update(id string, config container.UpdateConfig) error {
|
||||
_, err := r.client.ContainerUpdate(context.Background(), id, config)
|
||||
_, err := shell.ExecfWithTimeout(10*time.Second, "%s rename %s %s", r.cmd, id, newName)
|
||||
return err
|
||||
}
|
||||
|
||||
// Logs 查看容器日志
|
||||
func (r *containerRepo) Logs(id string) (string, error) {
|
||||
options := container.LogsOptions{
|
||||
ShowStdout: true,
|
||||
ShowStderr: true,
|
||||
}
|
||||
reader, err := r.client.ContainerLogs(context.Background(), id, options)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
data, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(data), nil
|
||||
return shell.ExecfWithTimeout(10*time.Second, "%s logs %s", r.cmd, id)
|
||||
}
|
||||
|
||||
// Prune 清理未使用的容器
|
||||
func (r *containerRepo) Prune() error {
|
||||
_, err := r.client.ContainersPrune(context.Background(), filters.NewArgs())
|
||||
_, err := shell.ExecfWithTimeout(10*time.Second, "%s container prune -f", r.cmd)
|
||||
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
|
||||
|
||||
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{
|
||||
IP: host,
|
||||
PublicPort: cast.ToUint(public),
|
||||
PrivatePort: cast.ToUint(private),
|
||||
Type: protocol,
|
||||
})
|
||||
}
|
||||
|
||||
return portList
|
||||
}
|
||||
|
||||
@@ -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.ContainerCreatePort `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"`
|
||||
}
|
||||
|
||||
@@ -2,8 +2,6 @@ package service
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-rat/chix"
|
||||
|
||||
@@ -31,21 +29,16 @@ func (s *ContainerService) List(w http.ResponseWriter, r *http.Request) {
|
||||
paged, total := Paginate(r, containers)
|
||||
items := make([]any, 0)
|
||||
for _, item := range paged {
|
||||
var name string
|
||||
if len(item.Names) > 0 {
|
||||
name = item.Names[0]
|
||||
}
|
||||
items = append(items, map[string]any{
|
||||
"id": item.ID,
|
||||
"name": strings.TrimLeft(name, "/"),
|
||||
"image": item.Image,
|
||||
"image_id": item.ImageID,
|
||||
"command": item.Command,
|
||||
"created": time.Unix(item.Created, 0).Format(time.DateTime),
|
||||
"ports": item.Ports,
|
||||
"labels": item.Labels,
|
||||
"state": item.State,
|
||||
"status": item.Status,
|
||||
"id": item.ID,
|
||||
"name": item.Name,
|
||||
"image": item.Image,
|
||||
"command": item.Command,
|
||||
"created_at": item.CreatedAt,
|
||||
"ports": item.Ports,
|
||||
"labels": item.Labels,
|
||||
"state": item.State,
|
||||
"status": item.Status,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -56,8 +49,7 @@ func (s *ContainerService) List(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (s *ContainerService) Search(w http.ResponseWriter, r *http.Request) {
|
||||
name := strings.Fields(r.FormValue("name"))
|
||||
containers, err := s.containerRepo.ListByNames(name)
|
||||
containers, err := s.containerRepo.ListByName(r.FormValue("name"))
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
|
||||
@@ -1,6 +1,28 @@
|
||||
package types
|
||||
|
||||
import "time"
|
||||
|
||||
type ContainerPort struct {
|
||||
IP string `json:"ip,omitempty"`
|
||||
PrivatePort uint `json:"private_port"`
|
||||
PublicPort uint `json:"public_port,omitempty"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
type Container struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Image string `json:"image"`
|
||||
ImageID string `json:"image_id"`
|
||||
Command string `json:"command"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
Ports []ContainerPort `json:"ports"`
|
||||
Labels []KV
|
||||
State string
|
||||
Status string
|
||||
}
|
||||
|
||||
type ContainerCreatePort struct {
|
||||
ContainerStart int `form:"container_start" json:"container_start"`
|
||||
ContainerEnd int `form:"container_end" json:"container_end"`
|
||||
Host string `form:"host" json:"host"`
|
||||
|
||||
Reference in New Issue
Block a user