Files
devstar/services/devcontainer/docker_agent.go
xinitx 9071a754f4 !108 给devcontainer增加变量和脚本功能
给devcontainer增加变量和脚本功能

- 能从devstar.cn上获取预定义的DEVSTAR_开头的变量或脚本
- 添加到脚本管理中的变量名,在devcontainer启动时会自动执行,然后才执行devcontainer.json中用户自定义脚本,其中可以调用设置的变量或脚本
- 变量或脚本在用户设置、项目设置和后台管理中都可以添加,如有重名优先级为:用户设置 > 项目设置 > 后台管理
2025-10-18 08:53:50 +00:00

635 lines
19 KiB
Go
Raw Permalink 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/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" `
// 遍历 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 'Successfully connected to the container';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 *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 {
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 {
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
}
_, 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
}