commit 78017d7525ec4f0dad83a96e5bd11032c1f351bb Author: DAI Mingchen <daimingchen@mail.ustc.edu.cn> Date: Tue Sep 3 11:16:17 2024 +0800 bugFix: org repo devContainer page HTTP 404
227 lines
9.6 KiB
Go
227 lines
9.6 KiB
Go
package devstar_devcontainer
|
||
|
||
import (
|
||
"code.gitea.io/gitea/models/db"
|
||
devstar_devcontainer_models "code.gitea.io/gitea/models/devstar_devcontainer"
|
||
"code.gitea.io/gitea/modules/log"
|
||
DevcontainersVO "code.gitea.io/gitea/routers/web/devcontainer/vo"
|
||
"context"
|
||
"fmt"
|
||
"github.com/google/uuid"
|
||
"regexp"
|
||
"strings"
|
||
"xorm.io/builder"
|
||
)
|
||
|
||
// GetRepoDevcontainerDetails 获取仓库对应 DevContainer 信息
|
||
func GetRepoDevcontainerDetails(ctx context.Context, opts *DevcontainersVO.RepoDevcontainerOptions) (DevcontainersVO.RepoDevContainerVO, error) {
|
||
|
||
// 0. 构造异常返回时候的空数据
|
||
resultRepoDevcontainerDetail := DevcontainersVO.RepoDevContainerVO{}
|
||
|
||
// 1. 检查参数是否有效
|
||
if opts == nil || opts.Actor == nil || opts.Repository == nil {
|
||
return resultRepoDevcontainerDetail, devstar_devcontainer_models.ErrFailedToOperateDevstarDevcontainerDB{
|
||
Action: "construct query condition for devContainer user list",
|
||
Message: "invalid search condition",
|
||
}
|
||
}
|
||
|
||
// 2. 查询数据库
|
||
/*
|
||
SELECT
|
||
devstar_devcontainer.id AS devcontainer_id,
|
||
devstar_devcontainer.name AS devcontainer_name,
|
||
devstar_devcontainer.devcontainer_host AS devcontainer_host,
|
||
devstar_devcontainer.devcontainer_port AS devcontainer_port,
|
||
devstar_devcontainer.devcontainer_username AS devcontainer_username,
|
||
devstar_devcontainer.devcontainer_password AS devcontainer_password,
|
||
devstar_devcontainer.devcontainer_work_dir AS devcontainer_work_dir,
|
||
devstar_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
|
||
FROM devstar_devcontainer
|
||
INNER JOIN repository on devstar_devcontainer.repo_id = repository.id
|
||
WHERE
|
||
devstar_devcontainer.user_id = #{opts.Actor.ID}
|
||
AND
|
||
devstar_devcontainer.repo_id = #{opts.Repository.ID};
|
||
*/
|
||
_, err := db.GetEngine(ctx).
|
||
Table("devstar_devcontainer").
|
||
Select(""+
|
||
"devstar_devcontainer.id AS devcontainer_id,"+
|
||
"devstar_devcontainer.name AS devcontainer_name,"+
|
||
"devstar_devcontainer.devcontainer_host AS devcontainer_host,"+
|
||
"devstar_devcontainer.devcontainer_port AS devcontainer_port,"+
|
||
"devstar_devcontainer.devcontainer_username AS devcontainer_username,"+
|
||
"devstar_devcontainer.devcontainer_password AS devcontainer_password,"+
|
||
"devstar_devcontainer.devcontainer_work_dir AS devcontainer_work_dir,"+
|
||
"devstar_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").
|
||
Join("INNER", "repository", "devstar_devcontainer.repo_id = repository.id").
|
||
Where("devstar_devcontainer.user_id = ? AND devstar_devcontainer.repo_id = ?", opts.Actor.ID, opts.Repository.ID).
|
||
Get(&resultRepoDevcontainerDetail)
|
||
|
||
// 3. 返回
|
||
if err != nil {
|
||
return resultRepoDevcontainerDetail, devstar_devcontainer_models.ErrFailedToOperateDevstarDevcontainerDB{
|
||
Action: fmt.Sprintf("query devcontainer with repo '%v' and username '%v'", opts.Repository.Name, opts.Actor.Name),
|
||
Message: err.Error(),
|
||
}
|
||
}
|
||
return resultRepoDevcontainerDetail, nil
|
||
}
|
||
|
||
// CreateRepoDevcontainer 创建 DevContainer
|
||
/*
|
||
必要假设:前置中间件已完成检查,确保数据有效:
|
||
- 当前用户为已登录用户
|
||
- 当前用户拥有 repo code写入权限
|
||
- 数据库此前不存在 该用户在该repo创建的 Dev Container
|
||
*/
|
||
func CreateRepoDevcontainer(ctx context.Context, opts *DevcontainersVO.RepoDevcontainerOptions) error {
|
||
// TODO: 调用 k8s client 创建
|
||
// 目前只是 mock数据
|
||
username := opts.Actor.Name
|
||
repoName := opts.Repository.Name
|
||
newDevcontainer := &devstar_devcontainer_models.DevstarDevcontainer{
|
||
Name: getSanitizedDevcontainerName(username, repoName),
|
||
DevcontainerHost: "127.0.0.1",
|
||
DevcontainerPort: 10086,
|
||
DevcontainerUsername: "mock-username",
|
||
DevcontainerPassword: "mock-password",
|
||
DevcontainerWorkDir: "~/workspace/",
|
||
RepoId: opts.Repository.ID,
|
||
UserId: opts.Actor.ID,
|
||
}
|
||
|
||
// 在数据库事务中创建 Dev Container 分配资源,出错时自动回滚相对应数据库字段,保证数据一致
|
||
dbTransactionErr := db.WithTx(ctx, func(ctx context.Context) error {
|
||
rowsAffect, err := db.GetEngine(ctx).
|
||
Table("devstar_devcontainer").
|
||
Insert(newDevcontainer)
|
||
|
||
if err != nil {
|
||
return devstar_devcontainer_models.ErrFailedToOperateDevstarDevcontainerDB{
|
||
Action: fmt.Sprintf("insert new DevContainer for user '%s' in repo '%s'", username, repoName),
|
||
Message: err.Error(),
|
||
}
|
||
} else if rowsAffect == 0 {
|
||
return devstar_devcontainer_models.ErrFailedToOperateDevstarDevcontainerDB{
|
||
Action: fmt.Sprintf("insert new DevContainer for user '%s' in repo '%s'", username, repoName),
|
||
Message: "expected 1 row to be inserted, but got 0",
|
||
}
|
||
}
|
||
|
||
err = claimDevcontainerResource(newDevcontainer)
|
||
if err != nil {
|
||
return devstar_devcontainer_models.ErrFailedToOperateDevstarDevcontainerDB{
|
||
Action: fmt.Sprintf("Failed to claim resource for Dev Container devstar_devcontainer.DevstarDevcontainer %v", newDevcontainer),
|
||
Message: err.Error(),
|
||
}
|
||
}
|
||
return nil
|
||
})
|
||
|
||
return dbTransactionErr
|
||
}
|
||
|
||
// DeleteRepoDevcontainer 按照 仓库 和/或 用户信息删除 DevContainer(s)
|
||
func DeleteRepoDevcontainer(ctx context.Context, opts *DevcontainersVO.RepoDevcontainerOptions) error {
|
||
if ctx == nil || opts == nil || (opts.Actor == nil && opts.Repository == nil) {
|
||
return devstar_devcontainer_models.ErrFailedToOperateDevstarDevcontainerDB{
|
||
Action: "construct query parameters",
|
||
Message: "Invalid parameters",
|
||
}
|
||
}
|
||
|
||
// 1. 构造查询条件
|
||
sqlDevcontainerCondition := builder.NewCond()
|
||
if opts.Actor != nil {
|
||
sqlDevcontainerCondition = sqlDevcontainerCondition.And(builder.Eq{"user_id": opts.Actor.ID})
|
||
}
|
||
if opts.Repository != nil {
|
||
sqlDevcontainerCondition = sqlDevcontainerCondition.And(builder.Eq{"repo_id": opts.Repository.ID})
|
||
}
|
||
var devcontainersList []devstar_devcontainer_models.DevstarDevcontainer
|
||
|
||
// 2. 开启事务:先获取 devcontainer列表,后删除
|
||
dbTransactionErr := db.WithTx(ctx, func(ctx context.Context) error {
|
||
var err error
|
||
// 2.1 条件查询: user_id 和/或 repo_id
|
||
err = db.GetEngine(ctx).
|
||
Table("devstar_devcontainer").
|
||
Where(sqlDevcontainerCondition).
|
||
Find(&devcontainersList)
|
||
if err != nil {
|
||
return devstar_devcontainer_models.ErrFailedToOperateDevstarDevcontainerDB{
|
||
Action: fmt.Sprintf("find devcontainer(s) with condition '%v'", sqlDevcontainerCondition),
|
||
Message: err.Error(),
|
||
}
|
||
}
|
||
|
||
// 2.2 条件删除: user_id 和/或 repo_id
|
||
_, err = db.GetEngine(ctx).
|
||
Table("devstar_devcontainer").
|
||
Where(sqlDevcontainerCondition).
|
||
Delete()
|
||
if err != nil {
|
||
return devstar_devcontainer_models.ErrFailedToOperateDevstarDevcontainerDB{
|
||
Action: fmt.Sprintf("MARK devcontainer(s) as DELETED with condition '%v'", sqlDevcontainerCondition),
|
||
Message: err.Error(),
|
||
}
|
||
}
|
||
|
||
return nil
|
||
})
|
||
|
||
if dbTransactionErr != nil {
|
||
return dbTransactionErr
|
||
}
|
||
|
||
// 3. 后台启动一个goroutine慢慢回收 Dev Container 资源 (如果回收失败,将会产生孤儿 Dev Container,只能管理员手动识别、删除)
|
||
go func() {
|
||
_ = purgeDevcontainersResource(devcontainersList)
|
||
}()
|
||
|
||
return dbTransactionErr
|
||
}
|
||
|
||
// getSanitizedDevcontainerName 辅助获取当前用户在当前仓库创建的 devContainer名称
|
||
// DevContainer命名规则: 用户名、仓库名各由小于15位的小写字母和数字组成,中间使用'-'分隔,后面使用'-'分隔符拼接32位UUID小写字母数字字符串
|
||
func getSanitizedDevcontainerName(username, repoName string) string {
|
||
regexpNonAlphaNum := regexp.MustCompile(`[^a-zA-Z0-9]`)
|
||
sanitizedUsername := regexpNonAlphaNum.ReplaceAllString(username, "")
|
||
sanitizedRepoName := regexpNonAlphaNum.ReplaceAllString(repoName, "")
|
||
if len(sanitizedUsername) > 15 {
|
||
sanitizedUsername = strings.ToLower(sanitizedUsername[:15])
|
||
}
|
||
if len(sanitizedRepoName) > 15 {
|
||
sanitizedRepoName = strings.ToLower(sanitizedRepoName[:15])
|
||
}
|
||
newUUID, _ := uuid.NewUUID()
|
||
uuidStr := newUUID.String()
|
||
uuidStr = regexpNonAlphaNum.ReplaceAllString(uuidStr, "")
|
||
return fmt.Sprintf("%s-%s-%s", sanitizedUsername, sanitizedRepoName, uuidStr)
|
||
}
|
||
|
||
// purgeDevcontainersResource 辅助函数,用于goroutine后台执行,回收DevContainer资源 // TODO
|
||
func purgeDevcontainersResource(devcontainersList []devstar_devcontainer_models.DevstarDevcontainer) error {
|
||
for _, devContainer := range devcontainersList {
|
||
log.Info("[Goroutine purgeDevcontainersResource]: 正在装模做样地回收资源: devstar_devcontainer_models.DevstarDevcontainer %v", devContainer)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// claimDevcontainerResource
|
||
func claimDevcontainerResource(newDevContainer *devstar_devcontainer_models.DevstarDevcontainer) error {
|
||
log.Info("[claimDevcontainerResource]: 正在装模做样地分配资源: devstar_devcontainer_models.DevstarDevcontainer %v", newDevContainer)
|
||
return nil
|
||
}
|