Files
devstar/services/devstar_devcontainer/RepoDevcontainerService.go

227 lines
9.6 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.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
}