package devcontainer import ( "archive/tar" "bytes" "context" "fmt" "io" "os/exec" "regexp" "strings" "time" "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" "github.com/docker/docker/client" "github.com/docker/docker/errdefs" "github.com/docker/go-connections/nat" ) 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 } func CreateDevContainerByDockerAPI(ctx context.Context, newDevcontainer *devcontainer_models.Devcontainer, imageName string, repo *repo.Repository, publicKeyList []string) error { 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 } _, 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 { return err } } _, 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{ nat.Port("22/tcp"): {}, }, 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 } output, err := docker_module.ExecCommandInContainer(ctx, cli, newDevcontainer.Name, `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;`, ) 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 } func CreateDevContainerByDockerCommand(ctx context.Context, newDevcontainer *devcontainer_models.Devcontainer, repo *repo.Repository, publicKeyList []string) (string, error) { dbEngine := db.GetEngine(ctx) configurationString, err := GetDevcontainerConfigurationString(ctx, repo) if err != nil { return "", err } configurationModel, err := UnmarshalDevcontainerConfigContent(configurationString) if err != nil { return "", err } var imageName = configurationModel.Image dockerSocket, err := docker_module.GetDockerSocketPath() if err != nil { return "", err } if configurationModel.Build != nil && configurationModel.Build.Dockerfile != "" { dockerfileContent, err := GetFileContentByPath(ctx, repo, ".devcontainer/"+configurationModel.Build.Dockerfile) if err != nil { return "", err } // 创建构建上下文(包含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 { return "", err } if _, err := tw.Write(content); err != nil { return "", err } // 执行镜像构建 imageName = fmt.Sprintf("%d", newDevcontainer.UserId) + "-" + fmt.Sprintf("%d", newDevcontainer.RepoId) + "-dockerfile" buildOptions := types.ImageBuildOptions{ Tags: []string{imageName}, // 镜像标签 } cli, err := docker_module.CreateDockerClient(ctx) if err != nil { return "", err } buildResponse, err := cli.ImageBuild( context.Background(), &buf, buildOptions, ) if err != nil { return "", err } output, err := io.ReadAll(buildResponse.Body) if err != nil { return "", err } log.Info(string(output)) } // 拉取镜像的命令 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) return imageName, err } cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf) if err != nil { return imageName, err } var startCommand string = `docker -H ` + dockerSocket + ` create --restart=always --name ` + newDevcontainer.Name // 将每个端口转换为 "-p " 格式 var portFlags string = " -p 22 " exposedPorts := configurationModel.ExtractContainerPorts() for _, port := range exposedPorts { portFlags = portFlags + fmt.Sprintf(" -p %d ", port) } startCommand += portFlags var envFlags string = ` -e RepoLink="` + strings.TrimSuffix(cfg.Section("server").Key("ROOT_URL").Value(), `/`) + repo.Link() + `" ` + ` -e DevstarHost="` + newDevcontainer.DevcontainerHost + `"` + ` -e WorkSpace="` + newDevcontainer.DevcontainerWorkDir + `/` + repo.Name + `" ` + ` -e DEVCONTAINER_STATUS="start" ` // 遍历 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) } startCommand += capAddFlags 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, " ") + " " overrideCommand := "" if !configurationModel.OverrideCommand { overrideCommand = ` sh -c "/home/webTerminal.sh" ` startCommand += ` --entrypoint="" ` } //创建并运行容器的命令 if _, err := dbEngine.Table("devcontainer_output").Insert(&devcontainer_models.DevcontainerOutput{ Output: "", Status: "waitting", UserId: newDevcontainer.UserId, RepoId: newDevcontainer.RepoId, Command: startCommand + imageName + overrideCommand + "\n", ListId: 2, DevcontainerId: newDevcontainer.Id, }); err != nil { log.Info("Failed to insert record: %v", err) return imageName, err } if _, err := dbEngine.Table("devcontainer_output").Insert(&devcontainer_models.DevcontainerOutput{ Output: "", Status: "waitting", UserId: newDevcontainer.UserId, RepoId: newDevcontainer.RepoId, Command: `docker -H ` + dockerSocket + ` start -a ` + newDevcontainer.Name + "\n", ListId: 3, DevcontainerId: newDevcontainer.Id, }); err != nil { log.Info("Failed to insert record: %v", err) return imageName, err } //连接容器的命令 if _, err := dbEngine.Table("devcontainer_output").Insert(&devcontainer_models.DevcontainerOutput{ Output: "", Status: "waitting", UserId: newDevcontainer.UserId, RepoId: newDevcontainer.RepoId, Command: `docker -H ` + dockerSocket + ` exec -it --workdir ` + newDevcontainer.DevcontainerWorkDir + "/" + repo.Name + ` ` + newDevcontainer.Name + ` sh -c "echo 'Successfully connected to the container';bash"` + "\n", ListId: 4, DevcontainerId: newDevcontainer.Id, }); err != nil { log.Info("Failed to insert record: %v", err) return imageName, err } return imageName, nil } 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 } func DeleteDevContainerByDocker(ctx context.Context, devContainerName string) error { // 创建docker client cli, err := docker_module.CreateDockerClient(ctx) if err != nil { return err } defer cli.Close() // 获取容器 ID containerID, err := docker_module.GetContainerID(cli, devContainerName) 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 } func RestartDevContainerByDocker(ctx context.Context, devContainerName string) error { // 创建docker client cli, err := docker_module.CreateDockerClient(ctx) if err != nil { return err } defer cli.Close() // 获取容器 ID containerID, err := docker_module.GetContainerID(cli, devContainerName) 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 } func StopDevContainerByDocker(ctx context.Context, devContainerName string) error { // 创建docker client cli, err := docker_module.CreateDockerClient(ctx) if err != nil { return err } defer cli.Close() // 获取容器 ID containerID, err := docker_module.GetContainerID(cli, devContainerName) 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 } // 定义正则表达式来匹配 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 { if client.IsErrNotFound(err) { return false, nil // 镜像不存在,但不是错误 } return false, err // 其他错误 } return true, nil // 镜像存在 } 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}, // 检查目录是否存在 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 表示目录存在 } 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{ Cmd: []string{"test", "-e", filePath}, // 检查文件是否存在 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 表示目录存在 } 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 { return fmt.Errorf("拉取web_terminal镜像失败:%v", err) } 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, nil, 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 } _, err = docker_module.GetMappedPort(ctx, containerName, "7681") if err != nil { return err } cfg.Section("devcontainer").Key("WEB_TERMINAL_CONTAINER").SetValue(containerName) if err = cfg.SaveTo(setting.CustomConf); err != nil { return err } return nil }