2024-08-30 12:28:59 +00:00
|
|
|
|
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 信息
|
2024-09-03 11:17:15 +08:00
|
|
|
|
func GetRepoDevcontainerDetails(ctx context.Context, opts *DevcontainersVO.RepoDevcontainerOptions) (DevcontainersVO.RepoDevContainerVO, error) {
|
2024-08-30 12:28:59 +00:00
|
|
|
|
|
|
|
|
|
|
// 0. 构造异常返回时候的空数据
|
2024-09-03 11:17:15 +08:00
|
|
|
|
resultRepoDevcontainerDetail := DevcontainersVO.RepoDevContainerVO{}
|
2024-08-30 12:28:59 +00:00
|
|
|
|
|
|
|
|
|
|
// 1. 检查参数是否有效
|
2024-09-03 11:17:15 +08:00
|
|
|
|
if opts == nil || opts.Actor == nil || opts.Repository == nil {
|
2024-08-30 12:28:59 +00:00
|
|
|
|
return resultRepoDevcontainerDetail, devstar_devcontainer_models.ErrFailedToOperateDevstarDevcontainerDB{
|
|
|
|
|
|
Action: "construct query condition for devContainer user list",
|
|
|
|
|
|
Message: "invalid search condition",
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 查询数据库
|
|
|
|
|
|
/*
|
2024-09-03 11:17:15 +08:00
|
|
|
|
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
|
2024-08-30 12:28:59 +00:00
|
|
|
|
FROM devstar_devcontainer
|
|
|
|
|
|
INNER JOIN repository on devstar_devcontainer.repo_id = repository.id
|
|
|
|
|
|
WHERE
|
2024-09-03 11:17:15 +08:00
|
|
|
|
devstar_devcontainer.user_id = #{opts.Actor.ID}
|
2024-08-30 12:28:59 +00:00
|
|
|
|
AND
|
2024-09-03 11:17:15 +08:00
|
|
|
|
devstar_devcontainer.repo_id = #{opts.Repository.ID};
|
2024-08-30 12:28:59 +00:00
|
|
|
|
*/
|
|
|
|
|
|
_, err := db.GetEngine(ctx).
|
|
|
|
|
|
Table("devstar_devcontainer").
|
2024-09-03 11:17:15 +08:00
|
|
|
|
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").
|
2024-08-30 12:28:59 +00:00
|
|
|
|
Join("INNER", "repository", "devstar_devcontainer.repo_id = repository.id").
|
2024-09-03 11:17:15 +08:00
|
|
|
|
Where("devstar_devcontainer.user_id = ? AND devstar_devcontainer.repo_id = ?", opts.Actor.ID, opts.Repository.ID).
|
2024-08-30 12:28:59 +00:00
|
|
|
|
Get(&resultRepoDevcontainerDetail)
|
|
|
|
|
|
|
|
|
|
|
|
// 3. 返回
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return resultRepoDevcontainerDetail, devstar_devcontainer_models.ErrFailedToOperateDevstarDevcontainerDB{
|
2024-09-03 11:17:15 +08:00
|
|
|
|
Action: fmt.Sprintf("query devcontainer with repo '%v' and username '%v'", opts.Repository.Name, opts.Actor.Name),
|
2024-08-30 12:28:59 +00:00
|
|
|
|
Message: err.Error(),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return resultRepoDevcontainerDetail, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// CreateRepoDevcontainer 创建 DevContainer
|
|
|
|
|
|
/*
|
|
|
|
|
|
必要假设:前置中间件已完成检查,确保数据有效:
|
|
|
|
|
|
- 当前用户为已登录用户
|
|
|
|
|
|
- 当前用户拥有 repo code写入权限
|
|
|
|
|
|
- 数据库此前不存在 该用户在该repo创建的 Dev Container
|
|
|
|
|
|
*/
|
2024-09-03 11:17:15 +08:00
|
|
|
|
func CreateRepoDevcontainer(ctx context.Context, opts *DevcontainersVO.RepoDevcontainerOptions) error {
|
2024-08-30 12:28:59 +00:00
|
|
|
|
// 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,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-09-03 11:17:15 +08:00
|
|
|
|
// 在数据库事务中创建 Dev Container 分配资源,出错时自动回滚相对应数据库字段,保证数据一致
|
2024-08-30 12:28:59 +00:00
|
|
|
|
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)
|
2024-09-03 11:17:15 +08:00
|
|
|
|
func DeleteRepoDevcontainer(ctx context.Context, opts *DevcontainersVO.RepoDevcontainerOptions) error {
|
2024-08-30 12:28:59 +00:00
|
|
|
|
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
|
|
|
|
|
|
}
|