Files
devstar/services/devcontainer/devcontainer.go
2025-09-04 10:50:44 +08:00

765 lines
20 KiB
Go

package devcontainer
import (
"archive/tar"
"bytes"
"context"
"fmt"
"math"
"net"
"net/url"
"os"
"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"
files_service "code.gitea.io/gitea/services/repository/files"
"github.com/docker/docker/api/types"
"xorm.io/builder"
)
func HasDevContainer(ctx context.Context, userID, repoID int64) (bool, error) {
var hasDevContainer bool
dbEngine := db.GetEngine(ctx)
hasDevContainer, err := dbEngine.
Table("devcontainer").
Select("*").
Where("user_id = ? AND repo_id = ?", userID, repoID).
Exist()
if err != nil {
return hasDevContainer, err
}
return hasDevContainer, nil
}
func HasDevContainerConfiguration(ctx context.Context, repo *gitea_context.Repository) (bool, error) {
_, err := FileExists(".devcontainer/devcontainer.json", repo)
if err != nil {
if git.IsErrNotExist(err) {
return false, nil
}
return false, err
}
configurationString, err := GetDevcontainerConfigurationString(ctx, repo.Repository)
if err != nil {
return true, err
}
configurationModel, err := UnmarshalDevcontainerConfigContent(configurationString)
if err != nil {
return true, err
}
// 执行验证
if errs := configurationModel.Validate(); len(errs) > 0 {
log.Info("配置验证失败:")
for _, err := range errs {
fmt.Printf(" - %s\n", err.Error())
}
return true, fmt.Errorf("配置格式错误")
} else {
return true, nil
}
}
func HasDevContainerDockerFile(ctx context.Context, repo *gitea_context.Repository) (bool, error) {
_, err := FileExists(".devcontainer/devcontainer.json", repo)
if err != nil {
if git.IsErrNotExist(err) {
return false, nil
}
return false, err
}
configurationString, err := GetDevcontainerConfigurationString(ctx, repo.Repository)
if err != nil {
return false, err
}
configurationModel, err := UnmarshalDevcontainerConfigContent(configurationString)
if err != nil {
return false, err
}
// 执行验证
if errs := configurationModel.Validate(); len(errs) > 0 {
log.Info("配置验证失败:")
for _, err := range errs {
fmt.Printf(" - %s\n", err.Error())
}
return false, fmt.Errorf("配置格式错误")
} else {
log.Info("%v", configurationModel)
if configurationModel.Build == nil || configurationModel.Build.Dockerfile == "" {
return false, nil
}
_, err := FileExists(".devcontainer/"+configurationModel.Build.Dockerfile, repo)
if err != nil {
if git.IsErrNotExist(err) {
return false, nil
}
return false, err
}
return true, nil
}
}
func CreateDevcontainerConfiguration(repo *repo.Repository, doer *user.User) error {
jsonString := `{
"name":"template",
"image":"mcr.microsoft.com/devcontainers/base:dev-ubuntu-20.04",
"forwardPorts": ["8080"],
"containerEnv": {
"NODE_ENV": "development"
},
"initializeCommand": "echo \"init\";",
"postCreateCommand": [
"echo \"created\";",
"echo \"test\";"
],
"runArgs": [
"-p",
"8888:8888"
]
}`
_, err := files_service.ChangeRepoFiles(db.DefaultContext, repo, doer, &files_service.ChangeRepoFilesOptions{
Files: []*files_service.ChangeRepoFile{
{
Operation: "create",
TreePath: ".devcontainer/devcontainer.json",
ContentReader: bytes.NewReader([]byte(jsonString)),
},
},
OldBranch: "main",
NewBranch: "main",
Message: "add container configuration",
})
if err != nil {
return err
}
return nil
}
func GetWebTerminalURL(ctx context.Context, userID, repoID int64) (string, error) {
var devcontainerName string
dbEngine := db.GetEngine(ctx)
_, err := dbEngine.
Table("devcontainer").
Select("name").
Where("user_id = ? AND repo_id = ?", userID, repoID).
Get(&devcontainerName)
if err != nil {
return "", err
}
if setting.K8sConfig.Enable {
//k8s的逻辑
}
return "", nil
}
/*
-1不存在
0 已创建数据库记录
1 正在拉取镜像
2 正在创建和启动容器
3 容器安装必要工具
4 容器正在运行
5 正在提交容器更新
6 正在重启
7 正在停止
8 容器已停止
9 正在删除
10已删除
*/
func GetDevContainerStatus(ctx context.Context, userID, repoID string) (string, error) {
var id int
var containerName string
var status uint16
var realTimeStatus uint16
dbEngine := db.GetEngine(ctx)
_, err := dbEngine.
Table("devcontainer").
Select("devcontainer_status, id, name").
Where("user_id = ? AND repo_id = ?", userID, repoID).
Get(&status, &id, &containerName)
if err != nil {
return "", err
}
if id == 0 {
return fmt.Sprintf("%d", -1), nil
}
realTimeStatus = status
switch status {
//正在重启
case 6:
if setting.K8sConfig.Enable {
//k8s的逻辑
} else {
containerRealTimeStatus, err := GetDevContainerStatusFromDocker(ctx, containerName)
if err != nil {
return "", err
} else if containerRealTimeStatus == "running" {
realTimeStatus = 4
}
}
break
//正在关闭
case 7:
if setting.K8sConfig.Enable {
//k8s的逻辑
} else {
containerRealTimeStatus, err := GetDevContainerStatusFromDocker(ctx, containerName)
if err != nil {
return "", err
} else if containerRealTimeStatus == "exited" {
realTimeStatus = 8
}
}
break
case 9:
if setting.K8sConfig.Enable {
//k8s的逻辑
} else {
isContainerNotFound, err := IsContainerNotFound(ctx, containerName)
if err != nil {
return "", err
} else if isContainerNotFound {
realTimeStatus = 10
}
}
break
default:
log.Info("other status")
}
//状态更新
if realTimeStatus != status {
if realTimeStatus == 10 {
_, err = dbEngine.Table("devcontainer").
Where("user_id = ? AND repo_id = ? ", userID, repoID).
Delete()
if err != nil {
return "", err
}
_, err = dbEngine.Table("devcontainer_output").
Where("user_id = ? AND repo_id = ? ", userID, repoID).
Delete()
if err != nil {
return "", err
}
return "-1", nil
}
_, err = dbEngine.Table("devcontainer").
Where("user_id = ? AND repo_id = ? ", userID, repoID).
Update(&devcontainer_models.Devcontainer{DevcontainerStatus: realTimeStatus})
if err != nil {
return "", err
}
_, err = dbEngine.Table("devcontainer_output").
Where("user_id = ? AND repo_id = ? AND list_id = ?", userID, repoID, status).
Update(&devcontainer_models.DevcontainerOutput{Status: "finished"})
if err != nil {
return "", err
}
}
return fmt.Sprintf("%d", realTimeStatus), nil
}
func CreateDevContainer(ctx context.Context, repo *repo.Repository, doer *user.User, publicKeyList []string, isWebTerminal bool) error {
containerName := getSanitizedDevcontainerName(doer.Name, repo.Name)
cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf)
if err != nil {
return err
}
unixTimestamp := time.Now().Unix()
newDevcontainer := devcontainer_models.Devcontainer{
Name: containerName,
DevcontainerHost: cfg.Section("server").Key("DOMAIN").Value(),
DevcontainerUsername: "root",
DevcontainerWorkDir: "/data/workspace",
DevcontainerStatus: 0,
RepoId: repo.ID,
UserId: doer.ID,
CreatedUnix: unixTimestamp,
UpdatedUnix: unixTimestamp,
}
dbEngine := db.GetEngine(ctx)
_, err = dbEngine.
Table("devcontainer").
Insert(newDevcontainer)
if err != nil {
return err
}
_, err = dbEngine.
Table("devcontainer").
Select("*").
Where("user_id = ? AND repo_id = ?", doer.ID, repo.ID).
Get(&newDevcontainer)
if err != nil {
return err
}
go func() {
otherCtx := context.Background()
if setting.K8sConfig.Enable {
//k8s的逻辑
} else {
if isWebTerminal {
CreateDevContainerByDockerCommand(otherCtx, &newDevcontainer, repo, publicKeyList)
} else {
CreateDevContainerByDockerAPI(otherCtx, &newDevcontainer, repo, publicKeyList)
}
}
}()
return nil
}
func DeleteDevContainer(ctx context.Context, userID, repoID int64) error {
dbEngine := db.GetEngine(ctx)
var devContainerInfo devcontainer_models.Devcontainer
_, err := dbEngine.
Table("devcontainer").
Select("*").
Where("user_id = ? AND repo_id = ?", userID, repoID).
Get(&devContainerInfo)
if err != nil {
return err
}
_, err = dbEngine.Table("devcontainer").
Where("user_id = ? AND repo_id = ? ", userID, repoID).
Update(&devcontainer_models.Devcontainer{DevcontainerStatus: 9})
if err != nil {
return err
}
go func() {
otherCtx := context.Background()
if setting.K8sConfig.Enable {
//k8s的逻辑
} else {
err = DeleteDevContainerByDocker(otherCtx, &devContainerInfo)
if err != nil {
log.Info(err.Error())
}
}
}()
return nil
}
func RestartDevContainer(ctx context.Context, userID, repoID int64) error {
dbEngine := db.GetEngine(ctx)
var devContainerInfo devcontainer_models.Devcontainer
_, err := dbEngine.
Table("devcontainer").
Select("*").
Where("user_id = ? AND repo_id = ?", userID, repoID).
Get(&devContainerInfo)
if err != nil {
return err
}
_, err = dbEngine.Table("devcontainer").
Where("user_id = ? AND repo_id = ? ", userID, repoID).
Update(&devcontainer_models.Devcontainer{DevcontainerStatus: 6})
if err != nil {
return err
}
go func() {
otherCtx := context.Background()
if setting.K8sConfig.Enable {
//k8s的逻辑
} else {
err = RestartDevContainerByDocker(otherCtx, &devContainerInfo)
if err != nil {
log.Info(err.Error())
}
}
}()
return nil
}
func StopDevContainer(ctx context.Context, userID, repoID int64) error {
dbEngine := db.GetEngine(ctx)
var devContainerInfo devcontainer_models.Devcontainer
_, err := dbEngine.
Table("devcontainer").
Select("*").
Where("user_id = ? AND repo_id = ?", userID, repoID).
Get(&devContainerInfo)
if err != nil {
return err
}
_, err = dbEngine.Table("devcontainer").
Where("user_id = ? AND repo_id = ? ", userID, repoID).
Update(&devcontainer_models.Devcontainer{DevcontainerStatus: 7})
if err != nil {
return err
}
go func() {
otherCtx := context.Background()
if setting.K8sConfig.Enable {
//k8s的逻辑
} else {
err = StopDevContainerByDocker(otherCtx, &devContainerInfo)
if err != nil {
log.Info(err.Error())
}
}
}()
return nil
}
func UpdateDevContainer(ctx context.Context, doer *user.User, repo *repo.Repository, updateInfo *UpdateInfo) error {
dbEngine := db.GetEngine(ctx)
var devContainerInfo devcontainer_models.Devcontainer
_, err := dbEngine.
Table("devcontainer").
Select("*").
Where("user_id = ? AND repo_id = ?", doer.ID, repo.ID).
Get(&devContainerInfo)
if err != nil {
return err
}
_, err = dbEngine.Table("devcontainer").
Where("user_id = ? AND repo_id = ? ", doer.ID, repo.ID).
Update(&devcontainer_models.Devcontainer{DevcontainerStatus: 5})
if err != nil {
return err
}
otherCtx := context.Background()
if setting.K8sConfig.Enable {
//k8s的逻辑
} else {
updateErr := UpdateDevContainerByDocker(otherCtx, &devContainerInfo, updateInfo, repo, doer)
_, err = dbEngine.Table("devcontainer").
Where("user_id = ? AND repo_id = ? ", doer.ID, repo.ID).
Update(&devcontainer_models.Devcontainer{DevcontainerStatus: 4})
if err != nil {
return err
}
if updateErr != nil {
return updateErr
}
}
return nil
}
func GetTerminalCommand(ctx context.Context, userID string, repo *repo.Repository) (string, string, error) {
dbEngine := db.GetEngine(ctx)
var devContainerInfo devcontainer_models.Devcontainer
_, err := dbEngine.
Table("devcontainer").
Select("*").
Where("user_id = ? AND repo_id = ?", userID, repo.ID).
Get(&devContainerInfo)
if err != nil {
return "", "", err
}
realTimeStatus := devContainerInfo.DevcontainerStatus
var cmd string
switch devContainerInfo.DevcontainerStatus {
case 0:
if devContainerInfo.Id > 0 {
realTimeStatus = 1
}
break
case 1:
//正在拉取镜像,当镜像拉取成功,则状态转移
if setting.K8sConfig.Enable {
//k8s的逻辑
} else {
configurationString, err := GetDevcontainerConfigurationString(ctx, repo)
if err != nil {
return "", "", err
}
configurationModel, err := UnmarshalDevcontainerConfigContent(configurationString)
if err != nil {
return "", "", err
}
var imageName string
if configurationModel.Build == nil || configurationModel.Build.Dockerfile == "" {
imageName = configurationModel.Image
} else {
imageName = userID + "-" + fmt.Sprintf("%d", repo.ID) + "-dockerfile"
}
isExist, err := ImageExists(ctx, imageName)
if err != nil {
return "", "", err
}
if isExist {
realTimeStatus = 2
}
}
break
case 2:
//正在创建容器,创建容器成功,则状态转移
if setting.K8sConfig.Enable {
//k8s的逻辑
} else {
status, err := GetDevContainerStatusFromDocker(ctx, devContainerInfo.Name)
if err != nil {
return "", "", err
}
if status == "running" {
//添加脚本文件
if setting.K8sConfig.Enable {
} else {
var scriptContent []byte
_, err = os.Stat("webTerminal.sh")
if os.IsNotExist(err) {
_, err = os.Stat("/app/gitea/webTerminal.sh")
if os.IsNotExist(err) {
return "", "", err
} else {
scriptContent, err = os.ReadFile("/app/gitea/webTerminal.sh")
if err != nil {
return "", "", err
}
}
} else {
scriptContent, err = os.ReadFile("webTerminal.sh")
if err != nil {
return "", "", err
}
}
// 创建 tar 归档文件
var buf bytes.Buffer
tw := tar.NewWriter(&buf)
defer tw.Close()
// 添加文件到 tar 归档
AddFileToTar(tw, "webTerminal.sh", string(scriptContent), 0777)
// 创建 Docker 客户端
cli, err := docker_module.CreateDockerClient(ctx)
if err != nil {
return "", "", err
}
// 获取容器 ID
containerID, err := docker_module.GetContainerID(cli, devContainerInfo.Name)
if err != nil {
return "", "", err
}
err = cli.CopyToContainer(ctx, containerID, "/home", bytes.NewReader(buf.Bytes()), types.CopyToContainerOptions{})
if err != nil {
log.Info("%v", err)
return "", "", err
}
}
realTimeStatus = 3
}
}
break
case 3:
//正在初始化容器,初始化容器成功,则状态转移
if setting.K8sConfig.Enable {
//k8s的逻辑
} else {
status, err := CheckDirExistsFromDocker(ctx, devContainerInfo.Name, devContainerInfo.DevcontainerWorkDir)
if err != nil {
return "", "", err
}
if status {
realTimeStatus = 4
}
}
break
case 4:
//正在连接容器
if setting.K8sConfig.Enable {
//k8s的逻辑
} else {
_, err = dbEngine.Table("devcontainer_output").
Select("command").
Where("user_id = ? AND repo_id = ? AND list_id = ?", userID, repo.ID, realTimeStatus).
Get(&cmd)
if err != nil {
return "", "", err
}
}
break
}
if realTimeStatus != devContainerInfo.DevcontainerStatus {
//下一条指令
_, err = dbEngine.Table("devcontainer_output").
Select("command").
Where("user_id = ? AND repo_id = ? AND list_id = ?", userID, repo.ID, realTimeStatus).
Get(&cmd)
if err != nil {
return "", "", err
}
_, err = dbEngine.Table("devcontainer").
Where("user_id = ? AND repo_id = ? ", userID, repo.ID).
Update(&devcontainer_models.Devcontainer{DevcontainerStatus: realTimeStatus})
if err != nil {
return "", "", err
}
}
return cmd, fmt.Sprintf("%d", realTimeStatus), nil
}
func GetDevContainerOutput(ctx context.Context, doer *user.User, repo *repo.Repository) (OutputResponse, error) {
var devContainerOutput []devcontainer_models.DevcontainerOutput
dbEngine := db.GetEngine(ctx)
resp := OutputResponse{}
var status string
var containerName string
_, err := dbEngine.
Table("devcontainer").
Select("devcontainer_status, name").
Where("user_id = ? AND repo_id = ?", doer.ID, repo.ID).
Get(&status, &containerName)
if err != nil {
return resp, err
}
err = dbEngine.Table("devcontainer_output").
Where("user_id = ? AND repo_id = ?", doer.ID, repo.ID).
Find(&devContainerOutput)
if err != nil {
return resp, err
}
if len(devContainerOutput) > 0 {
resp.CurrentJob.Title = repo.Name + " Devcontainer Info"
resp.CurrentJob.Detail = status
if status == "4" {
// 获取WebSSH服务端口
webTerminalURL, err := GetWebTerminalURL(ctx, doer.ID, repo.ID)
if err == nil {
return resp, err
}
// 解析URL
u, err := url.Parse(webTerminalURL)
if err != nil {
return resp, err
}
// 分离主机和端口
terminalHost, terminalPort, err := net.SplitHostPort(u.Host)
resp.CurrentJob.IP = terminalHost
resp.CurrentJob.Port = terminalPort
if err != nil {
return resp, err
}
}
for _, item := range devContainerOutput {
logLines := []ViewStepLogLine{}
logLines = append(logLines, ViewStepLogLine{
Index: 1,
Message: item.Output,
})
resp.CurrentJob.Steps = append(resp.CurrentJob.Steps, &ViewJobStep{
Summary: item.Command,
Status: item.Status,
Logs: logLines,
})
}
}
return resp, nil
}
func GetMappedPort(ctx context.Context, containerName string, port string) (uint16, error) {
if setting.K8sConfig.Enable {
//k8s的逻辑
return 0, nil
} else {
port, err := docker_module.GetMappedPort(ctx, containerName, port)
if err != nil {
return 0, err
}
return port, nil
}
}
func GetDevcontainersList(ctx context.Context, doer *user.User, pageNum, pageSize int) (DevcontainerList, error) {
// 0. 构造异常返回时的空数据
var resultDevContainerListVO = DevcontainerList{
Page: 0,
PageSize: 50,
PageTotalNum: 0,
ItemTotalNum: 0,
DevContainers: []devcontainer_models.Devcontainer{},
}
resultDevContainerListVO.UserID = doer.ID
resultDevContainerListVO.Username = doer.Name
paginationOption := db.ListOptions{
Page: pageNum,
PageSize: pageSize,
}
paginationOption.ListAll = false // 强制使用分页查询,禁止一次性列举所有 devContainers
if paginationOption.Page <= 0 { // 未指定页码/无效页码:查询第 1 页
paginationOption.Page = 1
}
if paginationOption.PageSize <= 0 || paginationOption.PageSize > 50 {
paginationOption.PageSize = 50 // /无效页面大小/超过每页最大限制:自动调整到系统最大开发容器页面大小
}
resultDevContainerListVO.Page = paginationOption.Page
resultDevContainerListVO.PageSize = paginationOption.PageSize
// 2. SQL 条件构建
sqlCondition := builder.Eq{"user_id": doer.ID}
// 执行数据库事务
err := db.WithTx(ctx, func(ctx context.Context) error {
// 查询总数
count, err := db.GetEngine(ctx).
Table("devcontainer").
Where(sqlCondition).
Count()
if err != nil {
return err
}
resultDevContainerListVO.ItemTotalNum = count
// 无记录直接返回
if count == 0 {
return nil
}
// 计算分页参数
pageSize := int64(resultDevContainerListVO.PageSize)
resultDevContainerListVO.PageTotalNum = int(math.Ceil(float64(count) / float64(pageSize)))
// 查询分页数据
sess := db.GetEngine(ctx).
Table("devcontainer").
Join("INNER", "repository", "devcontainer.repo_id = repository.id").
Where(sqlCondition).
OrderBy("devcontainer_id DESC").
Select(`devcontainer.id AS devcontainer_id,
devcontainer.name AS devcontainer_name,
devcontainer.devcontainer_host AS devcontainer_host,
devcontainer.devcontainer_username AS devcontainer_username,
devcontainer.devcontainer_work_dir AS devcontainer_work_dir,
devcontainer.repo_id AS repo_id,
repository.name AS repo_name,
repository.owner_name AS repo_owner_name,
repository.description AS repo_description,
CONCAT('/', repository.owner_name, '/', repository.name) AS repo_link`)
resultDevContainerListVO.DevContainers = make([]devcontainer_models.Devcontainer, 0, pageSize)
err = db.SetSessionPagination(sess, &paginationOption).
Find(&resultDevContainerListVO.DevContainers)
if err != nil {
return err
}
return nil
})
if err != nil {
return resultDevContainerListVO, err
}
return resultDevContainerListVO, nil
}