202 lines
8.7 KiB
Go
202 lines
8.7 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.SearchRepoDevcontainerDetailsVoOptions) (DevcontainersVO.RepoDevcontainerDetailVO, error) {
|
|||
|
|
|
|||
|
|
// 0. 构造异常返回时候的空数据
|
|||
|
|
resultRepoDevcontainerDetail := DevcontainersVO.RepoDevcontainerDetailVO{}
|
|||
|
|
|
|||
|
|
// 1. 检查参数是否有效
|
|||
|
|
if opts == nil || len(opts.UserName) == 0 || len(opts.RepoName) == 0 {
|
|||
|
|
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.description AS repo_description
|
|||
|
|
FROM devstar_devcontainer
|
|||
|
|
INNER JOIN repository on devstar_devcontainer.repo_id = repository.id
|
|||
|
|
WHERE
|
|||
|
|
devstar_devcontainer.user_id IN ( SELECT id FROM user WHERE name = '#{opts.UserName}')
|
|||
|
|
AND
|
|||
|
|
devstar_devcontainer.repo_id IN ( SELECT id FROM repository WHERE name = '#{opts.RepoName}');
|
|||
|
|
*/
|
|||
|
|
_, err := db.GetEngine(ctx).
|
|||
|
|
Table("devstar_devcontainer").
|
|||
|
|
Select("devstar_devcontainer.repo_id AS repo_id, repository.name AS repo_name, repository.description AS repo_description, 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").
|
|||
|
|
Join("INNER", "repository", "devstar_devcontainer.repo_id = repository.id").
|
|||
|
|
Where("devstar_devcontainer.user_id IN ( SELECT id FROM user WHERE name = ?) AND devstar_devcontainer.repo_id IN ( SELECT id FROM repository WHERE name = ?)", opts.UserName, opts.RepoName).
|
|||
|
|
Get(&resultRepoDevcontainerDetail)
|
|||
|
|
|
|||
|
|
// 3. 返回
|
|||
|
|
if err != nil {
|
|||
|
|
return resultRepoDevcontainerDetail, devstar_devcontainer_models.ErrFailedToOperateDevstarDevcontainerDB{
|
|||
|
|
Action: fmt.Sprintf("query devcontainer with repo '%v' and username '%v'", opts.RepoName, opts.UserName),
|
|||
|
|
Message: err.Error(),
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return resultRepoDevcontainerDetail, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// CreateRepoDevcontainer 创建 DevContainer
|
|||
|
|
/*
|
|||
|
|
必要假设:前置中间件已完成检查,确保数据有效:
|
|||
|
|
- 当前用户为已登录用户
|
|||
|
|
- 当前用户拥有 repo code写入权限
|
|||
|
|
- 数据库此前不存在 该用户在该repo创建的 Dev Container
|
|||
|
|
*/
|
|||
|
|
func CreateRepoDevcontainer(ctx context.Context, opts *DevcontainersVO.CreateRepoDevcontainerOptions) 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,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
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.DeleteRepoDevcontainerOptions) 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
|
|||
|
|
}
|