Files
devstar/services/devcontainer/docker_agent.go

635 lines
19 KiB
Go
Raw Normal View History

2025-08-16 18:31:14 +08:00
package devcontainer
import (
"archive/tar"
"bytes"
"context"
"fmt"
"io"
2025-08-21 21:31:51 +08:00
"os/exec"
2025-08-16 18:31:14 +08:00
"regexp"
"strings"
2025-08-29 16:08:02 +08:00
"time"
2025-08-16 18:31:14 +08:00
"code.gitea.io/gitea/models/db"
devcontainer_models "code.gitea.io/gitea/models/devcontainer"
"code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/user"
docker_module "code.gitea.io/gitea/modules/docker"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
2025-09-18 20:14:43 +08:00
"github.com/docker/docker/client"
2025-08-16 18:31:14 +08:00
"github.com/docker/docker/errdefs"
2025-08-21 21:31:51 +08:00
"github.com/docker/go-connections/nat"
2025-08-16 18:31:14 +08:00
)
func GetDevContainerStatusFromDocker(ctx context.Context, containerName string) (string, error) {
// 创建docker client
cli, err := docker_module.CreateDockerClient(ctx)
if err != nil {
return "", err
}
defer cli.Close()
containerID, err := docker_module.GetContainerID(cli, containerName)
if err != nil {
return "", err
}
containerStatus, err := docker_module.GetContainerStatus(cli, containerID)
if err != nil {
return "", err
}
return containerStatus, nil
}
2025-09-17 11:05:42 +08:00
func CreateDevContainerByDockerAPI(ctx context.Context, newDevcontainer *devcontainer_models.Devcontainer, imageName string, repo *repo.Repository, publicKeyList []string) error {
2025-08-16 18:31:14 +08:00
dbEngine := db.GetEngine(ctx)
configurationString, err := GetDevcontainerConfigurationString(ctx, repo)
if err != nil {
return err
}
configurationModel, err := UnmarshalDevcontainerConfigContent(configurationString)
if err != nil {
return err
}
dockerSocket, err := docker_module.GetDockerSocketPath()
if err != nil {
return err
}
2025-08-21 21:31:51 +08:00
_, err = dbEngine.Table("devcontainer").
Where("user_id = ? AND repo_id = ? ", newDevcontainer.UserId, newDevcontainer.RepoId).
Update(&devcontainer_models.Devcontainer{DevcontainerStatus: 1})
if err != nil {
log.Info("err %v", err)
}
cli, err := docker_module.CreateDockerClient(ctx)
if err != nil {
return err
}
if configurationModel.Build == nil || configurationModel.Build.Dockerfile == "" {
script := "docker " + "-H " + dockerSocket + " pull " + configurationModel.Image
cmd := exec.Command("sh", "-c", script)
err = cmd.Start()
if err != nil {
2025-08-16 18:31:14 +08:00
return err
}
2025-08-21 21:31:51 +08:00
}
_, err = dbEngine.Table("devcontainer").
Where("user_id = ? AND repo_id = ? ", newDevcontainer.UserId, newDevcontainer.RepoId).
Update(&devcontainer_models.Devcontainer{DevcontainerStatus: 2})
if err != nil {
return err
}
docker_module.CreateAndStartContainer(ctx, cli, imageName,
[]string{
"sh",
"-c",
"tail -f /dev/null;",
},
nil,
nil,
nat.PortSet{
2025-09-18 19:25:17 +08:00
nat.Port("22/tcp"): {},
2025-08-21 21:31:51 +08:00
},
newDevcontainer.Name)
_, err = dbEngine.Table("devcontainer").
Where("user_id = ? AND repo_id = ? ", newDevcontainer.UserId, newDevcontainer.RepoId).
Update(&devcontainer_models.Devcontainer{DevcontainerStatus: 3})
if err != nil {
return err
}
2025-10-12 23:06:34 +08:00
2025-08-21 21:31:51 +08:00
output, err := docker_module.ExecCommandInContainer(ctx, cli, newDevcontainer.Name,
2025-09-18 19:25:17 +08:00
`echo "`+newDevcontainer.DevcontainerHost+` host.docker.internal" | tee -a /etc/hosts;apt update;apt install -y git ;git clone `+strings.TrimSuffix(setting.AppURL, "/")+repo.Link()+" "+newDevcontainer.DevcontainerWorkDir+"/"+repo.Name+`; apt install -y ssh;echo "PubkeyAuthentication yes `+"\n"+`PermitRootLogin yes `+"\n"+`" | tee -a /etc/ssh/sshd_config;rm -f /etc/ssh/ssh_host_*; ssh-keygen -A; service ssh restart;mkdir -p ~/.ssh;chmod 700 ~/.ssh;echo "`+strings.Join(publicKeyList, "\n")+`" > ~/.ssh/authorized_keys;chmod 600 ~/.ssh/authorized_keys;`,
2025-08-21 21:31:51 +08:00
)
if err != nil {
return err
}
log.Info(output)
_, err = dbEngine.Table("devcontainer").
Where("user_id = ? AND repo_id = ? ", newDevcontainer.UserId, newDevcontainer.RepoId).
Update(&devcontainer_models.Devcontainer{DevcontainerStatus: 4})
if err != nil {
return err
}
return nil
}
2025-09-17 11:05:42 +08:00
func CreateDevContainerByDockerCommand(ctx context.Context, newDevcontainer *devcontainer_models.Devcontainer, repo *repo.Repository, publicKeyList []string) (string, error) {
2025-08-21 21:31:51 +08:00
dbEngine := db.GetEngine(ctx)
configurationString, err := GetDevcontainerConfigurationString(ctx, repo)
if err != nil {
2025-09-17 11:05:42 +08:00
return "", err
2025-08-21 21:31:51 +08:00
}
configurationModel, err := UnmarshalDevcontainerConfigContent(configurationString)
if err != nil {
2025-09-17 11:05:42 +08:00
return "", err
2025-08-21 21:31:51 +08:00
}
var imageName = configurationModel.Image
dockerSocket, err := docker_module.GetDockerSocketPath()
if err != nil {
2025-09-17 11:05:42 +08:00
return "", err
2025-08-21 21:31:51 +08:00
}
if configurationModel.Build != nil && configurationModel.Build.Dockerfile != "" {
2025-08-16 18:31:14 +08:00
dockerfileContent, err := GetFileContentByPath(ctx, repo, ".devcontainer/"+configurationModel.Build.Dockerfile)
if err != nil {
2025-09-17 11:05:42 +08:00
return "", err
2025-08-16 18:31:14 +08:00
}
// 创建构建上下文包含Dockerfile的tar包
var buf bytes.Buffer
tw := tar.NewWriter(&buf)
defer tw.Close()
// 添加Dockerfile到tar包
dockerfile := "Dockerfile"
content := []byte(dockerfileContent)
header := &tar.Header{
Name: dockerfile,
Size: int64(len(content)),
Mode: 0644,
}
if err := tw.WriteHeader(header); err != nil {
2025-09-17 11:05:42 +08:00
return "", err
2025-08-16 18:31:14 +08:00
}
if _, err := tw.Write(content); err != nil {
2025-09-17 11:05:42 +08:00
return "", err
2025-08-16 18:31:14 +08:00
}
// 执行镜像构建
2025-08-21 21:31:51 +08:00
imageName = fmt.Sprintf("%d", newDevcontainer.UserId) + "-" + fmt.Sprintf("%d", newDevcontainer.RepoId) + "-dockerfile"
2025-08-16 18:31:14 +08:00
buildOptions := types.ImageBuildOptions{
Tags: []string{imageName}, // 镜像标签
}
cli, err := docker_module.CreateDockerClient(ctx)
if err != nil {
2025-09-17 11:05:42 +08:00
return "", err
2025-08-16 18:31:14 +08:00
}
buildResponse, err := cli.ImageBuild(
context.Background(),
&buf,
buildOptions,
)
if err != nil {
2025-09-17 11:05:42 +08:00
return "", err
2025-08-16 18:31:14 +08:00
}
output, err := io.ReadAll(buildResponse.Body)
if err != nil {
2025-09-17 11:05:42 +08:00
return "", err
2025-08-16 18:31:14 +08:00
}
log.Info(string(output))
2025-08-21 21:31:51 +08:00
}
// 拉取镜像的命令
if _, err := dbEngine.Table("devcontainer_output").Insert(&devcontainer_models.DevcontainerOutput{
Output: "",
ListId: 1,
Status: "waitting",
UserId: newDevcontainer.UserId,
RepoId: newDevcontainer.RepoId,
Command: "docker " + "-H " + dockerSocket + " pull " + imageName + "\n",
DevcontainerId: newDevcontainer.Id,
}); err != nil {
log.Info("Failed to insert record: %v", err)
2025-09-17 11:05:42 +08:00
return imageName, err
2025-08-21 21:31:51 +08:00
}
2025-09-04 10:48:46 +08:00
cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf)
if err != nil {
2025-09-17 11:05:42 +08:00
return imageName, err
2025-09-04 10:48:46 +08:00
}
2025-10-16 23:41:01 +08:00
var startCommand string = `docker -H ` + dockerSocket + ` create --restart=always --name ` + newDevcontainer.Name
2025-09-13 17:00:58 +08:00
// 将每个端口转换为 "-p <port>" 格式
var portFlags string = " -p 22 "
exposedPorts := configurationModel.ExtractContainerPorts()
for _, port := range exposedPorts {
portFlags = portFlags + fmt.Sprintf(" -p %d ", port)
}
startCommand += portFlags
2025-10-12 23:06:34 +08:00
2025-09-13 17:00:58 +08:00
var envFlags string = ` -e RepoLink="` + strings.TrimSuffix(cfg.Section("server").Key("ROOT_URL").Value(), `/`) + repo.Link() + `" ` +
2025-09-29 20:13:44 +08:00
` -e DevstarHost="` + newDevcontainer.DevcontainerHost + `"` +
2025-10-16 23:41:01 +08:00
` -e WorkSpace="` + newDevcontainer.DevcontainerWorkDir + `/` + repo.Name + `" ` +
` -e DEVCONTAINER_STATUS="start" `
2025-09-13 17:00:58 +08:00
// 遍历 ContainerEnv 映射中的每个环境变量
for name, value := range configurationModel.ContainerEnv {
// 将每个环境变量转换为 "-e name=value" 格式
envFlags = envFlags + fmt.Sprintf(" -e %s=\"%s\" ", name, value)
}
startCommand += envFlags
if configurationModel.Init {
startCommand += " --init "
}
if configurationModel.Privileged {
startCommand += " --privileged "
}
var capAddFlags string
// 遍历 CapAdd 列表中的每个能力
for _, capability := range configurationModel.CapAdd {
// 将每个能力转换为 --cap-add=capability 格式
capAddFlags = capAddFlags + fmt.Sprintf(" --cap-add %s ", capability)
}
2025-10-18 15:03:56 +08:00
startCommand += capAddFlags
2025-09-13 17:00:58 +08:00
var securityOptFlags string
// 遍历 SecurityOpt 列表中的每个安全选项
for _, option := range configurationModel.SecurityOpt {
// 将每个选项转换为 --security-opt=option 格式
securityOptFlags = securityOptFlags + fmt.Sprintf(" --security-opt %s ", option)
}
startCommand += securityOptFlags
startCommand += " " + strings.Join(configurationModel.ExtractMountFlags(), " ") + " "
if configurationModel.WorkspaceFolder != "" {
startCommand += fmt.Sprintf(" -w %s ", configurationModel.WorkspaceFolder)
}
startCommand += " " + strings.Join(configurationModel.RunArgs, " ") + " "
2025-10-12 23:06:34 +08:00
overrideCommand := ""
if !configurationModel.OverrideCommand {
2025-10-16 23:41:01 +08:00
overrideCommand = ` sh -c "/home/webTerminal.sh" `
2025-10-12 23:06:34 +08:00
startCommand += ` --entrypoint="" `
}
2025-08-21 21:31:51 +08:00
//创建并运行容器的命令
if _, err := dbEngine.Table("devcontainer_output").Insert(&devcontainer_models.DevcontainerOutput{
2025-09-13 17:00:58 +08:00
Output: "",
Status: "waitting",
UserId: newDevcontainer.UserId,
RepoId: newDevcontainer.RepoId,
2025-10-12 23:06:34 +08:00
Command: startCommand + imageName + overrideCommand + "\n",
2025-08-21 21:31:51 +08:00
ListId: 2,
DevcontainerId: newDevcontainer.Id,
}); err != nil {
log.Info("Failed to insert record: %v", err)
2025-09-17 11:05:42 +08:00
return imageName, err
2025-08-21 21:31:51 +08:00
}
2025-10-12 23:06:34 +08:00
2025-08-21 21:31:51 +08:00
if _, err := dbEngine.Table("devcontainer_output").Insert(&devcontainer_models.DevcontainerOutput{
2025-10-12 23:06:34 +08:00
Output: "",
Status: "waitting",
UserId: newDevcontainer.UserId,
RepoId: newDevcontainer.RepoId,
2025-10-16 23:41:01 +08:00
Command: `docker -H ` + dockerSocket + ` start -a ` + newDevcontainer.Name + "\n",
2025-08-21 21:31:51 +08:00
ListId: 3,
DevcontainerId: newDevcontainer.Id,
}); err != nil {
log.Info("Failed to insert record: %v", err)
2025-09-17 11:05:42 +08:00
return imageName, err
2025-08-21 21:31:51 +08:00
}
//连接容器的命令
if _, err := dbEngine.Table("devcontainer_output").Insert(&devcontainer_models.DevcontainerOutput{
Output: "",
Status: "waitting",
UserId: newDevcontainer.UserId,
RepoId: newDevcontainer.RepoId,
2025-10-19 01:10:47 +08:00
Command: `docker -H ` + dockerSocket + ` exec -it --workdir ` + newDevcontainer.DevcontainerWorkDir + "/" + repo.Name + ` ` + newDevcontainer.Name + ` sh -c "echo '$WEB_TERMINAL_HELLO';bash"` + "\n",
2025-08-21 21:31:51 +08:00
ListId: 4,
DevcontainerId: newDevcontainer.Id,
}); err != nil {
log.Info("Failed to insert record: %v", err)
2025-09-17 11:05:42 +08:00
return imageName, err
2025-08-16 18:31:14 +08:00
}
2025-09-17 11:05:42 +08:00
return imageName, nil
2025-08-16 18:31:14 +08:00
}
func IsContainerNotFound(ctx context.Context, containerName string) (bool, error) {
cli, err := docker_module.CreateDockerClient(ctx)
if err != nil {
return false, err
}
defer cli.Close()
containerID, err := docker_module.GetContainerID(cli, containerName)
if err != nil {
// 检查是否为 "未找到" 错误
if errdefs.IsNotFound(err) {
return true, nil
}
return false, err
}
_, err = cli.ContainerInspect(ctx, containerID)
if err != nil {
// 检查是否为 "未找到" 错误
if docker_module.IsContainerNotFound(err) {
return true, nil
}
// 其他类型的错误
return false, err
}
// 无错误表示容器存在
return false, nil
}
2025-09-19 12:50:54 +08:00
func DeleteDevContainerByDocker(ctx context.Context, devContainerName string) error {
2025-08-16 18:31:14 +08:00
// 创建docker client
cli, err := docker_module.CreateDockerClient(ctx)
if err != nil {
return err
}
defer cli.Close()
// 获取容器 ID
2025-09-19 12:50:54 +08:00
containerID, err := docker_module.GetContainerID(cli, devContainerName)
2025-08-16 18:31:14 +08:00
if err != nil {
if errdefs.IsNotFound(err) {
return nil
}
return err
}
// 删除容器
if err := docker_module.DeleteContainer(ctx, cli, containerID); err != nil {
return err
}
return nil
}
2025-09-19 12:50:54 +08:00
func RestartDevContainerByDocker(ctx context.Context, devContainerName string) error {
2025-08-16 18:31:14 +08:00
// 创建docker client
cli, err := docker_module.CreateDockerClient(ctx)
if err != nil {
return err
}
defer cli.Close()
// 获取容器 ID
2025-09-19 12:50:54 +08:00
containerID, err := docker_module.GetContainerID(cli, devContainerName)
2025-08-16 18:31:14 +08:00
if err != nil {
return err
}
// restart容器
timeout := 10 // 超时时间(秒)
err = cli.ContainerRestart(context.Background(), containerID, container.StopOptions{
Timeout: &timeout,
})
if err != nil {
return err
} else {
log.Info("容器已重启")
}
return nil
}
2025-09-29 20:13:44 +08:00
func StopDevContainerByDocker(ctx context.Context, devContainerName string) error {
2025-08-16 18:31:14 +08:00
// 创建docker client
cli, err := docker_module.CreateDockerClient(ctx)
if err != nil {
return err
}
defer cli.Close()
2025-09-29 20:13:44 +08:00
// 获取容器 ID
containerID, err := docker_module.GetContainerID(cli, devContainerName)
2025-08-16 18:31:14 +08:00
if err != nil {
return err
}
// stop容器
timeout := 10 // 超时时间(秒)
err = cli.ContainerStop(context.Background(), containerID, container.StopOptions{
Timeout: &timeout,
})
if err != nil {
return err
} else {
log.Info("容器已停止")
}
return nil
}
func UpdateDevContainerByDocker(ctx context.Context, devContainerInfo *devcontainer_models.Devcontainer, updateInfo *UpdateInfo, repo *repo.Repository, doer *user.User) error {
// 创建docker client
cli, err := docker_module.CreateDockerClient(ctx)
if err != nil {
return err
}
defer cli.Close()
// update容器
imageRef := updateInfo.RepositoryAddress + "/" + updateInfo.RepositoryUsername + "/" + updateInfo.ImageName
configurationString, err := GetDevcontainerConfigurationString(ctx, repo)
if err != nil {
return err
}
configurationModel, err := UnmarshalDevcontainerConfigContent(configurationString)
if err != nil {
return err
}
if updateInfo.SaveMethod == "on" {
// 创建构建上下文包含Dockerfile的tar包
var buf bytes.Buffer
tw := tar.NewWriter(&buf)
defer tw.Close()
// 添加Dockerfile到tar包
dockerfile := "Dockerfile"
dockerfileContent, err := GetFileContentByPath(ctx, repo, ".devcontainer/"+configurationModel.Build.Dockerfile)
if err != nil {
return err
}
content := []byte(dockerfileContent)
header := &tar.Header{
Name: dockerfile,
Size: int64(len(content)),
Mode: 0644,
}
if err := tw.WriteHeader(header); err != nil {
return err
}
if _, err := tw.Write(content); err != nil {
return err
}
buildOptions := types.ImageBuildOptions{
Tags: []string{imageRef}, // 镜像标签
}
_, err = cli.ImageBuild(
context.Background(),
&buf,
buildOptions,
)
if err != nil {
log.Info(err.Error())
return err
}
} else {
// 获取容器 ID
containerID, err := docker_module.GetContainerID(cli, devContainerInfo.Name)
if err != nil {
return err
}
// 提交容器
_, err = cli.ContainerCommit(ctx, containerID, types.ContainerCommitOptions{Reference: imageRef})
if err != nil {
return err
}
}
// 推送到仓库
dockerHost, err := docker_module.GetDockerSocketPath()
if err != nil {
return err
}
err = docker_module.PushImage(dockerHost, updateInfo.RepositoryUsername, updateInfo.PassWord, updateInfo.RepositoryAddress, imageRef)
if err != nil {
return err
}
2025-10-22 15:39:09 +08:00
2025-08-16 18:31:14 +08:00
// 定义正则表达式来匹配 image 字段
re := regexp.MustCompile(`"image"\s*:\s*"([^"]+)"`)
// 使用正则表达式查找并替换 image 字段的值
newConfiguration := re.ReplaceAllString(configurationString, `"image": "`+imageRef+`"`)
err = UpdateDevcontainerConfiguration(newConfiguration, repo, doer)
if err != nil {
return err
}
return nil
}
// ImageExists 检查指定镜像是否存在
// 返回值:
// - bool: 镜像是否存在true=存在false=不存在)
// - error: 非空表示检查过程中发生错误
func ImageExists(ctx context.Context, imageName string) (bool, error) {
// 创建 Docker 客户端
cli, err := docker_module.CreateDockerClient(ctx)
if err != nil {
return false, err // 其他错误
}
// 获取镜像信息
_, _, err = cli.ImageInspectWithRaw(ctx, imageName)
if err != nil {
2025-09-18 20:14:43 +08:00
if client.IsErrNotFound(err) {
return false, nil // 镜像不存在,但不是错误
}
2025-08-16 18:31:14 +08:00
return false, err // 其他错误
}
return true, nil // 镜像存在
}
2025-10-16 23:41:01 +08:00
2025-08-16 18:31:14 +08:00
func CheckDirExistsFromDocker(ctx context.Context, containerName, dirPath string) (bool, error) {
// 上下文
// 创建 Docker 客户端
cli, err := docker_module.CreateDockerClient(ctx)
if err != nil {
return false, err
}
// 获取容器 ID
containerID, err := docker_module.GetContainerID(cli, containerName)
if err != nil {
return false, err
}
// 创建 exec 配置
execConfig := types.ExecConfig{
Cmd: []string{"test", "-d", dirPath}, // 检查目录是否存在
2025-10-12 23:06:34 +08:00
AttachStdout: true,
AttachStderr: true,
}
// 创建 exec 实例
execResp, err := cli.ContainerExecCreate(context.Background(), containerID, execConfig)
if err != nil {
return false, err
}
// 执行命令
var exitCode int
err = cli.ContainerExecStart(context.Background(), execResp.ID, types.ExecStartCheck{})
if err != nil {
return false, err
}
// 获取命令执行结果
resp, err := cli.ContainerExecInspect(context.Background(), execResp.ID)
if err != nil {
return false, err
}
exitCode = resp.ExitCode
return exitCode == 0, nil // 退出码为 0 表示目录存在
}
2025-10-22 15:39:09 +08:00
2025-10-12 23:06:34 +08:00
func CheckFileExistsFromDocker(ctx context.Context, containerName, filePath string) (bool, error) {
// 上下文
// 创建 Docker 客户端
cli, err := docker_module.CreateDockerClient(ctx)
if err != nil {
return false, err
}
// 获取容器 ID
containerID, err := docker_module.GetContainerID(cli, containerName)
if err != nil {
return false, err
}
// 创建 exec 配置
execConfig := types.ExecConfig{
2025-10-16 23:41:01 +08:00
Cmd: []string{"test", "-e", filePath}, // 检查文件是否存在
2025-08-16 18:31:14 +08:00
AttachStdout: true,
AttachStderr: true,
}
// 创建 exec 实例
execResp, err := cli.ContainerExecCreate(context.Background(), containerID, execConfig)
if err != nil {
return false, err
}
// 执行命令
var exitCode int
err = cli.ContainerExecStart(context.Background(), execResp.ID, types.ExecStartCheck{})
if err != nil {
return false, err
}
// 获取命令执行结果
resp, err := cli.ContainerExecInspect(context.Background(), execResp.ID)
if err != nil {
return false, err
}
exitCode = resp.ExitCode
return exitCode == 0, nil // 退出码为 0 表示目录存在
}
2025-08-29 16:08:02 +08:00
func RegistWebTerminal(ctx context.Context) error {
log.Info("开始构建WebTerminal...")
cli, err := docker_module.CreateDockerClient(ctx)
if err != nil {
return err
}
defer cli.Close()
//拉取web_terminal镜像
dockerHost, err := docker_module.GetDockerSocketPath()
if err != nil {
return fmt.Errorf("获取docker socket路径失败:%v", err)
}
// 拉取镜像
err = docker_module.PullImage(ctx, cli, dockerHost, setting.DevContainerConfig.Web_Terminal_Image)
if err != nil {
fmt.Errorf("拉取web_terminal镜像失败:%v", err)
2025-08-29 16:08:02 +08:00
}
timestamp := time.Now().Format("20060102150405")
binds := []string{
"/var/run/docker.sock:/var/run/docker.sock",
}
containerName := "webterminal-" + timestamp
//创建并启动WebTerminal容器
err = docker_module.CreateAndStartContainer(ctx, cli, setting.DevContainerConfig.Web_Terminal_Image,
2025-09-04 10:48:46 +08:00
nil,
2025-08-29 16:08:02 +08:00
nil, binds,
nat.PortSet{
"7681/tcp": struct{}{},
},
containerName)
if err != nil {
return fmt.Errorf("创建并注册WebTerminal失败:%v", err)
}
// Save settings.
cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf)
if err != nil {
return err
}
2025-09-13 17:00:58 +08:00
_, err = docker_module.GetMappedPort(ctx, containerName, "7681")
2025-08-29 16:08:02 +08:00
if err != nil {
return err
}
2025-09-13 17:00:58 +08:00
cfg.Section("devcontainer").Key("WEB_TERMINAL_CONTAINER").SetValue(containerName)
2025-08-29 16:08:02 +08:00
if err = cfg.SaveTo(setting.CustomConf); err != nil {
return err
}
return nil
}