Files
devstar/services/devcontainer/docker_agent.go
2025-09-04 10:50:44 +08:00

589 lines
17 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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/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, 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
}
var imageName = configurationModel.Image
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
}
} else {
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}, // 镜像标签
}
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))
}
_, 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"): {},
nat.Port("7681/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
}
log.Info("ExecCommandInContainerExecCommandInContainerExecCommandInContainerExecCommandInContainerExecCommandInContainer")
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()+" /data/workspace/"+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) 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 err
}
cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf)
if err != nil {
return err
}
//创建并运行容器的命令
if _, err := dbEngine.Table("devcontainer_output").Insert(&devcontainer_models.DevcontainerOutput{
Output: "",
Status: "waitting",
UserId: newDevcontainer.UserId,
RepoId: newDevcontainer.RepoId,
Command: `docker -H ` + dockerSocket + ` run -d --name ` + newDevcontainer.Name +
` -p 22 ` +
` -e RepoLink="` + strings.TrimSuffix(cfg.Section("server").Key("ROOT_URL").Value(), `/`) + repo.Link() + `" ` +
` -e WorkSpace="` + newDevcontainer.DevcontainerWorkDir + `/` + repo.Name + `"` +
` -e PublicKeyList="` + strings.Join(publicKeyList, "\n") + `" ` +
imageName +
` sh -c "tail -f /dev/null"` + "\n",
ListId: 2,
DevcontainerId: newDevcontainer.Id,
}); err != nil {
log.Info("Failed to insert record: %v", err)
return 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 ` + newDevcontainer.Name + ` /home/webTerminal.sh start` + "\n",
ListId: 3,
DevcontainerId: newDevcontainer.Id,
}); err != nil {
log.Info("Failed to insert record: %v", err)
return 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 + ` ` + 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 err
}
return 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, devContainerInfo *devcontainer_models.Devcontainer) error {
// 创建docker client
cli, err := docker_module.CreateDockerClient(ctx)
if err != nil {
return err
}
defer cli.Close()
// 获取容器 ID
containerID, err := docker_module.GetContainerID(cli, devContainerInfo.Name)
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, devContainerInfo *devcontainer_models.Devcontainer) error {
// 创建docker client
cli, err := docker_module.CreateDockerClient(ctx)
if err != nil {
return err
}
defer cli.Close()
// 获取容器 ID
containerID, err := docker_module.GetContainerID(cli, devContainerInfo.Name)
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, devContainerInfo *devcontainer_models.Devcontainer) error {
// 创建docker client
cli, err := docker_module.CreateDockerClient(ctx)
if err != nil {
return err
}
defer cli.Close()
// 获取容器 ID
containerID, err := docker_module.GetContainerID(cli, devContainerInfo.Name)
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 {
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 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
}
port, err := docker_module.GetMappedPort(ctx, containerName, "7681")
if err != nil {
return err
}
cfg.Section("devcontainer").Key("WEB_TERMINAL_PORT").SetValue(fmt.Sprintf("%d", port))
if err = cfg.SaveTo(setting.CustomConf); err != nil {
return err
}
return nil
}