Files
devstar/services/devcontainer/docker_agent.go
2025-06-03 20:11:57 -07:00

659 lines
19 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"
"bufio"
"bytes"
"context"
"fmt"
"os/exec"
"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"
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 7681集合
natPort22 := nat.Port("22/tcp")
natPort7681 := nat.Port("7681/tcp")
devContainerJSON.ForwardPorts[natPort22] = struct{}{}
devContainerJSON.ForwardPorts[natPort7681] = 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,
InitializeCommand: initializeScript,
RestartCommand: restartScript,
}
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 {
var stdoutScanner, stderrScanner *bufio.Scanner
// 创建扫描器来读取输出
dbEngine := db.GetEngine(*ctx)
_, err := dbEngine.Table("devcontainer").
Where("user_id = ? AND repo_id = ? ", opts.UserId, opts.RepoId).
Update(&devcontainer_model.Devcontainer{DevcontainerStatus: 1})
if err != nil {
log.Info("err %v", err)
}
if opts.DockerfileContent != "" {
// 创建构建上下文包含Dockerfile的tar包
var buf bytes.Buffer
tw := tar.NewWriter(&buf)
defer tw.Close()
// 添加Dockerfile到tar包
dockerfile := "Dockerfile"
content := []byte(opts.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)
}
// 执行镜像构建
opts.Image = fmt.Sprintf("%d", opts.UserId) + "-" + fmt.Sprintf("%d", opts.RepoId) + "-dockerfileimage"
buildOptions := types.ImageBuildOptions{
Tags: []string{opts.Image}, // 镜像标签
}
buildResponse, err := cli.ImageBuild(
context.Background(),
&buf,
buildOptions,
)
if err != nil {
log.Info(err.Error())
return err
}
stdoutScanner = bufio.NewScanner(buildResponse.Body)
} else {
script := "docker " + "-H " + dockerHost + " pull " + opts.Image
cmd := exec.Command("sh", "-c", script)
// 获取标准输出和标准错误输出的管道
stdout, err := cmd.StdoutPipe()
if err != nil {
return err
}
stderr, err := cmd.StderrPipe()
if err != nil {
return err
}
err = cmd.Start()
if err != nil {
return err
}
stdoutScanner = bufio.NewScanner(stdout)
stderrScanner = bufio.NewScanner(stderr)
}
var pullImageOutput = devcontainer_models.DevcontainerOutput{
Output: "",
ListId: 0,
Status: "running",
UserId: opts.UserId,
RepoId: opts.RepoId,
Command: "Pull Image",
}
if _, err := dbEngine.Table("devcontainer_output").Insert(&pullImageOutput); err != nil {
log.Info("Failed to insert record: %v", err)
return err
}
_, err = dbEngine.Table("devcontainer_output").
Where("user_id = ? AND repo_id = ? ", opts.UserId, opts.RepoId).
Get(&pullImageOutput)
if err != nil {
log.Info("err %v", err)
return err
}
if _, err := dbEngine.Table("devcontainer_output").Insert(&devcontainer_models.DevcontainerOutput{
Output: "",
Status: "running",
UserId: opts.UserId,
RepoId: opts.RepoId,
Command: "Initialize Workspace",
ListId: 1,
}); err != nil {
log.Info("Failed to insert record: %v", err)
return err
}
_, err = dbEngine.Table("devcontainer_output").Insert(&devcontainer_models.DevcontainerOutput{
Output: "",
Status: "running",
UserId: opts.UserId,
RepoId: opts.RepoId,
Command: "Initialize DevStar",
ListId: 2,
})
if err != nil {
log.Info("Failed to insert record: %v", err)
return err
}
if len(opts.PostCreateCommand) > 1 {
_, err := dbEngine.Table("devcontainer_output").Insert(&devcontainer_models.DevcontainerOutput{
Output: "",
Status: "running",
UserId: opts.UserId,
RepoId: opts.RepoId,
Command: "Run postCreateCommand",
ListId: 3,
})
if err != nil {
log.Info("Failed to insert record: %v", err)
return err
}
}
// 使用 goroutine 来读取标准输出
go func() {
var output string
var cur int = 0
for stdoutScanner.Scan() {
output += "\n" + stdoutScanner.Text()
cur++
if cur%10 == 0 {
_, err = dbEngine.Table("devcontainer_output").
Where("id = ?", pullImageOutput.Id).
Update(&devcontainer_models.DevcontainerOutput{
Output: output})
if err != nil {
log.Info("err %v", err)
}
}
}
if stderrScanner != nil {
for stderrScanner.Scan() {
output += "\n" + stderrScanner.Text()
cur++
if cur%10 == 0 {
_, err = dbEngine.Table("devcontainer_output").
Where("id = ?", pullImageOutput.Id).
Update(&devcontainer_models.DevcontainerOutput{
Output: output})
if err != nil {
log.Info("err %v", err)
}
}
}
}
_, err = dbEngine.Table("devcontainer_output").
Where("id = ?", pullImageOutput.Id).
Update(&devcontainer_models.DevcontainerOutput{
Output: output})
if err != nil {
log.Info("err %v", err)
}
dbEngine.Table("devcontainer_output").
Where("id = ?", pullImageOutput.Id).
Update(&devcontainer_models.DevcontainerOutput{Status: "success"})
// 创建并启动容器
_, err := dbEngine.Table("devcontainer").
Where("user_id = ? AND repo_id = ? ", opts.UserId, opts.RepoId).
Update(&devcontainer_model.Devcontainer{DevcontainerStatus: 2})
if err != nil {
log.Info("err %v", err)
}
output, err = docker_module.CreateAndStartContainer(cli, opts)
if err != nil {
log.Info("创建或启动容器失败: %v", err)
}
containerID, err := docker_module.GetContainerID(cli, opts.Name)
if err != nil {
log.Info("获取容器ID:%v", err)
}
portInfo, err := docker_module.GetAllMappedPort(cli, containerID)
if err != nil {
log.Info("创建或启动容器失败:%v", err)
}
// 存储到数据库
if _, err := dbEngine.Table("devcontainer_output").
Where("user_id = ? AND repo_id = ? AND list_id = ?", opts.UserId, opts.RepoId, 1).
Update(&devcontainer_models.DevcontainerOutput{
Output: output + portInfo,
Status: "success",
}); err != nil {
log.Info("Error storing output for command %v: %v\n", opts.CommandList[2], err)
}
// 创建 exec 实例
_, err = dbEngine.Table("devcontainer").
Where("user_id = ? AND repo_id = ? ", opts.UserId, opts.RepoId).
Update(&devcontainer_model.Devcontainer{DevcontainerStatus: 3})
if err != nil {
log.Info("err %v", err)
}
var buffer string = ""
var state int = 2
for index, cmd := range opts.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)
}
}
_, 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 pull image: %v\n", err)
}
_, err = dbEngine.Table("devcontainer").
Where("user_id = ? AND repo_id = ? ", opts.UserId, opts.RepoId).
Update(&devcontainer_model.Devcontainer{DevcontainerStatus: 4})
if err != nil {
log.Info("err %v", 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
}
}