Files
devstar/services/devcontainer/docker_agent.go
2025-08-11 11:29:50 +08:00

438 lines
13 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"
"regexp"
"strconv"
"strings"
"code.gitea.io/gitea/models/db"
devcontainer_model "code.gitea.io/gitea/models/devcontainer"
devcontainer_models "code.gitea.io/gitea/models/devcontainer"
docker_module "code.gitea.io/gitea/modules/docker"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
gitea_web_context "code.gitea.io/gitea/services/context"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/client"
"github.com/docker/go-connections/nat"
)
func CreateDevcontainer(ctx *context.Context, newDevContainer *CreateDevcontainerDTO, devContainerJSON *DevStarJSON, initializeScript string, restartScript string) error {
log.Info("开始创建容器.....")
// 1. 创建docker client
cli, err := docker_module.CreateDockerClient(ctx)
if err != nil {
return err
}
defer cli.Close()
// 加入22 集合
natPort22 := nat.Port("22/tcp")
devContainerJSON.ForwardPorts[natPort22] = struct{}{}
// 2. 创建容器
opts := &docker_module.CreateDevcontainerOptions{
DockerfileContent: newDevContainer.DockerfileContent,
Name: newDevContainer.Name,
Image: newDevContainer.Image,
CommandList: []string{
"sh",
"-c",
strings.Join(devContainerJSON.InitializeCommand, "") + "tail -f /dev/null;",
},
RepoId: newDevContainer.RepoId,
UserId: newDevContainer.UserId,
SSHPublicKeyList: newDevContainer.SSHPublicKeyList,
GitRepositoryURL: newDevContainer.GitRepositoryURL,
ContainerEnv: devContainerJSON.ContainerEnv,
PostCreateCommand: append([]string{"/home/devcontainer_init.sh"}, devContainerJSON.PostCreateCommand...),
ForwardPorts: devContainerJSON.ForwardPorts,
}
var flag string
for _, content := range devContainerJSON.RunArgs {
if flag == "-p" {
if opts.PortBindings == nil {
opts.PortBindings = nat.PortMap{}
}
// Split the string by ':'
parts := strings.Split(content, ":")
if len(parts) != 2 {
continue
}
hostPort := strings.TrimSpace(parts[0])
containerPortWithProtocol := strings.TrimSpace(parts[1])
// Split the container port and protocol
containerParts := strings.Split(containerPortWithProtocol, "/")
var containerProtocol = "tcp"
if len(containerParts) == 2 {
containerProtocol = containerParts[1]
}
containerPort := containerParts[0]
// Create nat.Port and nat.PortBinding
port := nat.Port(fmt.Sprintf("%s/%s", containerPort, containerProtocol))
portBinding := nat.PortBinding{
HostIP: "0.0.0.0",
HostPort: hostPort,
}
// Add to the port map
opts.PortBindings[port] = []nat.PortBinding{portBinding}
}
flag = strings.TrimSpace(content)
}
dockerHost, err := docker_module.GetDockerSocketPath()
if err != nil {
return fmt.Errorf("获取docker socket路径失败:%v", err)
}
// 拉取镜像
err = PullImageAsyncAndStartContainer(ctx, cli, dockerHost, opts)
if err != nil {
return fmt.Errorf("创建容器失败:%v", err)
}
return nil
}
func DeleteDevcontainer(ctx *context.Context, devcontainersList *[]devcontainer_model.Devcontainer) error {
log.Info("开始删除容器...")
// 创建docker client
cli, err := docker_module.CreateDockerClient(ctx)
if err != nil {
return err
}
defer cli.Close()
// 获取容器 ID
var containerIDList []string
for _, devcontainer := range *devcontainersList {
v, err := docker_module.GetContainerID(cli, devcontainer.Name)
if err != nil {
return err
}
containerIDList = append(containerIDList, v)
}
// 删除容器
for _, id := range containerIDList {
if err := docker_module.DeleteContainer(cli, id); err != nil {
return err
}
}
return nil
}
func GetDevcontainer(ctx *context.Context, opts *OpenDevcontainerAppDispatcherOptions) (uint16, error) {
// 创建docker client
cli, err := docker_module.CreateDockerClient(ctx)
if err != nil {
return 0, err
}
if cli != nil {
defer cli.Close()
}
// 获取容器ID
containerID, err := docker_module.GetContainerID(cli, opts.Name)
if err != nil {
return 0, err
}
port, err := docker_module.GetMappedPort(cli, containerID, "22")
if err != nil {
return 0, err
}
// 添加公钥
_, err = docker_module.ExecCommandInContainer(ctx, cli, containerID, fmt.Sprintf("echo '%s' >> ~/.ssh/authorized_keys", opts.UserPublicKey))
if err != nil {
return 0, err
}
v, err := strconv.ParseUint(port, 10, 16)
if err != nil {
return 0, err
}
return uint16(v), nil
}
func SaveDevcontainer(ctx *gitea_web_context.Context, opts *UpdateDevcontainerOptions) error {
// 创建docker client
reqctx := ctx.Req.Context()
cli, err := docker_module.CreateDockerClient(&reqctx)
imageRef := opts.RepositoryAddress + "/" + opts.RepositoryUsername + "/" + opts.ImageName
if err != nil {
return fmt.Errorf("创建docker client失败 %v", err)
}
defer cli.Close()
dbEngine := db.GetEngine(*ctx)
_, err = dbEngine.Table("devcontainer").
Where("user_id = ? AND repo_id = ? ", opts.Actor.ID, opts.Repository.ID).
Update(&devcontainer_model.Devcontainer{DevcontainerStatus: 6})
if err != nil {
log.Info("err %v", err)
}
if opts.SaveMethod == "on" {
// 创建构建上下文包含Dockerfile的tar包
var buf bytes.Buffer
tw := tar.NewWriter(&buf)
defer tw.Close()
// 添加Dockerfile到tar包
dockerfile := "Dockerfile"
dockerfileContent, err := GetDockerfileContent(ctx, opts.Repository)
content := []byte(dockerfileContent)
header := &tar.Header{
Name: dockerfile,
Size: int64(len(content)),
Mode: 0644,
}
if err := tw.WriteHeader(header); err != nil {
panic(err)
}
if _, err := tw.Write(content); err != nil {
panic(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, opts.DevContainerName)
if err != nil {
return fmt.Errorf("获取容器ID失败 %v", err)
}
// 提交容器
_, err = cli.ContainerCommit(ctx, containerID, types.ContainerCommitOptions{Reference: imageRef})
if err != nil {
return fmt.Errorf("提交容器失败 %v", err)
}
}
// 推送到仓库
dockerHost, err := docker_module.GetDockerSocketPath()
if err != nil {
return fmt.Errorf("推送到仓库失败 %v", err)
}
docker_module.PushImage(dockerHost, opts.RepositoryUsername, opts.PassWord, opts.RepositoryAddress, imageRef)
devcontainerJson, err := GetDevcontainerJsonString(ctx, opts.Repository)
// 定义正则表达式来匹配 image 字段
re := regexp.MustCompile(`"image"\s*:\s*"([^"]+)"`)
// 使用正则表达式查找并替换 image 字段的值
newJSONStr := re.ReplaceAllString(devcontainerJson, `"image": "`+imageRef+`"`)
_, err = dbEngine.Table("devcontainer").
Where("user_id = ? AND repo_id = ? ", opts.Actor.ID, opts.Repository.ID).
Update(&devcontainer_model.Devcontainer{DevcontainerStatus: 4})
if err != nil {
log.Info("err %v", err)
}
return UpdateDevcontainerJSON(ctx, newJSONStr)
}
func PullImageAsyncAndStartContainer(ctx *context.Context, cli *client.Client, dockerHost string, opts *docker_module.CreateDevcontainerOptions) error {
dbEngine := db.GetEngine(*ctx)
if _, err := dbEngine.Table("devcontainer_output").Insert(&devcontainer_models.DevcontainerOutput{
Output: "",
ListId: 1,
Status: "waitting",
UserId: opts.UserId,
RepoId: opts.RepoId,
Command: "docker " + "-H " + dockerHost + " pull " + opts.Image + "\n",
}); 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: opts.UserId,
RepoId: opts.RepoId,
Command: `docker run -d --name ` + opts.Name + ` -p 22 ` + opts.Image + ` /bin/bash -c "tail -f /dev/null"` + "\n",
ListId: 2,
}); err != nil {
log.Info("Failed to insert record: %v", err)
return err
}
cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf)
if err != nil {
log.Error("Failed to load custom conf '%s': %v", setting.CustomConf, err)
return err
}
if _, err := dbEngine.Table("devcontainer_output").Insert(&devcontainer_models.DevcontainerOutput{
Output: "",
Status: "waitting",
UserId: opts.UserId,
RepoId: opts.RepoId,
Command: `docker exec ` + opts.Name + ` sh -c "echo \"` + cfg.Section("server").Key("DOMAIN").Value() + ` host.docker.internal\" | tee -a /etc/hosts;apt update;apt install -y git ; git clone ` + opts.GitRepositoryURL + " " + "/data/workspace" + ` "` + "\n",
ListId: 3,
}); err != nil {
log.Info("Failed to insert record: %v", err)
return err
}
return nil
}
func DockerRestartContainer(gitea_ctx *gitea_web_context.Context, opts *RepoDevContainer) error {
// 创建docker client
ctx := context.Background()
cli, err := docker_module.CreateDockerClient(&ctx)
if err != nil {
return fmt.Errorf("创建docker client失败 %v", err)
}
defer cli.Close()
// 获取容器ID
containerID, err := docker_module.GetContainerID(cli, opts.DevContainerName)
if err != nil {
return fmt.Errorf("获取容器ID失败 %v", err)
}
dbEngine := db.GetEngine(ctx)
_, err = dbEngine.Table("devcontainer").
Where("user_id = ? AND repo_id = ? ", opts.UserId, opts.RepoId).
Update(&devcontainer_model.Devcontainer{DevcontainerStatus: 5})
if err != nil {
log.Info("err %v", err)
}
timeout := 10 // 超时时间(秒)
err = cli.ContainerRestart(context.Background(), containerID, container.StopOptions{
Timeout: &timeout,
})
if err != nil {
return fmt.Errorf("重启容器失败: %s\n", err)
} else {
log.Info("容器已重启")
}
devContainerJson, err := GetDevcontainerJsonModel(*gitea_ctx, gitea_ctx.Repo.Repository)
if err != nil {
return err
}
cmd := []string{"/home/devcontainer_restart.sh"}
postCreateCommand := append(cmd, devContainerJson.PostCreateCommand...)
// 创建 exec 实例
var buffer string = ""
var state int = 2
_, err = dbEngine.Table("devcontainer_output").
Where("user_id = ? AND repo_id = ? AND list_id = ?", opts.UserId, opts.RepoId, state).
Update(&devcontainer_models.DevcontainerOutput{
Status: "running",
})
if err != nil {
return err
}
if len(devContainerJson.PostCreateCommand) > 1 {
_, err = dbEngine.Table("devcontainer_output").
Where("user_id = ? AND repo_id = ? AND list_id = ?", opts.UserId, opts.RepoId, state+1).
Update(&devcontainer_models.DevcontainerOutput{
Status: "running",
})
if err != nil {
return err
}
}
for index, cmd := range postCreateCommand {
if index == 1 {
_, err = dbEngine.Table("devcontainer_output").
Where("user_id = ? AND repo_id = ? AND list_id = ?", opts.UserId, opts.RepoId, state).
Update(&devcontainer_models.DevcontainerOutput{
Status: "success",
})
if err != nil {
log.Info("Error storing output for command %v: %v\n", cmd, err)
}
buffer = ""
state = 3
continue
}
output, err := docker_module.ExecCommandInContainer(&ctx, cli, containerID, cmd)
buffer += output
if err != nil {
log.Info("执行命令失败:%v", err)
}
_, err = dbEngine.Table("devcontainer_output").
Where("user_id = ? AND repo_id = ? AND list_id = ?", opts.UserId, opts.RepoId, state).
Update(&devcontainer_models.DevcontainerOutput{
Output: buffer,
})
if err != nil {
log.Info("Error storing output for command %v: %v\n", cmd, err)
return err
}
}
_, err = dbEngine.Table("devcontainer_output").
Where("user_id = ? AND repo_id = ? AND list_id = ?", opts.UserId, opts.RepoId, 2).
Update(&devcontainer_models.DevcontainerOutput{
Status: "success",
})
if err != nil {
return err
}
if len(devContainerJson.PostCreateCommand) > 1 {
_, err = dbEngine.Table("devcontainer_output").
Where("user_id = ? AND repo_id = ? AND list_id = ?", opts.UserId, opts.RepoId, 3).
Update(&devcontainer_models.DevcontainerOutput{
Status: "success",
})
if err != nil {
log.Info("Error storing output for command %v: %v\n", cmd, err)
return err
}
}
_, err = dbEngine.Table("devcontainer").
Where("user_id = ? AND repo_id = ? ", opts.UserId, opts.RepoId).
Update(&devcontainer_model.Devcontainer{DevcontainerStatus: 4})
log.Info("DockerRestartContainerDockerRestartContainerDockerRestartContainer")
if err != nil {
log.Info("err %v", err)
}
return nil
}
func DockerStopContainer(ctx *context.Context, opts *RepoDevContainer) error {
// 创建docker client
cli, err := docker_module.CreateDockerClient(ctx)
if err != nil {
return fmt.Errorf("创建docker client失败 %v", err)
}
defer cli.Close()
// 获取容器ID
containerID, err := docker_module.GetContainerID(cli, opts.DevContainerName)
if err != nil {
return fmt.Errorf("获取容器ID失败 %v", err)
}
dbEngine := db.GetEngine(*ctx)
_, err = dbEngine.Table("devcontainer").
Where("user_id = ? AND repo_id = ? ", opts.UserId, opts.RepoId).
Update(&devcontainer_model.Devcontainer{DevcontainerStatus: 6})
if err != nil {
log.Info("err %v", err)
}
timeout := 10 // 超时时间(秒)
err = cli.ContainerStop(context.Background(), containerID, container.StopOptions{
Timeout: &timeout,
})
if err != nil {
return fmt.Errorf("停止容器失败: %s\n", err)
} else {
log.Info("容器已停止")
return nil
}
}