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 }