Files
devstar/services/devstar_devcontainer/RepoDevcontainerService.go

202 lines
8.7 KiB
Go
Raw Normal View History

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
}