Files
devstar/services/devcontainer/docker_agent.go
xinitx 02baa3b7af !67 增加了重启停止容器、dockerfile方式创建保存容器功能
* change initializeScript path
* Merge branch 'add-dockerfile-method-and-start-stop-container' of https…
* 更新了容器镜像方式的构建、安装和使用方法,但是devcontainer功能还有问题
* fix run postCreateCommand bug
* sh文件方式管理启动脚本
* add restart command and fix bug
* add dockerfile method to create container and save container .restart …
2025-05-07 11:10:30 +00:00

600 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"
"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()
if opts.SaveMethod == "Container" {
// 获取容器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)
}
} else if opts.SaveMethod == "DockerFile" {
// 创建构建上下文包含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
}
}
// 推送到仓库
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+`"`)
return UpdateDevcontainerJSON(ctx, newJSONStr)
}
func PullImageAsyncAndStartContainer(ctx *context.Context, cli *client.Client, dockerHost string, opts *docker_module.CreateDevcontainerOptions) error {
var stdoutScanner, stderrScanner *bufio.Scanner
// 创建扫描器来读取输出
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)
}
dbEngine := db.GetEngine(*ctx)
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"})
// 创建并启动容器
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 实例
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)
}
}()
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)
}
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 实例
dbEngine := db.GetEngine(ctx)
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
}
}
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)
}
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
}
}