Files
devstar/services/devcontainer/docker_agent.go

701 lines
21 KiB
Go
Raw Permalink Normal View History

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/git"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
gitea_context "code.gitea.io/gitea/services/context"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"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 <port>" 格式
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" ` +
` -e WEB_TERMINAL_HELLO="Successfully connected to the devcontainer" `
// 遍历 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 "$WEB_TERMINAL_HELLO";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 *gitea_context.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.Repository)
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包
var dockerfileContent string
dockerfile := "Dockerfile"
if configurationModel.Build == nil || configurationModel.Build.Dockerfile == "" {
_, err := FileExists(".devcontainer/Dockerfile", repo)
if err != nil {
return err
}
dockerfileContent, err = GetFileContentByPath(ctx, repo.Repository, ".devcontainer/Dockerfile")
if err != nil {
return err
}
} else {
_, err := FileExists(".devcontainer/"+configurationModel.Build.Dockerfile, repo)
if err != nil {
if git.IsErrNotExist(err) {
_, err := FileExists(".devcontainer/Dockerfile", repo)
if err != nil {
return err
}
dockerfileContent, err = GetFileContentByPath(ctx, repo.Repository, ".devcontainer/Dockerfile")
if err != nil {
return err
}
}
return err
} else {
dockerfileContent, err = GetFileContentByPath(ctx, repo.Repository, ".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.Repository, 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 {
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
}
// ContainerExists 检查容器是否存在返回存在状态和容器ID如果存在
func ContainerExists(ctx context.Context, containerName string) (bool, string, error) {
cli, err := docker_module.CreateDockerClient(ctx)
if err != nil {
return false, "", err
}
// 设置过滤器,根据容器名称过滤
filter := filters.NewArgs()
filter.Add("name", containerName)
// 获取容器列表,使用过滤器
containers, err := cli.ContainerList(context.Background(), types.ContainerListOptions{
All: true, // 包括所有容器(运行的和停止的)
Filters: filter,
})
if err != nil {
return false, "", err
}
// 遍历容器,检查名称是否完全匹配
for _, container := range containers {
for _, name := range container.Names {
// 容器名称在Docker API中是以斜杠开头的例如 "/my-container"
// 所以我们需要检查去掉斜杠后的名称是否匹配
if strings.TrimPrefix(name, "/") == containerName {
return true, container.ID, nil
}
}
}
return false, "", nil
}