Files
devstar/services/devstar_devcontainer/RepoDevcontainerService.go
DAI Mingchen 18485260b5 Squashed commit of the following:
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
2024-09-03 11:17:15 +08:00

227 lines
9.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}