mirror of
https://github.com/tnb-labs/panel.git
synced 2026-10-16 15:42:54 +08:00
- 补齐后端已支持但表单未暴露的主机名、静态 IP、网络别名、DNS、额外 Hosts、工作目录、用户、Init、只读、tmpfs、共享内存、ulimit、停止信号与超时、健康检查、能力、安全选项、设备、内核参数,编辑时完整回填,删除重建不再丢配置;新增网络与高级设置标签页 - 静态 IP 改写入 IPAMConfig 并支持 IPv6,原先写在只读的 IPAddress 上被 Docker 忽略 - 设备权限与容器路径缺省时对齐 docker run 默认值,健康检查 NONE 转为 --no-healthcheck,非法 DNS 直接报错 - 共享内存单位改为 MB、健康检查时长改为秒,与内存等字段一致 - 网络下拉改按名称传值,网络名同时作为 NetworkMode 与端点 key,修复选 host 网络后无法启动 - 启动从 data 层移到 biz 层,启动失败时返回错误,不再提示创建成功 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
237 lines
6.8 KiB
Go
237 lines
6.8 KiB
Go
package docker
|
|
|
|
import (
|
|
"cmp"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/distribution/reference"
|
|
"github.com/libtnb/utils/str"
|
|
|
|
"github.com/acepanel/panel/v3/internal/request"
|
|
"github.com/acepanel/panel/v3/pkg/shell"
|
|
)
|
|
|
|
// ImagePullShell 生成拉取镜像的命令,需要认证时返回配套的清理命令
|
|
func ImagePullShell(sock string, req *request.ContainerImagePull) (string, string, error) {
|
|
if !req.Auth {
|
|
return Command(sock, "pull", req.Name), "", nil
|
|
}
|
|
|
|
named, err := reference.ParseNormalizedNamed(req.Name)
|
|
if err != nil {
|
|
return "", "", fmt.Errorf("invalid image reference: %w", err)
|
|
}
|
|
|
|
configDir := filepath.Join(os.TempDir(), "ace-docker-task-"+str.Random(16))
|
|
cleanup := "rm -rf " + shell.Quote(configDir)
|
|
shell := strings.Join([]string{
|
|
"set -e",
|
|
"mkdir -p " + shell.Quote(configDir),
|
|
"trap " + shell.Quote(cleanup) + " EXIT",
|
|
"printf %s " + shell.Quote(req.Password) + " | " + Command(sock, "--config", configDir, "login", "--username", req.Username, "--password-stdin", reference.Domain(named)),
|
|
Command(sock, "--config", configDir, "pull", req.Name),
|
|
}, "\n")
|
|
|
|
return shell, cleanup, nil
|
|
}
|
|
|
|
// RunShell 生成后台创建容器的命令
|
|
func RunShell(sock string, req *request.ContainerCreate) (string, error) {
|
|
args := []string{"run", "--detach"}
|
|
|
|
if req.Name != "" {
|
|
args = append(args, "--name", req.Name)
|
|
}
|
|
if req.Network != "" {
|
|
args = append(args, "--network", req.Network)
|
|
}
|
|
if req.Hostname != "" {
|
|
args = append(args, "--hostname", req.Hostname)
|
|
}
|
|
if req.WorkingDir != "" {
|
|
args = append(args, "--workdir", req.WorkingDir)
|
|
}
|
|
if req.User != "" {
|
|
args = append(args, "--user", req.User)
|
|
}
|
|
for _, alias := range req.NetworkAliases {
|
|
args = append(args, "--network-alias", alias)
|
|
}
|
|
if req.StaticIP != "" {
|
|
flag := "--ip"
|
|
if strings.Contains(req.StaticIP, ":") {
|
|
flag = "--ip6"
|
|
}
|
|
args = append(args, flag, req.StaticIP)
|
|
}
|
|
for _, dns := range req.DNS {
|
|
args = append(args, "--dns", dns)
|
|
}
|
|
for _, host := range req.ExtraHosts {
|
|
args = append(args, "--add-host", host)
|
|
}
|
|
for _, capability := range req.CapAdd {
|
|
args = append(args, "--cap-add", capability)
|
|
}
|
|
for _, capability := range req.CapDrop {
|
|
args = append(args, "--cap-drop", capability)
|
|
}
|
|
for _, device := range req.Devices {
|
|
args = append(args, "--device", mountSpec(device.Host, cmp.Or(device.Container, device.Host), device.Permissions))
|
|
}
|
|
for _, option := range req.SecurityOpt {
|
|
args = append(args, "--security-opt", option)
|
|
}
|
|
for _, sysctl := range req.Sysctls {
|
|
args = append(args, "--sysctl", sysctl.Key+"="+sysctl.Value)
|
|
}
|
|
for _, ulimit := range req.Ulimits {
|
|
args = append(args, "--ulimit", fmt.Sprintf("%s=%d:%d", ulimit.Name, ulimit.Soft, ulimit.Hard))
|
|
}
|
|
for _, tmpfs := range req.Tmpfs {
|
|
value := tmpfs.Key
|
|
if tmpfs.Value != "" {
|
|
value += ":" + tmpfs.Value
|
|
}
|
|
args = append(args, "--tmpfs", value)
|
|
}
|
|
if req.ShmSize > 0 {
|
|
args = append(args, "--shm-size", strconv.FormatInt(req.ShmSize, 10)+"m")
|
|
}
|
|
if req.Init {
|
|
args = append(args, "--init")
|
|
}
|
|
if req.StopSignal != "" {
|
|
args = append(args, "--stop-signal", req.StopSignal)
|
|
}
|
|
if req.StopTimeout > 0 {
|
|
args = append(args, "--stop-timeout", strconv.Itoa(req.StopTimeout))
|
|
}
|
|
if req.ReadonlyRootfs {
|
|
args = append(args, "--read-only")
|
|
}
|
|
if hc := req.Healthcheck; hc != nil && len(hc.Test) > 0 && hc.Test[0] == "NONE" {
|
|
args = append(args, "--no-healthcheck")
|
|
} else if hc != nil {
|
|
// Test 为空表示沿用镜像的检查命令,只覆盖时长与次数
|
|
if len(hc.Test) > 0 {
|
|
test := hc.Test
|
|
if test[0] == "CMD" || test[0] == "CMD-SHELL" {
|
|
test = test[1:]
|
|
}
|
|
args = append(args, "--health-cmd", strings.Join(test, " "))
|
|
}
|
|
if hc.Interval > 0 {
|
|
args = append(args, "--health-interval", strconv.Itoa(hc.Interval)+"s")
|
|
}
|
|
if hc.Timeout > 0 {
|
|
args = append(args, "--health-timeout", strconv.Itoa(hc.Timeout)+"s")
|
|
}
|
|
if hc.StartPeriod > 0 {
|
|
args = append(args, "--health-start-period", strconv.Itoa(hc.StartPeriod)+"s")
|
|
}
|
|
if hc.Retries > 0 {
|
|
args = append(args, "--health-retries", strconv.Itoa(hc.Retries))
|
|
}
|
|
}
|
|
if req.PublishAllPorts {
|
|
args = append(args, "--publish-all")
|
|
} else {
|
|
for _, port := range req.Ports {
|
|
if port.ContainerStart < 1 || port.ContainerEnd > 65535 || port.ContainerStart > port.ContainerEnd ||
|
|
port.HostStart < 1 || port.HostEnd > 65535 || port.HostStart > port.HostEnd {
|
|
return "", errors.New("port range is invalid")
|
|
}
|
|
if port.ContainerEnd-port.ContainerStart != port.HostEnd-port.HostStart {
|
|
return "", errors.New("container port and host port count do not match")
|
|
}
|
|
|
|
for offset := uint(0); offset <= port.HostEnd-port.HostStart; offset++ {
|
|
host := ""
|
|
if port.Host.IsValid() {
|
|
host = port.Host.String() + ":"
|
|
if port.Host.Is6() {
|
|
host = "[" + port.Host.String() + "]:"
|
|
}
|
|
}
|
|
mapping := fmt.Sprintf("%s%d:%d/%s", host, port.HostStart+offset, port.ContainerStart+offset, port.Protocol)
|
|
args = append(args, "--publish", mapping)
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, volume := range req.Volumes {
|
|
args = append(args, "--volume", mountSpec(volume.Host, volume.Container, volume.Mode))
|
|
}
|
|
for _, env := range req.Env {
|
|
args = append(args, "--env", env.Key+"="+env.Value)
|
|
}
|
|
for _, label := range req.Labels {
|
|
args = append(args, "--label", label.Key+"="+label.Value)
|
|
}
|
|
|
|
if len(req.Entrypoint) > 0 {
|
|
args = append(args, "--entrypoint", req.Entrypoint[0])
|
|
}
|
|
if req.RestartPolicy != "" {
|
|
restartPolicy := req.RestartPolicy
|
|
if restartPolicy == "on-failure" {
|
|
restartPolicy += ":5"
|
|
}
|
|
args = append(args, "--restart", restartPolicy)
|
|
}
|
|
if req.AutoRemove {
|
|
args = append(args, "--rm")
|
|
}
|
|
if req.Privileged {
|
|
args = append(args, "--privileged")
|
|
}
|
|
if req.OpenStdin {
|
|
args = append(args, "--interactive")
|
|
}
|
|
if req.Tty {
|
|
args = append(args, "--tty")
|
|
}
|
|
if req.CPUShares > 0 {
|
|
args = append(args, "--cpu-shares", strconv.FormatInt(req.CPUShares, 10))
|
|
}
|
|
if req.CPUs > 0 {
|
|
args = append(args, "--cpus", strconv.FormatFloat(req.CPUs, 'f', -1, 64))
|
|
}
|
|
if req.Memory > 0 {
|
|
args = append(args, "--memory", strconv.FormatInt(req.Memory, 10)+"m")
|
|
}
|
|
|
|
args = append(args, req.Image)
|
|
if len(req.Entrypoint) > 1 {
|
|
args = append(args, req.Entrypoint[1:]...)
|
|
}
|
|
args = append(args, req.Command...)
|
|
|
|
return Command(sock, args...), nil
|
|
}
|
|
|
|
// mountSpec 拼接 docker 的 源:目标[:选项] 形式,
|
|
// 选项为空时不能留下尾随冒号,否则 docker 报 empty section between colons
|
|
func mountSpec(source, target, option string) string {
|
|
spec := source + ":" + target
|
|
if option != "" {
|
|
spec += ":" + option
|
|
}
|
|
return spec
|
|
}
|
|
|
|
// Command 拼接指向指定套接字的 docker 命令,各参数均做转义
|
|
func Command(sock string, args ...string) string {
|
|
args = append([]string{"docker", "--host", sock}, args...)
|
|
for i := range args {
|
|
args[i] = shell.Quote(args[i])
|
|
}
|
|
return strings.Join(args, " ")
|
|
}
|