diff --git a/go.mod b/go.mod index 5df3bb6221..bce6e43773 100644 --- a/go.mod +++ b/go.mod @@ -51,6 +51,7 @@ require ( require ( filippo.io/edwards25519 v1.1.0 // indirect + github.com/creack/pty v1.1.23 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/devhaozi/huaweicloud-sdk-go-v3 v0.0.0-20241018211007-bbebb6de5db7 // indirect github.com/dustin/go-humanize v1.0.1 // indirect diff --git a/go.sum b/go.sum index 1f1dee4849..1cf809add3 100644 --- a/go.sum +++ b/go.sum @@ -6,6 +6,8 @@ github.com/bddjr/hlfhr v1.1.3 h1:O1Vxi7Qf0tOs9oGoSb3Q9KAGTSfh/EXyVMVSO9V1m90= github.com/bddjr/hlfhr v1.1.3/go.mod h1:oyIv4Q9JpCgZFdtH3KyTNWp7YYRWl4zl8k4ozrMAB4g= github.com/beevik/ntp v1.4.3 h1:PlbTvE5NNy4QHmA4Mg57n7mcFTmr1W1j3gcK7L1lqho= github.com/beevik/ntp v1.4.3/go.mod h1:Unr8Zg+2dRn7d8bHFuehIMSvvUYssHMxW3Q5Nx4RW5Q= +github.com/creack/pty v1.1.23 h1:4M6+isWdcStXEf15G/RbrMPOQj1dZ7HPZCGwE4kOeP0= +github.com/creack/pty v1.1.23/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= diff --git a/internal/apps/docker/init.go b/internal/apps/docker/init.go index 111407ce3a..6b04d9d92c 100644 --- a/internal/apps/docker/init.go +++ b/internal/apps/docker/init.go @@ -1,4 +1,4 @@ -package podman +package docker import ( "github.com/go-chi/chi/v5" diff --git a/internal/apps/docker/request.go b/internal/apps/docker/request.go index 0b3ba9ea08..33a2f2d562 100644 --- a/internal/apps/docker/request.go +++ b/internal/apps/docker/request.go @@ -1,4 +1,4 @@ -package podman +package docker type UpdateConfig struct { Config string `form:"config" json:"config" validate:"required"` diff --git a/internal/apps/docker/service.go b/internal/apps/docker/service.go index cebb68221d..ce3121a8cd 100644 --- a/internal/apps/docker/service.go +++ b/internal/apps/docker/service.go @@ -1,4 +1,4 @@ -package podman +package docker import ( "net/http" diff --git a/internal/data/container.go b/internal/data/container.go index 3a877f7f6c..6b59d14b3a 100644 --- a/internal/data/container.go +++ b/internal/data/container.go @@ -62,9 +62,12 @@ func (r *containerRepo) ListAll() ([]types.Container, error) { Host: port.IP, }) } + if len(item.Names) == 0 { + item.Names = append(item.Names, "") + } containers = append(containers, types.Container{ ID: item.ID, - Name: item.Names[0], + Name: strings.TrimPrefix(item.Names[0], "/"), // https://github.com/moby/moby/issues/7519 Image: item.Image, ImageID: item.ImageID, Command: item.Command, @@ -96,7 +99,7 @@ func (r *containerRepo) ListByName(names string) ([]types.Container, error) { // Create 创建容器 func (r *containerRepo) Create(req *request.ContainerCreate) (string, error) { var sb strings.Builder - sb.WriteString(fmt.Sprintf("docker run --name %s", req.Name)) + sb.WriteString(fmt.Sprintf("docker run -d --name %s", req.Name)) if req.PublishAllPorts { sb.WriteString(" -P") } else { @@ -155,65 +158,65 @@ func (r *containerRepo) Create(req *request.ContainerCreate) (string, error) { sb.WriteString(fmt.Sprintf(" --memory %d", req.Memory)) } - sb.WriteString(" %s") - return shell.Execf(sb.String(), req.Image) + sb.WriteString(" %s bash") + return shell.ExecfWithTTY(sb.String(), req.Image) } // Remove 移除容器 func (r *containerRepo) Remove(id string) error { - _, err := shell.ExecfWithTimeout(30*time.Second, "docker rm -f %s", id) + _, err := shell.ExecfWithTimeout(120*time.Second, "docker rm -f %s", id) return err } // Start 启动容器 func (r *containerRepo) Start(id string) error { - _, err := shell.ExecfWithTimeout(30*time.Second, "docker start %s", id) + _, err := shell.ExecfWithTimeout(120*time.Second, "docker start %s", id) return err } // Stop 停止容器 func (r *containerRepo) Stop(id string) error { - _, err := shell.ExecfWithTimeout(30*time.Second, "docker stop %s", id) + _, err := shell.ExecfWithTimeout(120*time.Second, "docker stop %s", id) return err } // Restart 重启容器 func (r *containerRepo) Restart(id string) error { - _, err := shell.ExecfWithTimeout(30*time.Second, "docker restart %s", id) + _, err := shell.ExecfWithTimeout(120*time.Second, "docker restart %s", id) return err } // Pause 暂停容器 func (r *containerRepo) Pause(id string) error { - _, err := shell.ExecfWithTimeout(30*time.Second, "docker pause %s", id) + _, err := shell.ExecfWithTimeout(120*time.Second, "docker pause %s", id) return err } // Unpause 恢复容器 func (r *containerRepo) Unpause(id string) error { - _, err := shell.ExecfWithTimeout(30*time.Second, "docker unpause %s", id) + _, err := shell.ExecfWithTimeout(120*time.Second, "docker unpause %s", id) return err } // Kill 杀死容器 func (r *containerRepo) Kill(id string) error { - _, err := shell.ExecfWithTimeout(30*time.Second, "docker kill %s", id) + _, err := shell.ExecfWithTimeout(120*time.Second, "docker kill %s", id) return err } // Rename 重命名容器 func (r *containerRepo) Rename(id string, newName string) error { - _, err := shell.ExecfWithTimeout(30*time.Second, "docker rename %s %s", id, newName) + _, err := shell.ExecfWithTimeout(120*time.Second, "docker rename %s %s", id, newName) return err } // Logs 查看容器日志 func (r *containerRepo) Logs(id string) (string, error) { - return shell.ExecfWithTimeout(30*time.Second, "docker logs %s", id) + return shell.ExecfWithTimeout(120*time.Second, "docker logs %s", id) } // Prune 清理未使用的容器 func (r *containerRepo) Prune() error { - _, err := shell.ExecfWithTimeout(30*time.Second, "docker container prune -f") + _, err := shell.ExecfWithTimeout(120*time.Second, "docker container prune -f") return err } diff --git a/internal/data/container_image.go b/internal/data/container_image.go index 8f6de5a811..d35f2f3ffc 100644 --- a/internal/data/container_image.go +++ b/internal/data/container_image.go @@ -80,7 +80,7 @@ func (r *containerImageRepo) Pull(req *request.ContainerImagePull) error { sb.WriteString(fmt.Sprintf("docker pull %s", req.Name)) if _, err := shell.Execf(sb.String()); err != nil { // nolint: govet - return fmt.Errorf("pull failed: %w", err) + return err } return nil @@ -88,12 +88,12 @@ func (r *containerImageRepo) Pull(req *request.ContainerImagePull) error { // Remove 删除镜像 func (r *containerImageRepo) Remove(id string) error { - _, err := shell.ExecfWithTimeout(30*time.Second, "docker rmi %s", id) + _, err := shell.ExecfWithTimeout(120*time.Second, "docker rmi %s", id) return err } // Prune 清理未使用的镜像 func (r *containerImageRepo) Prune() error { - _, err := shell.ExecfWithTimeout(30*time.Second, "docker image prune -f") + _, err := shell.ExecfWithTimeout(120*time.Second, "docker image prune -f") return err } diff --git a/internal/data/container_network.go b/internal/data/container_network.go index c06757a508..c1d41d0e38 100644 --- a/internal/data/container_network.go +++ b/internal/data/container_network.go @@ -110,17 +110,17 @@ func (r *containerNetworkRepo) Create(req *request.ContainerNetworkCreate) (stri sb.WriteString(fmt.Sprintf(" --opt %s=%s", option.Key, option.Value)) } - return shell.ExecfWithTimeout(30*time.Second, sb.String()) // nolint: govet + return shell.ExecfWithTimeout(120*time.Second, sb.String()) // nolint: govet } // Remove 删除网络 func (r *containerNetworkRepo) Remove(id string) error { - _, err := shell.ExecfWithTimeout(30*time.Second, "docker network rm -f %s", id) + _, err := shell.ExecfWithTimeout(120*time.Second, "docker network rm -f %s", id) return err } // Prune 清理未使用的网络 func (r *containerNetworkRepo) Prune() error { - _, err := shell.ExecfWithTimeout(30*time.Second, "docker network prune -f") + _, err := shell.ExecfWithTimeout(120*time.Second, "docker network prune -f") return err } diff --git a/internal/data/container_volume.go b/internal/data/container_volume.go index bee8bcbe31..c09da9d909 100644 --- a/internal/data/container_volume.go +++ b/internal/data/container_volume.go @@ -84,17 +84,17 @@ func (r *containerVolumeRepo) Create(req *request.ContainerVolumeCreate) (string sb.WriteString(fmt.Sprintf(" --opt %s=%s", option.Key, option.Value)) } - return shell.ExecfWithTimeout(30*time.Second, sb.String()) // nolint: govet + return shell.ExecfWithTimeout(120*time.Second, sb.String()) // nolint: govet } // Remove 删除存储卷 func (r *containerVolumeRepo) Remove(id string) error { - _, err := shell.ExecfWithTimeout(30*time.Second, "docker volume rm -f %s", id) + _, err := shell.ExecfWithTimeout(120*time.Second, "docker volume rm -f %s", id) return err } // Prune 清理未使用的存储卷 func (r *containerVolumeRepo) Prune() error { - _, err := shell.ExecfWithTimeout(30*time.Second, "docker volume prune -f") + _, err := shell.ExecfWithTimeout(120*time.Second, "docker volume prune -f") return err } diff --git a/pkg/firewall/firewall.go b/pkg/firewall/firewall.go index b2c75cf031..8cf99beadd 100644 --- a/pkg/firewall/firewall.go +++ b/pkg/firewall/firewall.go @@ -197,9 +197,8 @@ func (r *Firewall) Port(rule FireInfo, operation Operation) error { } protocols := strings.Split(string(rule.Protocol), "/") for protocol := range slices.Values(protocols) { - stdout, err := shell.Execf("firewall-cmd --zone=public --%s-port=%d-%d/%s --permanent", operation, rule.PortStart, rule.PortEnd, protocol) - if err != nil { - return fmt.Errorf("%s port %d-%d/%s failed, err: %s", operation, rule.PortStart, rule.PortEnd, protocol, stdout) + if _, err := shell.Execf("firewall-cmd --zone=public --%s-port=%d-%d/%s --permanent", operation, rule.PortStart, rule.PortEnd, protocol); err != nil { + return err } } @@ -243,7 +242,7 @@ func (r *Firewall) RichRules(rule FireInfo, operation Operation) error { ruleBuilder.WriteString(string(rule.Strategy)) _, err := shell.Execf("firewall-cmd --zone=public --%s-rich-rule '%s' --permanent", operation, ruleBuilder.String()) if err != nil { - return fmt.Errorf("%s rich rules (%s) failed, err: %v", operation, ruleBuilder.String(), err) + return err } } @@ -269,7 +268,7 @@ func (r *Firewall) Forward(rule Forward, operation Operation) error { _, err := shell.Execf(ruleBuilder.String()) // nolint: govet if err != nil { - return fmt.Errorf("%s port forward failed, err: %v", operation, err) + return err } } diff --git a/pkg/shell/exec.go b/pkg/shell/exec.go index cd75ef0f63..dad62c3843 100644 --- a/pkg/shell/exec.go +++ b/pkg/shell/exec.go @@ -10,7 +10,10 @@ import ( "os/exec" "slices" "strings" + "syscall" "time" + + "github.com/creack/pty" ) // Execf 执行 shell 命令 @@ -26,12 +29,11 @@ func Execf(shell string, args ...any) (string, error) { cmd.Stdout = &stdout cmd.Stderr = &stderr - err := cmd.Run() - if err != nil { - return strings.TrimSpace(stdout.String()), errors.New(strings.TrimSpace(stderr.String())) + if err := cmd.Run(); err != nil { + return strings.TrimSpace(stdout.String()), fmt.Errorf("run %s failed, err: %s", fmt.Sprintf(shell, args...), strings.TrimSpace(stderr.String())) } - return strings.TrimSpace(stdout.String()), err + return strings.TrimSpace(stdout.String()), nil } // ExecfAsync 异步执行 shell 命令 @@ -50,7 +52,7 @@ func ExecfAsync(shell string, args ...any) error { go func() { if err = cmd.Wait(); err != nil { - fmt.Println(err.Error()) + fmt.Println(fmt.Errorf("run %s failed, err: %s", fmt.Sprintf(shell, args...), strings.TrimSpace(err.Error()))) } }() @@ -72,7 +74,7 @@ func ExecfWithTimeout(timeout time.Duration, shell string, args ...any) (string, err := cmd.Start() if err != nil { - return "", err + return strings.TrimSpace(stdout.String()), fmt.Errorf("run %s failed, err: %s", fmt.Sprintf(shell, args...), strings.TrimSpace(stderr.String())) } done := make(chan error) @@ -83,10 +85,10 @@ func ExecfWithTimeout(timeout time.Duration, shell string, args ...any) (string, select { case <-time.After(timeout): _ = cmd.Process.Kill() - return strings.TrimSpace(stdout.String()), errors.New("执行超时") + return strings.TrimSpace(stdout.String()), fmt.Errorf("run %s failed, err: %s", fmt.Sprintf(shell, args...), "timeout") case err = <-done: if err != nil { - return strings.TrimSpace(stdout.String()), errors.New(strings.TrimSpace(stderr.String())) + return strings.TrimSpace(stdout.String()), fmt.Errorf("run %s failed, err: %s", fmt.Sprintf(shell, args...), strings.TrimSpace(stderr.String())) } } @@ -140,12 +142,40 @@ func ExecfWithDir(dir, shell string, args ...any) (string, error) { cmd.Stdout = &stdout cmd.Stderr = &stderr - err := cmd.Run() + if err := cmd.Run(); err != nil { + return strings.TrimSpace(stdout.String()), fmt.Errorf("run %s failed, err: %s", fmt.Sprintf(shell, args...), strings.TrimSpace(stderr.String())) + } + + return strings.TrimSpace(stdout.String()), nil +} + +// ExecfWithTTY 在伪终端下执行 shell 命令 +func ExecfWithTTY(shell string, args ...any) (string, error) { + if !preCheckArg(args) { + return "", errors.New("command contains illegal characters") + } + + _ = os.Setenv("LC_ALL", "C") + cmd := exec.Command("bash", "-i", "-c", fmt.Sprintf(shell, args...)) + + var out bytes.Buffer + var stderr bytes.Buffer + cmd.Stderr = &stderr // https://github.com/creack/pty/issues/147 取 stderr + + f, err := pty.Start(cmd) if err != nil { - return strings.TrimSpace(stdout.String()), errors.New(strings.TrimSpace(stderr.String())) + return "", fmt.Errorf("run %s failed", fmt.Sprintf(shell, args...)) } + defer f.Close() - return strings.TrimSpace(stdout.String()), err + if _, err = io.Copy(&out, f); ptyError(err) != nil { + return "", fmt.Errorf("run %s failed, out: %s, err: %w", fmt.Sprintf(shell, args...), strings.TrimSpace(out.String()), err) + } + if stderr.Len() > 0 { + return "", fmt.Errorf("run %s failed, out: %s", fmt.Sprintf(shell, args...), strings.TrimSpace(stderr.String())) + } + + return strings.TrimSpace(out.String()), nil } func preCheckArg(args []any) bool { @@ -158,3 +188,15 @@ func preCheckArg(args []any) bool { return true } + +// Linux kernel return EIO when attempting to read from a master pseudo +// terminal which no longer has an open slave. So ignore error here. +// See https://github.com/creack/pty/issues/21 +func ptyError(err error) error { + var pathErr *os.PathError + if !errors.As(err, &pathErr) || !errors.Is(pathErr.Err, syscall.EIO) { + return err + } + + return nil +} diff --git a/web/src/router/guard/app-install-guard.ts b/web/src/router/guard/app-install-guard.ts index 8073b2ab84..e16b421aa6 100644 --- a/web/src/router/guard/app-install-guard.ts +++ b/web/src/router/guard/app-install-guard.ts @@ -24,12 +24,23 @@ export function createAppInstallGuard(router: Router) { } // 容器 if (to.path.startsWith('/container')) { - await app.isInstalled('podman').then((res) => { - if (!res.data.installed) { - window.$message.error(`容器引擎 ${res.data.name} 未安装`) - return router.push({ name: 'app-index' }) + let flag = false + await app.isInstalled('docker').then((res) => { + if (res.data.installed) { + flag = true } }) + if (!flag) { + await app.isInstalled('podman').then((res) => { + if (res.data.installed) { + flag = true + } + }) + } + if (!flag) { + window.$message.error(`容器引擎 Docker / Podman 未安装`) + return router.push({ name: 'app-index' }) + } } }) } diff --git a/web/src/views/container/ImageView.vue b/web/src/views/container/ImageView.vue index fcd1e69238..d51c343120 100644 --- a/web/src/views/container/ImageView.vue +++ b/web/src/views/container/ImageView.vue @@ -26,7 +26,7 @@ const columns: any = [ { title: 'ID', key: 'id', - width: 150, + minWidth: 400, resizable: true, ellipsis: { tooltip: true } }, @@ -40,7 +40,7 @@ const columns: any = [ { title: '镜像', key: 'repo_tags', - minWidth: 300, + minWidth: 200, resizable: true, ellipsis: { tooltip: true }, render(row: any): any {