2025-01-07 01:25:54 +00:00
|
|
|
|
package devcontainer
|
2024-08-30 12:28:59 +00:00
|
|
|
|
|
|
|
|
|
|
import (
|
2024-10-28 11:35:35 +00:00
|
|
|
|
"context"
|
|
|
|
|
|
"fmt"
|
2025-01-07 01:25:54 +00:00
|
|
|
|
"io/ioutil"
|
2025-05-07 11:10:30 +00:00
|
|
|
|
"net/url"
|
2025-01-07 01:25:54 +00:00
|
|
|
|
"os"
|
|
|
|
|
|
"path/filepath"
|
2024-10-28 11:35:35 +00:00
|
|
|
|
"regexp"
|
|
|
|
|
|
"strings"
|
|
|
|
|
|
"time"
|
|
|
|
|
|
|
2025-03-18 15:52:08 +00:00
|
|
|
|
auth_model "code.gitea.io/gitea/models/auth"
|
2024-08-30 12:28:59 +00:00
|
|
|
|
"code.gitea.io/gitea/models/db"
|
2025-02-17 05:36:49 +00:00
|
|
|
|
devcontainer_model "code.gitea.io/gitea/models/devcontainer"
|
|
|
|
|
|
devcontainer_models_errors "code.gitea.io/gitea/models/devcontainer/errors"
|
2025-05-27 23:58:00 +08:00
|
|
|
|
"code.gitea.io/gitea/models/perm"
|
|
|
|
|
|
repo_model "code.gitea.io/gitea/models/repo"
|
|
|
|
|
|
"code.gitea.io/gitea/models/unit"
|
2025-03-29 06:26:42 +00:00
|
|
|
|
user_model "code.gitea.io/gitea/models/user"
|
2025-02-13 05:56:32 +00:00
|
|
|
|
"code.gitea.io/gitea/modules/docker"
|
2025-05-13 10:50:26 +00:00
|
|
|
|
devcontainer_k8s_agent_module "code.gitea.io/gitea/modules/k8s"
|
2024-08-30 12:28:59 +00:00
|
|
|
|
"code.gitea.io/gitea/modules/log"
|
2024-09-30 06:48:01 +00:00
|
|
|
|
"code.gitea.io/gitea/modules/setting"
|
2025-02-13 05:56:32 +00:00
|
|
|
|
gitea_context "code.gitea.io/gitea/services/context"
|
2025-01-07 01:25:54 +00:00
|
|
|
|
devcontainer_service_errors "code.gitea.io/gitea/services/devcontainer/errors"
|
2025-05-27 23:58:00 +08:00
|
|
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
2025-02-17 05:36:49 +00:00
|
|
|
|
|
2025-01-07 01:25:54 +00:00
|
|
|
|
"code.gitea.io/gitea/services/devstar_ssh_key_pair/api_service"
|
2024-08-30 12:28:59 +00:00
|
|
|
|
"github.com/google/uuid"
|
|
|
|
|
|
"xorm.io/builder"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2025-02-17 05:36:49 +00:00
|
|
|
|
// 封装查询 DevContainer 实时信息,与具体 agent 无关,返回前端
|
|
|
|
|
|
type OpenDevcontainerAbstractAgent struct {
|
|
|
|
|
|
NodePortAssigned uint16
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// OpenDevcontainerService 获取 DevContainer 连接信息,抽象方法,适配多种 DevContainer Agent
|
|
|
|
|
|
func OpenDevcontainerService(ctx *gitea_context.Context, opts *OpenDevcontainerAppDispatcherOptions) (*OpenDevcontainerAbstractAgent, error) {
|
2025-05-20 12:06:35 +08:00
|
|
|
|
log.Info("OpenDevcontainerService: 开始获取 DevContainer 连接信息 name=%s, wait=%v",
|
|
|
|
|
|
opts.Name, opts.Wait)
|
2025-02-17 05:36:49 +00:00
|
|
|
|
|
|
|
|
|
|
// 0. 检查参数
|
|
|
|
|
|
if ctx == nil || opts == nil || len(opts.Name) == 0 {
|
2025-05-20 12:06:35 +08:00
|
|
|
|
log.Error("OpenDevcontainerService: 参数无效 ctx=%v, opts=%v", ctx != nil, opts != nil)
|
2025-02-17 05:36:49 +00:00
|
|
|
|
return nil, devcontainer_service_errors.ErrIllegalParams{
|
|
|
|
|
|
FieldNameList: []string{"ctx", "opts.Name"},
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 1. 检查 DevContainer 功能是否开启
|
|
|
|
|
|
if setting.Devcontainer.Enabled == false {
|
2025-05-20 12:06:35 +08:00
|
|
|
|
log.Warn("OpenDevcontainerService: DevContainer 功能已全局关闭")
|
2025-02-17 05:36:49 +00:00
|
|
|
|
return nil, devcontainer_service_errors.ErrOperateDevcontainer{
|
|
|
|
|
|
Action: "check availability of DevStar DevContainer",
|
|
|
|
|
|
Message: "DevContainer is turned off globally",
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 根据 DevContainer Agent 类型分发任务
|
|
|
|
|
|
apiRequestContext := ctx.Req.Context()
|
|
|
|
|
|
openDevcontainerAbstractAgentVO := &OpenDevcontainerAbstractAgent{}
|
|
|
|
|
|
switch setting.Devcontainer.Agent {
|
2025-05-20 12:06:35 +08:00
|
|
|
|
case setting.KUBERNETES, "k8s":
|
|
|
|
|
|
log.Info("OpenDevcontainerService: 使用 K8s Agent 获取 DevContainer: %s", opts.Name)
|
2025-02-17 05:36:49 +00:00
|
|
|
|
devcontainerApp, err := AssignDevcontainerGetting2K8sOperator(&apiRequestContext, opts)
|
|
|
|
|
|
if err != nil {
|
2025-05-20 12:06:35 +08:00
|
|
|
|
log.Error("OpenDevcontainerService: K8s DevContainer 获取失败: %v", err)
|
2025-02-17 05:36:49 +00:00
|
|
|
|
return nil, devcontainer_service_errors.ErrOperateDevcontainer{
|
|
|
|
|
|
Action: "Open DevContainer in k8s",
|
|
|
|
|
|
Message: err.Error(),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
openDevcontainerAbstractAgentVO.NodePortAssigned = devcontainerApp.Status.NodePortAssigned
|
2025-05-20 12:06:35 +08:00
|
|
|
|
log.Info("OpenDevcontainerService: K8s DevContainer 获取成功, name=%s, nodePort=%d, ready=%v",
|
|
|
|
|
|
opts.Name, devcontainerApp.Status.NodePortAssigned, devcontainerApp.Status.Ready)
|
2025-02-17 05:36:49 +00:00
|
|
|
|
case setting.DOCKER:
|
|
|
|
|
|
port, err := GetDevcontainer(&apiRequestContext, opts)
|
|
|
|
|
|
log.Info("port %d", port)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, devcontainer_service_errors.ErrOperateDevcontainer{
|
|
|
|
|
|
Action: "Open DevContainer in docker",
|
|
|
|
|
|
Message: err.Error(),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
openDevcontainerAbstractAgentVO.NodePortAssigned = port
|
|
|
|
|
|
default:
|
2025-05-20 12:06:35 +08:00
|
|
|
|
log.Error("OpenDevcontainerService: 未知的 DevContainer Agent 类型: %s", setting.Devcontainer.Agent)
|
2025-02-17 05:36:49 +00:00
|
|
|
|
return nil, devcontainer_service_errors.ErrOperateDevcontainer{
|
|
|
|
|
|
Action: "Open DevContainer",
|
|
|
|
|
|
Message: "No Valid DevContainer Agent Found",
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 3. 封装返回结果
|
2025-05-20 12:06:35 +08:00
|
|
|
|
log.Info("OpenDevcontainerService: 获取 DevContainer 连接信息完成, nodePort=%d",
|
|
|
|
|
|
openDevcontainerAbstractAgentVO.NodePortAssigned)
|
2025-02-17 05:36:49 +00:00
|
|
|
|
return openDevcontainerAbstractAgentVO, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-08-30 12:28:59 +00:00
|
|
|
|
// GetRepoDevcontainerDetails 获取仓库对应 DevContainer 信息
|
2025-02-17 05:36:49 +00:00
|
|
|
|
func GetRepoDevcontainerDetails(ctx context.Context, opts *RepoDevcontainerOptions) (RepoDevContainer, error) {
|
2025-05-20 12:06:35 +08:00
|
|
|
|
log.Info("GetRepoDevcontainerDetails: 开始查询仓库 DevContainer 信息")
|
2024-08-30 12:28:59 +00:00
|
|
|
|
|
|
|
|
|
|
// 0. 构造异常返回时候的空数据
|
2025-02-17 05:36:49 +00:00
|
|
|
|
resultRepoDevcontainerDetail := RepoDevContainer{}
|
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 {
|
2025-05-20 12:06:35 +08:00
|
|
|
|
log.Error("GetRepoDevcontainerDetails: 参数无效 opts=%v, actor=%v, repo=%v",
|
|
|
|
|
|
opts != nil, opts != nil && opts.Actor != nil, opts != nil && opts.Repository != nil)
|
2025-03-18 15:52:08 +00:00
|
|
|
|
return resultRepoDevcontainerDetail, devcontainer_models_errors.ErrFailedToOperateDevcontainerDB{
|
2024-08-30 12:28:59 +00:00
|
|
|
|
Action: "construct query condition for devContainer user list",
|
|
|
|
|
|
Message: "invalid search condition",
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-05-20 12:06:35 +08:00
|
|
|
|
log.Info("GetRepoDevcontainerDetails: 查询用户=%s (ID=%d) 的仓库=%s (ID=%d) 的 DevContainer",
|
|
|
|
|
|
opts.Actor.Name, opts.Actor.ID, opts.Repository.Name, opts.Repository.ID)
|
|
|
|
|
|
|
2024-08-30 12:28:59 +00:00
|
|
|
|
// 2. 查询数据库
|
|
|
|
|
|
_, err := db.GetEngine(ctx).
|
2025-03-18 15:52:08 +00:00
|
|
|
|
Table("devcontainer").
|
2024-09-03 11:17:15 +08:00
|
|
|
|
Select(""+
|
2025-03-18 15:52:08 +00:00
|
|
|
|
"devcontainer.id AS devcontainer_id,"+
|
|
|
|
|
|
"devcontainer.name AS devcontainer_name,"+
|
|
|
|
|
|
"devcontainer.devcontainer_host AS devcontainer_host,"+
|
2025-05-27 23:58:00 +08:00
|
|
|
|
"devcontainer.devcontainer_status AS devcontainer_status,"+
|
2025-03-18 15:52:08 +00:00
|
|
|
|
"devcontainer.devcontainer_username AS devcontainer_username,"+
|
|
|
|
|
|
"devcontainer.devcontainer_work_dir AS devcontainer_work_dir,"+
|
|
|
|
|
|
"devcontainer.repo_id AS repo_id,"+
|
|
|
|
|
|
"devcontainer.user_id AS user_id,"+
|
2024-09-03 11:17:15 +08:00
|
|
|
|
"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").
|
2025-03-18 15:52:08 +00:00
|
|
|
|
Join("INNER", "repository", "devcontainer.repo_id = repository.id").
|
|
|
|
|
|
Where("devcontainer.user_id = ? AND devcontainer.repo_id = ?", opts.Actor.ID, opts.Repository.ID).
|
2024-08-30 12:28:59 +00:00
|
|
|
|
Get(&resultRepoDevcontainerDetail)
|
|
|
|
|
|
|
|
|
|
|
|
// 3. 返回
|
|
|
|
|
|
if err != nil {
|
2025-05-20 12:06:35 +08:00
|
|
|
|
log.Error("GetRepoDevcontainerDetails: 数据库查询失败: %v", err)
|
2025-03-18 15:52:08 +00:00
|
|
|
|
return resultRepoDevcontainerDetail, devcontainer_models_errors.ErrFailedToOperateDevcontainerDB{
|
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
|
|
|
|
|
|
*/
|
2025-02-17 05:36:49 +00:00
|
|
|
|
func CreateRepoDevcontainer(ctx context.Context, opts *CreateRepoDevcontainerOptions) error {
|
2024-08-30 12:28:59 +00:00
|
|
|
|
username := opts.Actor.Name
|
|
|
|
|
|
repoName := opts.Repository.Name
|
2024-10-13 12:07:28 +00:00
|
|
|
|
|
2025-05-20 12:06:35 +08:00
|
|
|
|
log.Info("CreateRepoDevcontainer: 开始创建 DevContainer, user=%s, repo=%s, repoID=%d",
|
|
|
|
|
|
username, repoName, opts.Repository.ID)
|
|
|
|
|
|
|
2024-10-23 03:05:44 +00:00
|
|
|
|
// unixTimestamp is the number of seconds elapsed since January 1, 1970 UTC.
|
|
|
|
|
|
unixTimestamp := time.Now().Unix()
|
2025-05-20 12:06:35 +08:00
|
|
|
|
|
|
|
|
|
|
log.Info("CreateRepoDevcontainer: 获取 DevContainer JSON 模型")
|
2025-03-18 15:52:08 +00:00
|
|
|
|
devContainerJson, err := GetDevcontainerJsonModel(ctx, opts.Repository)
|
|
|
|
|
|
if err != nil {
|
2025-05-20 12:06:35 +08:00
|
|
|
|
log.Error("CreateRepoDevcontainer: 获取 DevContainer JSON 失败: %v", err)
|
2025-03-18 15:52:08 +00:00
|
|
|
|
return devcontainer_service_errors.ErrOperateDevcontainer{
|
|
|
|
|
|
Action: "Get DevContainer Error",
|
|
|
|
|
|
Message: err.Error(),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-05-20 12:06:35 +08:00
|
|
|
|
log.Info("CreateRepoDevcontainer: DevContainer JSON 获取成功, image=%s, dockerfilePath=%s",
|
|
|
|
|
|
devContainerJson.Image, devContainerJson.DockerfilePath)
|
|
|
|
|
|
|
2025-05-07 11:10:30 +00:00
|
|
|
|
var dockerfileContent string
|
|
|
|
|
|
if devContainerJson.DockerfilePath != "" {
|
2025-05-20 12:06:35 +08:00
|
|
|
|
log.Info("CreateRepoDevcontainer: 获取 Dockerfile 内容, path=%s", devContainerJson.DockerfilePath)
|
2025-05-07 11:10:30 +00:00
|
|
|
|
dockerfileContent, err = GetDockerfileContent(ctx, opts.Repository)
|
|
|
|
|
|
if err != nil {
|
2025-05-20 12:06:35 +08:00
|
|
|
|
log.Error("CreateRepoDevcontainer: 获取 Dockerfile 内容失败: %v", err)
|
2025-05-07 11:10:30 +00:00
|
|
|
|
return devcontainer_service_errors.ErrOperateDevcontainer{
|
|
|
|
|
|
Action: "Get DockerFileContent Error",
|
|
|
|
|
|
Message: err.Error(),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-05-20 12:06:35 +08:00
|
|
|
|
log.Debug("CreateRepoDevcontainer: Dockerfile 内容获取成功, 长度=%d", len(dockerfileContent))
|
2025-05-07 11:10:30 +00:00
|
|
|
|
}
|
2025-05-20 12:06:35 +08:00
|
|
|
|
|
2025-05-07 11:10:30 +00:00
|
|
|
|
cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf)
|
|
|
|
|
|
if err != nil {
|
2025-05-20 12:06:35 +08:00
|
|
|
|
log.Error("CreateRepoDevcontainer: 加载配置文件失败: %v", err)
|
2025-05-07 11:10:30 +00:00
|
|
|
|
}
|
2025-05-20 12:06:35 +08:00
|
|
|
|
|
|
|
|
|
|
containerName := getSanitizedDevcontainerName(username, repoName)
|
|
|
|
|
|
log.Info("CreateRepoDevcontainer: 生成 DevContainer 名称: %s", containerName)
|
|
|
|
|
|
|
2025-02-17 05:36:49 +00:00
|
|
|
|
newDevcontainer := &CreateDevcontainerDTO{
|
2025-03-18 15:52:08 +00:00
|
|
|
|
Devcontainer: devcontainer_model.Devcontainer{
|
2025-05-20 12:06:35 +08:00
|
|
|
|
Name: containerName,
|
2025-05-07 11:10:30 +00:00
|
|
|
|
DevcontainerHost: cfg.Section("server").Key("DOMAIN").Value(),
|
2024-10-13 12:07:28 +00:00
|
|
|
|
DevcontainerUsername: "root",
|
2024-10-23 03:05:44 +00:00
|
|
|
|
DevcontainerWorkDir: "/data/workspace",
|
2025-05-27 23:58:00 +08:00
|
|
|
|
DevcontainerStatus: 0,
|
2024-10-13 12:07:28 +00:00
|
|
|
|
RepoId: opts.Repository.ID,
|
|
|
|
|
|
UserId: opts.Actor.ID,
|
2024-10-23 03:05:44 +00:00
|
|
|
|
CreatedUnix: unixTimestamp,
|
|
|
|
|
|
UpdatedUnix: unixTimestamp,
|
2024-10-13 12:07:28 +00:00
|
|
|
|
},
|
2025-05-07 11:10:30 +00:00
|
|
|
|
DockerfileContent: dockerfileContent,
|
|
|
|
|
|
Image: devContainerJson.Image,
|
|
|
|
|
|
GitRepositoryURL: strings.TrimSuffix(setting.AppURL, "/") + opts.Repository.Link(),
|
2024-08-30 12:28:59 +00:00
|
|
|
|
}
|
2025-05-20 12:06:35 +08:00
|
|
|
|
log.Info("CreateRepoDevcontainer: 初始化 DevContainer 对象, host=%s, workDir=%s, gitURL=%s",
|
|
|
|
|
|
newDevcontainer.DevcontainerHost, newDevcontainer.DevcontainerWorkDir, newDevcontainer.GitRepositoryURL)
|
2025-06-25 13:19:09 +08:00
|
|
|
|
if setting.Devcontainer.Agent == setting.DOCKER {
|
|
|
|
|
|
rowsAffect, err := db.GetEngine(ctx).
|
|
|
|
|
|
Table("devcontainer").
|
|
|
|
|
|
Insert(newDevcontainer.Devcontainer)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return devcontainer_models_errors.ErrFailedToOperateDevcontainerDB{
|
|
|
|
|
|
Action: fmt.Sprintf("insert new DevContainer for user '%s' in repo '%s'", username, repoName),
|
|
|
|
|
|
Message: err.Error(),
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if rowsAffect == 0 {
|
|
|
|
|
|
return devcontainer_models_errors.ErrFailedToOperateDevcontainerDB{
|
|
|
|
|
|
Action: fmt.Sprintf("insert new DevContainer for user '%s' in repo '%s'", username, repoName),
|
|
|
|
|
|
Message: "expected 1 row to be inserted, but got 0",
|
|
|
|
|
|
}
|
2025-05-27 23:58:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2024-08-30 12:28:59 +00:00
|
|
|
|
|
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 {
|
2024-09-30 06:48:01 +00:00
|
|
|
|
var err error
|
2025-05-20 12:06:35 +08:00
|
|
|
|
log.Info("CreateRepoDevcontainer: 开始数据库事务")
|
|
|
|
|
|
|
2024-10-23 03:05:44 +00:00
|
|
|
|
// 0. 查询数据库,收集用户 SSH 公钥,合并用户临时填入SSH公钥
|
2025-05-20 12:06:35 +08:00
|
|
|
|
log.Info("CreateRepoDevcontainer: 查询用户 SSH 公钥, userID=%d", opts.Actor.ID)
|
2024-10-23 03:05:44 +00:00
|
|
|
|
var userSSHPublicKeyList []string
|
|
|
|
|
|
err = db.GetEngine(ctx).
|
|
|
|
|
|
Table("public_key").
|
|
|
|
|
|
Select("content").
|
|
|
|
|
|
Where("owner_id = ?", opts.Actor.ID).
|
|
|
|
|
|
Find(&userSSHPublicKeyList)
|
|
|
|
|
|
if err != nil {
|
2025-05-20 12:06:35 +08:00
|
|
|
|
log.Error("CreateRepoDevcontainer: 查询用户 SSH 公钥失败: %v", err)
|
2024-10-23 03:05:44 +00:00
|
|
|
|
return devcontainer_service_errors.ErrOperateDevcontainer{
|
|
|
|
|
|
Action: fmt.Sprintf("query SSH Public Key List for User %s", opts.Actor.Name),
|
|
|
|
|
|
Message: err.Error(),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-05-20 12:06:35 +08:00
|
|
|
|
log.Info("CreateRepoDevcontainer: 找到用户 SSH 公钥 %d 个", len(userSSHPublicKeyList))
|
|
|
|
|
|
|
2024-10-23 03:05:44 +00:00
|
|
|
|
newDevcontainer.SSHPublicKeyList = append(userSSHPublicKeyList, opts.SSHPublicKeyList...)
|
2025-05-20 12:06:35 +08:00
|
|
|
|
log.Info("CreateRepoDevcontainer: 合并 SSH 公钥后共 %d 个", len(newDevcontainer.SSHPublicKeyList))
|
|
|
|
|
|
|
2025-01-07 01:25:54 +00:00
|
|
|
|
devstarPublicKey := getDevStarPublicKey()
|
|
|
|
|
|
if devstarPublicKey == "" {
|
2025-05-20 12:06:35 +08:00
|
|
|
|
log.Error("CreateRepoDevcontainer: 获取 DevStar SSH 公钥失败")
|
2024-10-23 03:05:44 +00:00
|
|
|
|
return devcontainer_service_errors.ErrOperateDevcontainer{
|
2025-01-07 01:25:54 +00:00
|
|
|
|
Action: fmt.Sprintf("devstar SSH Public Key Error "),
|
|
|
|
|
|
Message: err.Error(),
|
2024-10-23 03:05:44 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-05-20 12:06:35 +08:00
|
|
|
|
log.Info("CreateRepoDevcontainer: 获取 DevStar SSH 公钥成功")
|
2025-01-07 01:25:54 +00:00
|
|
|
|
newDevcontainer.SSHPublicKeyList = append(newDevcontainer.SSHPublicKeyList)
|
2025-05-20 12:06:35 +08:00
|
|
|
|
|
|
|
|
|
|
// 1. 调用 k8s Agent,创建 DevContainer 资源,同时更新k8s调度器分配的 NodePort
|
|
|
|
|
|
if setting.Devcontainer.Agent == setting.KUBERNETES || setting.Devcontainer.Agent == "k8s" {
|
|
|
|
|
|
log.Info("CreateRepoDevcontainer: 调用 K8s controller 创建 DevContainer 资源")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-03-18 15:52:08 +00:00
|
|
|
|
err = claimDevcontainerResource(&ctx, newDevcontainer, devContainerJson)
|
2024-09-30 06:48:01 +00:00
|
|
|
|
if err != nil {
|
2025-05-20 12:06:35 +08:00
|
|
|
|
if setting.Devcontainer.Agent == setting.KUBERNETES || setting.Devcontainer.Agent == "k8s" {
|
|
|
|
|
|
log.Error("CreateRepoDevcontainer: K8s controller 创建失败: %v", err)
|
|
|
|
|
|
}
|
2025-03-18 15:52:08 +00:00
|
|
|
|
return devcontainer_models_errors.ErrFailedToOperateDevcontainerDB{
|
2024-09-30 06:48:01 +00:00
|
|
|
|
Action: fmt.Sprintf("claim resource for Dev Container %v", newDevcontainer),
|
|
|
|
|
|
Message: err.Error(),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-05-20 12:06:35 +08:00
|
|
|
|
|
|
|
|
|
|
if setting.Devcontainer.Agent == setting.KUBERNETES || setting.Devcontainer.Agent == "k8s" {
|
|
|
|
|
|
log.Info("CreateRepoDevcontainer: K8s controller 创建成功, nodePort=%d", newDevcontainer.DevcontainerPort)
|
2025-06-25 13:19:09 +08:00
|
|
|
|
// 2. 根据分配的 NodePort 更新数据库字段
|
|
|
|
|
|
log.Info("CreateRepoDevcontainer: 在数据库中创建 DevContainer 记录, nodePort=%d",
|
|
|
|
|
|
newDevcontainer.DevcontainerPort)
|
|
|
|
|
|
rowsAffect, err := db.GetEngine(ctx).
|
|
|
|
|
|
Table("devcontainer").
|
|
|
|
|
|
Insert(newDevcontainer.Devcontainer)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
log.Error("CreateRepoDevcontainer: 数据库插入失败: %v", err)
|
|
|
|
|
|
return devcontainer_models_errors.ErrFailedToOperateDevcontainerDB{
|
|
|
|
|
|
Action: fmt.Sprintf("insert new DevContainer for user '%s' in repo '%s'", username, repoName),
|
|
|
|
|
|
Message: err.Error(),
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if rowsAffect == 0 {
|
|
|
|
|
|
log.Error("CreateRepoDevcontainer: 数据库插入失败: 影响行数为0")
|
|
|
|
|
|
return devcontainer_models_errors.ErrFailedToOperateDevcontainerDB{
|
|
|
|
|
|
Action: fmt.Sprintf("insert new DevContainer for user '%s' in repo '%s'", username, repoName),
|
|
|
|
|
|
Message: "expected 1 row to be inserted, but got 0",
|
|
|
|
|
|
}
|
2024-08-30 12:28:59 +00:00
|
|
|
|
}
|
2025-06-25 13:19:09 +08:00
|
|
|
|
log.Info("CreateRepoDevcontainer: 数据库插入成功, 影响行数=%d", rowsAffect)
|
2024-08-30 12:28:59 +00:00
|
|
|
|
}
|
2025-06-25 13:19:09 +08:00
|
|
|
|
|
2024-08-30 12:28:59 +00:00
|
|
|
|
return nil
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2025-05-20 12:06:35 +08:00
|
|
|
|
if dbTransactionErr != nil {
|
|
|
|
|
|
log.Error("CreateRepoDevcontainer: 创建失败: %v", dbTransactionErr)
|
|
|
|
|
|
return dbTransactionErr
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
log.Info("CreateRepoDevcontainer: DevContainer 创建成功, name=%s", newDevcontainer.Name)
|
|
|
|
|
|
return nil
|
2024-08-30 12:28:59 +00:00
|
|
|
|
}
|
2025-01-07 01:25:54 +00:00
|
|
|
|
func getDevStarPublicKey() string {
|
|
|
|
|
|
// 获取当前用户的主目录
|
|
|
|
|
|
homeDir, err := os.UserHomeDir()
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
log.Info("Failed to get home directory: %s", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 构建公钥文件的路径
|
|
|
|
|
|
publicKeyPath := filepath.Join(homeDir, ".ssh", "id_rsa_devstar.pub")
|
|
|
|
|
|
privateKeyPath := filepath.Join(homeDir, ".ssh", "id_rsa_devstar")
|
|
|
|
|
|
if !fileExists(publicKeyPath) || !fileExists(privateKeyPath) {
|
|
|
|
|
|
err, key := api_service.GenerateNewRSASSHSessionKeyPair()
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
log.Info("无法创建密钥:", err)
|
|
|
|
|
|
return ""
|
|
|
|
|
|
}
|
|
|
|
|
|
// 确保~/.ssh目录存在
|
|
|
|
|
|
sshDir := filepath.Join(homeDir, ".ssh")
|
|
|
|
|
|
if err := os.MkdirAll(sshDir, 0700); err != nil {
|
|
|
|
|
|
log.Info("无法创建~/.ssh目录:", err)
|
|
|
|
|
|
return ""
|
|
|
|
|
|
}
|
|
|
|
|
|
// 创建密钥文件
|
|
|
|
|
|
if err := ioutil.WriteFile(privateKeyPath, []byte(key.PrivateKeyPEM), 0600); err != nil {
|
|
|
|
|
|
log.Info("无法写入私钥文件:", err)
|
|
|
|
|
|
return ""
|
|
|
|
|
|
}
|
|
|
|
|
|
if err := ioutil.WriteFile(publicKeyPath, []byte(key.PublicKeySsh), 0600); err != nil {
|
|
|
|
|
|
log.Info("无法写入公钥文件:", err)
|
|
|
|
|
|
return ""
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// 读取公钥文件内容
|
|
|
|
|
|
publicKeyBytes, err := ioutil.ReadFile(publicKeyPath)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
log.Info("Failed to read public key file: %s", err)
|
|
|
|
|
|
return ""
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 将文件内容转换为字符串
|
|
|
|
|
|
return string(publicKeyBytes)
|
|
|
|
|
|
}
|
|
|
|
|
|
func fileExists(filename string) bool {
|
|
|
|
|
|
info, err := os.Stat(filename)
|
|
|
|
|
|
if os.IsNotExist(err) {
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
return !info.IsDir()
|
|
|
|
|
|
}
|
2025-02-13 05:56:32 +00:00
|
|
|
|
func GetWebTerminalURL(ctx context.Context, devcontainerName string) (string, error) {
|
|
|
|
|
|
switch setting.Devcontainer.Agent {
|
2025-05-20 12:06:35 +08:00
|
|
|
|
case setting.KUBERNETES, "k8s":
|
|
|
|
|
|
log.Info("GetWebTerminalURL: 开始查找 K8s DevContainer ttyd 端口, name=%s", devcontainerName)
|
|
|
|
|
|
|
2025-05-13 10:50:26 +00:00
|
|
|
|
// 创建 K8s 客户端,直接查询 CRD 以获取 ttyd 端口
|
|
|
|
|
|
k8sClient, err := devcontainer_k8s_agent_module.GetKubernetesClient(&ctx)
|
|
|
|
|
|
if err != nil {
|
2025-05-20 12:06:35 +08:00
|
|
|
|
log.Error("GetWebTerminalURL: 获取 K8s 客户端失败: %v", err)
|
2025-05-13 10:50:26 +00:00
|
|
|
|
return "", err
|
|
|
|
|
|
}
|
2025-05-20 12:06:35 +08:00
|
|
|
|
log.Info("GetWebTerminalURL: K8s 客户端创建成功")
|
2025-05-13 10:50:26 +00:00
|
|
|
|
|
|
|
|
|
|
// 直接从K8s获取CRD信息,不依赖数据库
|
|
|
|
|
|
opts := &devcontainer_k8s_agent_module.GetDevcontainerOptions{
|
|
|
|
|
|
GetOptions: metav1.GetOptions{},
|
|
|
|
|
|
Name: devcontainerName,
|
|
|
|
|
|
Namespace: setting.Devcontainer.Namespace,
|
|
|
|
|
|
Wait: false,
|
|
|
|
|
|
}
|
2025-05-20 12:06:35 +08:00
|
|
|
|
log.Info("GetWebTerminalURL: 从 K8s 获取 DevcontainerApp %s, namespace=%s",
|
|
|
|
|
|
devcontainerName, setting.Devcontainer.Namespace)
|
2025-05-13 10:50:26 +00:00
|
|
|
|
|
|
|
|
|
|
devcontainerApp, err := devcontainer_k8s_agent_module.GetDevcontainer(&ctx, k8sClient, opts)
|
|
|
|
|
|
if err != nil {
|
2025-05-20 12:06:35 +08:00
|
|
|
|
log.Error("GetWebTerminalURL: 获取 DevcontainerApp 失败: %v", err)
|
2025-05-13 10:50:26 +00:00
|
|
|
|
return "", err
|
|
|
|
|
|
}
|
2025-05-20 12:06:35 +08:00
|
|
|
|
log.Info("GetWebTerminalURL: 成功获取 DevcontainerApp, extraPorts=%d",
|
|
|
|
|
|
len(devcontainerApp.Status.ExtraPortsAssigned))
|
2025-05-13 10:50:26 +00:00
|
|
|
|
|
|
|
|
|
|
// 在额外端口中查找 ttyd 端口,使用多个条件匹配
|
|
|
|
|
|
var ttydNodePort uint16 = 0
|
|
|
|
|
|
for _, portInfo := range devcontainerApp.Status.ExtraPortsAssigned {
|
|
|
|
|
|
// 检查各种可能的情况:名称为ttyd、名称包含ttyd、名称为port-7681、端口为7681
|
2025-05-20 12:06:35 +08:00
|
|
|
|
log.Debug("GetWebTerminalURL: 检查端口 name=%s, containerPort=%d, nodePort=%d",
|
|
|
|
|
|
portInfo.Name, portInfo.ContainerPort, portInfo.NodePort)
|
|
|
|
|
|
|
2025-05-13 10:50:26 +00:00
|
|
|
|
if portInfo.Name == "ttyd" ||
|
|
|
|
|
|
strings.Contains(portInfo.Name, "ttyd") ||
|
|
|
|
|
|
portInfo.Name == "port-7681" ||
|
|
|
|
|
|
portInfo.ContainerPort == 7681 {
|
|
|
|
|
|
ttydNodePort = portInfo.NodePort
|
2025-05-20 12:06:35 +08:00
|
|
|
|
log.Info("GetWebTerminalURL: 找到 ttyd 端口: %d, 名称: %s", ttydNodePort, portInfo.Name)
|
2025-05-13 10:50:26 +00:00
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 如果找到 ttyd 端口,构建 URL
|
|
|
|
|
|
if ttydNodePort > 0 {
|
|
|
|
|
|
cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf)
|
|
|
|
|
|
if err != nil {
|
2025-05-20 12:06:35 +08:00
|
|
|
|
log.Error("GetWebTerminalURL: 加载配置文件失败: %v", err)
|
2025-05-13 10:50:26 +00:00
|
|
|
|
return "", err
|
|
|
|
|
|
}
|
2025-05-28 01:30:51 +08:00
|
|
|
|
|
|
|
|
|
|
// 检查是否启用了基于路径的访问方式
|
|
|
|
|
|
|
2025-05-13 10:50:26 +00:00
|
|
|
|
domain := cfg.Section("server").Key("DOMAIN").Value()
|
2025-05-28 01:30:51 +08:00
|
|
|
|
scheme := "https"
|
|
|
|
|
|
|
|
|
|
|
|
// 从容器名称中提取用户名和仓库名
|
|
|
|
|
|
parts := strings.Split(devcontainerName, "-")
|
|
|
|
|
|
if len(parts) >= 2 {
|
|
|
|
|
|
username := parts[0]
|
|
|
|
|
|
repoName := parts[1]
|
|
|
|
|
|
|
|
|
|
|
|
// 构建访问路径
|
|
|
|
|
|
path := fmt.Sprintf("/%s/%s/dev-container-webterminal", username, repoName)
|
|
|
|
|
|
terminalURL := fmt.Sprintf("%s://%s%s", scheme, domain, path)
|
|
|
|
|
|
|
|
|
|
|
|
log.Info("GetWebTerminalURL: 使用 Ingress 路径方式生成 ttyd URL: %s", terminalURL)
|
|
|
|
|
|
return terminalURL, nil
|
|
|
|
|
|
}
|
2025-05-13 10:50:26 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 如果没有找到ttyd端口,记录详细的调试信息
|
2025-05-20 12:06:35 +08:00
|
|
|
|
log.Warn("GetWebTerminalURL: 未找到 ttyd 端口 (7681), 可用的额外端口: %v",
|
|
|
|
|
|
devcontainerApp.Status.ExtraPortsAssigned)
|
2025-05-13 10:50:26 +00:00
|
|
|
|
return "", fmt.Errorf("ttyd port (7681) not found for container: %s", devcontainerName)
|
2025-02-13 05:56:32 +00:00
|
|
|
|
case setting.DOCKER:
|
2025-08-11 11:29:50 +08:00
|
|
|
|
return "http://localhost:7681/?", nil
|
2025-02-13 05:56:32 +00:00
|
|
|
|
default:
|
|
|
|
|
|
return "", fmt.Errorf("unknown agent")
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-03-29 06:26:42 +00:00
|
|
|
|
func Get_IDE_TerminalURL(ctx *gitea_context.Context, devcontainer *RepoDevContainer) (string, error) {
|
|
|
|
|
|
var access_token string
|
2025-05-28 01:30:51 +08:00
|
|
|
|
|
|
|
|
|
|
// 检查 session 中是否已存在 token
|
2025-03-29 06:26:42 +00:00
|
|
|
|
if ctx.Session.Get("access_token") != nil {
|
|
|
|
|
|
access_token = ctx.Session.Get("access_token").(string)
|
|
|
|
|
|
} else {
|
2025-05-28 01:30:51 +08:00
|
|
|
|
// 生成 token
|
2025-03-29 06:26:42 +00:00
|
|
|
|
token := &auth_model.AccessToken{
|
|
|
|
|
|
UID: devcontainer.UserId,
|
|
|
|
|
|
Name: "terminal_login_token",
|
|
|
|
|
|
}
|
|
|
|
|
|
exist, err := auth_model.AccessTokenByNameExists(ctx, token)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return "", err
|
|
|
|
|
|
}
|
|
|
|
|
|
if exist {
|
|
|
|
|
|
db.GetEngine(ctx).Table("access_token").Where("uid = ? AND name = ?", devcontainer.UserId, "terminal_login_token").Delete()
|
|
|
|
|
|
}
|
|
|
|
|
|
scope, err := auth_model.AccessTokenScope(strings.Join([]string{"write:user", "write:repository"}, ",")).Normalize()
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return "", err
|
|
|
|
|
|
}
|
|
|
|
|
|
token.Scope = scope
|
|
|
|
|
|
err = auth_model.NewAccessToken(db.DefaultContext, token)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return "", err
|
|
|
|
|
|
}
|
|
|
|
|
|
ctx.Session.Set("terminal_login_token", token.Token)
|
|
|
|
|
|
access_token = token.Token
|
2025-05-28 01:30:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 根据不同的代理类型获取 SSH 端口
|
|
|
|
|
|
var port string
|
|
|
|
|
|
|
|
|
|
|
|
switch setting.Devcontainer.Agent {
|
|
|
|
|
|
case setting.KUBERNETES, "k8s":
|
|
|
|
|
|
// 创建 K8s 客户端
|
|
|
|
|
|
apiRequestContext := ctx.Req.Context()
|
|
|
|
|
|
k8sClient, err := devcontainer_k8s_agent_module.GetKubernetesClient(&apiRequestContext)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
log.Error("Get_IDE_TerminalURL: 创建 K8s 客户端失败: %v", err)
|
|
|
|
|
|
return "", err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 获取 DevcontainerApp 资源
|
|
|
|
|
|
opts := &devcontainer_k8s_agent_module.GetDevcontainerOptions{
|
|
|
|
|
|
GetOptions: metav1.GetOptions{},
|
|
|
|
|
|
Name: devcontainer.DevContainerName,
|
|
|
|
|
|
Namespace: setting.Devcontainer.Namespace,
|
|
|
|
|
|
Wait: false,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
log.Info("Get_IDE_TerminalURL: 从 K8s 获取 DevcontainerApp %s, namespace=%s",
|
|
|
|
|
|
devcontainer.DevContainerName, setting.Devcontainer.Namespace)
|
|
|
|
|
|
|
|
|
|
|
|
devcontainerApp, err := devcontainer_k8s_agent_module.GetDevcontainer(&apiRequestContext, k8sClient, opts)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
log.Error("Get_IDE_TerminalURL: 获取 DevcontainerApp 失败: %v", err)
|
|
|
|
|
|
return "", err
|
|
|
|
|
|
}
|
2025-03-29 06:26:42 +00:00
|
|
|
|
|
2025-05-28 01:30:51 +08:00
|
|
|
|
// 使用 NodePort 作为 SSH 端口
|
|
|
|
|
|
port = fmt.Sprintf("%d", devcontainerApp.Status.NodePortAssigned)
|
|
|
|
|
|
log.Info("Get_IDE_TerminalURL: K8s 环境使用 NodePort %s 作为 SSH 端口", port)
|
|
|
|
|
|
|
|
|
|
|
|
case setting.DOCKER:
|
|
|
|
|
|
// 原有 Docker 处理逻辑
|
|
|
|
|
|
defalut_ctx := context.Background()
|
|
|
|
|
|
cli, err := docker.CreateDockerClient(&defalut_ctx)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return "", err
|
|
|
|
|
|
}
|
|
|
|
|
|
defer cli.Close()
|
|
|
|
|
|
|
|
|
|
|
|
containerID, err := docker.GetContainerID(cli, devcontainer.DevContainerName)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return "", err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
mappedPort, err := docker.GetMappedPort(cli, containerID, "22")
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return "", err
|
|
|
|
|
|
}
|
|
|
|
|
|
port = mappedPort
|
|
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
|
return "", fmt.Errorf("不支持的 DevContainer Agent 类型: %s", setting.Devcontainer.Agent)
|
2025-03-18 15:52:08 +00:00
|
|
|
|
}
|
2025-05-28 01:30:51 +08:00
|
|
|
|
|
2025-07-03 08:24:54 +08:00
|
|
|
|
// 加载配置文件
|
|
|
|
|
|
cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
log.Error("Get_IDE_TerminalURL: 加载配置文件失败: %v", err)
|
|
|
|
|
|
return "", err
|
|
|
|
|
|
}
|
|
|
|
|
|
log.Info("Get_IDE_TerminalURL: 配置文件加载成功, ROOT_URL=%s", cfg.Section("server").Key("ROOT_URL").Value())
|
2025-05-28 01:30:51 +08:00
|
|
|
|
// 构建并返回 URL
|
2025-03-29 06:26:42 +00:00
|
|
|
|
return "://mengning.devstar/" +
|
2025-05-22 03:17:18 -07:00
|
|
|
|
"openProject?host=" + devcontainer.RepoName +
|
|
|
|
|
|
"&hostname=" + devcontainer.DevContainerHost +
|
2025-03-18 15:52:08 +00:00
|
|
|
|
"&port=" + port +
|
|
|
|
|
|
"&username=" + devcontainer.DevContainerUsername +
|
|
|
|
|
|
"&path=" + devcontainer.DevContainerWorkDir +
|
2025-03-29 06:26:42 +00:00
|
|
|
|
"&access_token=" + access_token +
|
2025-07-03 08:24:54 +08:00
|
|
|
|
"&devstar_username=" + devcontainer.RepoOwnerName +
|
|
|
|
|
|
"&devstar_domain=" + cfg.Section("server").Key("ROOT_URL").Value(), nil
|
2025-03-18 15:52:08 +00:00
|
|
|
|
}
|
2024-08-30 12:28:59 +00:00
|
|
|
|
|
2025-03-29 06:26:42 +00:00
|
|
|
|
func AddPublicKeyToAllRunningDevContainer(ctx context.Context, user *user_model.User, publicKey string) error {
|
|
|
|
|
|
switch setting.Devcontainer.Agent {
|
2025-05-20 12:06:35 +08:00
|
|
|
|
case setting.KUBERNETES, "k8s":
|
2025-07-03 08:24:54 +08:00
|
|
|
|
log.Info("AddPublicKeyToAllRunningDevContainer: 开始为用户 %s (ID=%d) 的所有运行中容器添加公钥",
|
|
|
|
|
|
user.Name, user.ID)
|
|
|
|
|
|
|
|
|
|
|
|
// 1. 获取用户的所有 DevContainer
|
|
|
|
|
|
opts := &SearchUserDevcontainerListItemVoOptions{
|
|
|
|
|
|
Actor: user,
|
|
|
|
|
|
}
|
|
|
|
|
|
userDevcontainersVO, err := GetUserDevcontainersList(ctx, opts)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
log.Error("AddPublicKeyToAllRunningDevContainer: 获取用户容器列表失败: %v", err)
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
repoDevContainerList := userDevcontainersVO.DevContainers
|
|
|
|
|
|
if len(repoDevContainerList) == 0 {
|
|
|
|
|
|
log.Info("AddPublicKeyToAllRunningDevContainer: 用户 %s 没有任何 DevContainer", user.Name)
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
log.Info("AddPublicKeyToAllRunningDevContainer: 找到 %d 个 DevContainer", len(repoDevContainerList))
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 获取 K8s 客户端
|
|
|
|
|
|
k8sClient, err := devcontainer_k8s_agent_module.GetKubernetesClient(&ctx)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
log.Error("AddPublicKeyToAllRunningDevContainer: 获取 K8s 客户端失败: %v", err)
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 3. 获取标准 K8s 客户端用于执行命令
|
|
|
|
|
|
stdClient, err := getStandardKubernetesClient()
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
log.Error("AddPublicKeyToAllRunningDevContainer: 获取标准 K8s 客户端失败: %v", err)
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 4. 遍历所有容器,检查状态并添加公钥
|
|
|
|
|
|
successCount := 0
|
|
|
|
|
|
errorCount := 0
|
|
|
|
|
|
|
|
|
|
|
|
for _, repoDevContainer := range repoDevContainerList {
|
|
|
|
|
|
log.Info("AddPublicKeyToAllRunningDevContainer: 处理容器 %s", repoDevContainer.DevContainerName)
|
|
|
|
|
|
|
|
|
|
|
|
// 4.1 检查 DevContainer 是否运行
|
|
|
|
|
|
getOpts := &devcontainer_k8s_agent_module.GetDevcontainerOptions{
|
|
|
|
|
|
GetOptions: metav1.GetOptions{},
|
|
|
|
|
|
Name: repoDevContainer.DevContainerName,
|
|
|
|
|
|
Namespace: setting.Devcontainer.Namespace,
|
|
|
|
|
|
Wait: false,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
devcontainerApp, err := devcontainer_k8s_agent_module.GetDevcontainer(&ctx, k8sClient, getOpts)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
log.Error("AddPublicKeyToAllRunningDevContainer: 获取容器 %s 状态失败: %v",
|
|
|
|
|
|
repoDevContainer.DevContainerName, err)
|
|
|
|
|
|
errorCount++
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 4.2 检查容器是否就绪
|
|
|
|
|
|
if !devcontainerApp.Status.Ready {
|
|
|
|
|
|
log.Info("AddPublicKeyToAllRunningDevContainer: 容器 %s 未就绪,跳过",
|
|
|
|
|
|
repoDevContainer.DevContainerName)
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
log.Info("AddPublicKeyToAllRunningDevContainer: 容器 %s 就绪,开始添加公钥",
|
|
|
|
|
|
repoDevContainer.DevContainerName)
|
|
|
|
|
|
|
|
|
|
|
|
// 4.3 构建添加公钥的命令
|
|
|
|
|
|
// 使用更安全的方式添加公钥,避免重复添加
|
|
|
|
|
|
addKeyCommand := fmt.Sprintf(`
|
|
|
|
|
|
# 确保 .ssh 目录存在
|
|
|
|
|
|
mkdir -p ~/.ssh
|
|
|
|
|
|
chmod 700 ~/.ssh
|
|
|
|
|
|
|
|
|
|
|
|
# 检查公钥是否已存在
|
|
|
|
|
|
if ! grep -Fxq "%s" ~/.ssh/authorized_keys 2>/dev/null; then
|
|
|
|
|
|
echo "%s" >> ~/.ssh/authorized_keys
|
|
|
|
|
|
chmod 600 ~/.ssh/authorized_keys
|
|
|
|
|
|
echo "Public key added successfully"
|
|
|
|
|
|
else
|
|
|
|
|
|
echo "Public key already exists"
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
# 验证文件内容
|
|
|
|
|
|
wc -l ~/.ssh/authorized_keys
|
|
|
|
|
|
`, publicKey, publicKey)
|
|
|
|
|
|
|
|
|
|
|
|
// 4.4 在容器中执行命令
|
|
|
|
|
|
err = executeCommandInK8sPod(&ctx, stdClient,
|
|
|
|
|
|
setting.Devcontainer.Namespace,
|
|
|
|
|
|
repoDevContainer.DevContainerName, // 传递 DevContainer 名称而不是 Pod 名称
|
|
|
|
|
|
repoDevContainer.DevContainerName, // 容器名通常与 DevContainer 名相同
|
|
|
|
|
|
[]string{"/bin/bash", "-c", addKeyCommand})
|
|
|
|
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
log.Error("AddPublicKeyToAllRunningDevContainer: 在容器 %s 中执行添加公钥命令失败: %v",
|
|
|
|
|
|
repoDevContainer.DevContainerName, err)
|
|
|
|
|
|
errorCount++
|
|
|
|
|
|
} else {
|
|
|
|
|
|
log.Info("AddPublicKeyToAllRunningDevContainer: 成功为容器 %s 添加公钥",
|
|
|
|
|
|
repoDevContainer.DevContainerName)
|
|
|
|
|
|
successCount++
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
log.Info("AddPublicKeyToAllRunningDevContainer: 完成处理 - 成功: %d, 失败: %d",
|
|
|
|
|
|
successCount, errorCount)
|
|
|
|
|
|
|
|
|
|
|
|
if errorCount > 0 && successCount == 0 {
|
|
|
|
|
|
return fmt.Errorf("所有容器添加公钥都失败了,错误数量: %d", errorCount)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
|
|
2025-03-29 06:26:42 +00:00
|
|
|
|
case setting.DOCKER:
|
|
|
|
|
|
cli, err := docker.CreateDockerClient(&ctx)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
defer cli.Close()
|
|
|
|
|
|
|
|
|
|
|
|
// 查询所有打开的容器
|
|
|
|
|
|
opts := &SearchUserDevcontainerListItemVoOptions{
|
|
|
|
|
|
Actor: user,
|
|
|
|
|
|
}
|
|
|
|
|
|
userDevcontainersVO, err := GetUserDevcontainersList(ctx, opts)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
repoDevContainerlist := userDevcontainersVO.DevContainers
|
|
|
|
|
|
if len(repoDevContainerlist) > 0 {
|
|
|
|
|
|
// 将公钥写入这些打开的容器中
|
|
|
|
|
|
for _, repoDevContainer := range repoDevContainerlist {
|
|
|
|
|
|
containerID, err := docker.GetContainerID(cli, repoDevContainer.DevContainerName)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
log.Info("container id: %s, name: %s", containerID, repoDevContainer.DevContainerName)
|
|
|
|
|
|
// 检查容器状态
|
|
|
|
|
|
containerStatus, err := docker.GetContainerStatus(cli, containerID)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if containerStatus == "running" {
|
|
|
|
|
|
// 只为处于运行状态的容器添加公钥
|
|
|
|
|
|
_, err = docker.ExecCommandInContainer(&ctx, cli, containerID, fmt.Sprintf("echo '%s' >> ~/.ssh/authorized_keys", publicKey))
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return nil
|
2025-07-03 08:24:54 +08:00
|
|
|
|
|
2025-03-29 06:26:42 +00:00
|
|
|
|
default:
|
2025-07-03 08:24:54 +08:00
|
|
|
|
return fmt.Errorf("unknown agent: %s", setting.Devcontainer.Agent)
|
2025-03-29 06:26:42 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-08-30 12:28:59 +00:00
|
|
|
|
// DeleteRepoDevcontainer 按照 仓库 和/或 用户信息删除 DevContainer(s)
|
2025-02-17 05:36:49 +00:00
|
|
|
|
func DeleteRepoDevcontainer(ctx context.Context, opts *RepoDevcontainerOptions) error {
|
2025-05-20 12:06:35 +08:00
|
|
|
|
log.Info("DeleteRepoDevcontainer: 开始删除 DevContainer")
|
|
|
|
|
|
|
2024-08-30 12:28:59 +00:00
|
|
|
|
if ctx == nil || opts == nil || (opts.Actor == nil && opts.Repository == nil) {
|
2025-05-20 12:06:35 +08:00
|
|
|
|
log.Error("DeleteRepoDevcontainer: 参数无效 ctx=%v, opts=%v, actor=%v, repo=%v",
|
|
|
|
|
|
ctx != nil, opts != nil,
|
|
|
|
|
|
opts != nil && opts.Actor != nil,
|
|
|
|
|
|
opts != nil && opts.Repository != nil)
|
2025-03-18 15:52:08 +00:00
|
|
|
|
return devcontainer_models_errors.ErrFailedToOperateDevcontainerDB{
|
2024-08-30 12:28:59 +00:00
|
|
|
|
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})
|
2025-05-20 12:06:35 +08:00
|
|
|
|
log.Info("DeleteRepoDevcontainer: 添加用户条件, userID=%d", opts.Actor.ID)
|
2024-08-30 12:28:59 +00:00
|
|
|
|
}
|
|
|
|
|
|
if opts.Repository != nil {
|
|
|
|
|
|
sqlDevcontainerCondition = sqlDevcontainerCondition.And(builder.Eq{"repo_id": opts.Repository.ID})
|
2025-05-20 12:06:35 +08:00
|
|
|
|
log.Info("DeleteRepoDevcontainer: 添加仓库条件, repoID=%d", opts.Repository.ID)
|
2024-08-30 12:28:59 +00:00
|
|
|
|
}
|
2025-05-20 12:06:35 +08:00
|
|
|
|
|
|
|
|
|
|
log.Info("DeleteRepoDevcontainer: 查询条件构建完成: %v", sqlDevcontainerCondition)
|
2025-03-18 15:52:08 +00:00
|
|
|
|
var devcontainersList []devcontainer_model.Devcontainer
|
2024-08-30 12:28:59 +00:00
|
|
|
|
|
|
|
|
|
|
// 2. 开启事务:先获取 devcontainer列表,后删除
|
|
|
|
|
|
dbTransactionErr := db.WithTx(ctx, func(ctx context.Context) error {
|
|
|
|
|
|
var err error
|
2025-05-20 12:06:35 +08:00
|
|
|
|
log.Info("DeleteRepoDevcontainer: 开始数据库事务")
|
|
|
|
|
|
|
2024-08-30 12:28:59 +00:00
|
|
|
|
// 2.1 条件查询: user_id 和/或 repo_id
|
2025-05-20 12:06:35 +08:00
|
|
|
|
log.Info("DeleteRepoDevcontainer: 查询符合条件的 DevContainer")
|
2024-08-30 12:28:59 +00:00
|
|
|
|
err = db.GetEngine(ctx).
|
2025-03-18 15:52:08 +00:00
|
|
|
|
Table("devcontainer").
|
2024-08-30 12:28:59 +00:00
|
|
|
|
Where(sqlDevcontainerCondition).
|
|
|
|
|
|
Find(&devcontainersList)
|
|
|
|
|
|
if err != nil {
|
2025-05-20 12:06:35 +08:00
|
|
|
|
log.Error("DeleteRepoDevcontainer: 查询失败: %v", err)
|
2025-03-18 15:52:08 +00:00
|
|
|
|
return devcontainer_models_errors.ErrFailedToOperateDevcontainerDB{
|
2024-08-30 12:28:59 +00:00
|
|
|
|
Action: fmt.Sprintf("find devcontainer(s) with condition '%v'", sqlDevcontainerCondition),
|
|
|
|
|
|
Message: err.Error(),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-05-20 12:06:35 +08:00
|
|
|
|
log.Info("DeleteRepoDevcontainer: 找到 %d 个符合条件的 DevContainer", len(devcontainersList))
|
2024-08-30 12:28:59 +00:00
|
|
|
|
|
2024-09-30 06:48:01 +00:00
|
|
|
|
// 2.2 空列表,直接结束事务(由于前一个操作只是查询,所以回滚事务不会导致数据不一致问题)
|
|
|
|
|
|
if len(devcontainersList) == 0 {
|
2025-05-20 12:06:35 +08:00
|
|
|
|
log.Warn("DeleteRepoDevcontainer: 未找到符合条件的 DevContainer")
|
2025-03-18 15:52:08 +00:00
|
|
|
|
return devcontainer_models_errors.ErrFailedToOperateDevcontainerDB{
|
2024-09-30 06:48:01 +00:00
|
|
|
|
Action: fmt.Sprintf("find devcontainer(s) with condition '%v'", sqlDevcontainerCondition),
|
|
|
|
|
|
Message: "No DevContainer found",
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 2.3 条件删除: user_id 和/或 repo_id
|
2025-05-20 12:06:35 +08:00
|
|
|
|
log.Info("DeleteRepoDevcontainer: 从数据库删除 DevContainer 记录")
|
|
|
|
|
|
rowsAffected, err := db.GetEngine(ctx).
|
2025-03-18 15:52:08 +00:00
|
|
|
|
Table("devcontainer").
|
2024-08-30 12:28:59 +00:00
|
|
|
|
Where(sqlDevcontainerCondition).
|
|
|
|
|
|
Delete()
|
|
|
|
|
|
if err != nil {
|
2025-05-20 12:06:35 +08:00
|
|
|
|
log.Error("DeleteRepoDevcontainer: 删除 DevContainer 记录失败: %v", err)
|
2025-03-18 15:52:08 +00:00
|
|
|
|
return devcontainer_models_errors.ErrFailedToOperateDevcontainerDB{
|
2024-08-30 12:28:59 +00:00
|
|
|
|
Action: fmt.Sprintf("MARK devcontainer(s) as DELETED with condition '%v'", sqlDevcontainerCondition),
|
|
|
|
|
|
Message: err.Error(),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-05-20 12:06:35 +08:00
|
|
|
|
log.Info("DeleteRepoDevcontainer: DevContainer 记录删除成功, 影响行数=%d", rowsAffected)
|
|
|
|
|
|
|
|
|
|
|
|
// 删除对应的输出记录
|
|
|
|
|
|
log.Info("DeleteRepoDevcontainer: 删除 DevContainer 输出记录")
|
|
|
|
|
|
outputRowsAffected, err := db.GetEngine(ctx).
|
2025-03-18 15:52:08 +00:00
|
|
|
|
Table("devcontainer_output").
|
|
|
|
|
|
Where(sqlDevcontainerCondition).
|
|
|
|
|
|
Delete()
|
2025-05-20 12:06:35 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
log.Error("DeleteRepoDevcontainer: 删除输出记录失败: %v", err)
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
log.Info("DeleteRepoDevcontainer: DevContainer 输出记录删除成功, 影响行数=%d", outputRowsAffected)
|
|
|
|
|
|
|
2024-08-30 12:28:59 +00:00
|
|
|
|
return nil
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
if dbTransactionErr != nil {
|
2025-05-20 12:06:35 +08:00
|
|
|
|
log.Error("DeleteRepoDevcontainer: 数据库操作失败: %v", dbTransactionErr)
|
2024-08-30 12:28:59 +00:00
|
|
|
|
return dbTransactionErr
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 3. 后台启动一个goroutine慢慢回收 Dev Container 资源 (如果回收失败,将会产生孤儿 Dev Container,只能管理员手动识别、删除)
|
2025-05-20 12:06:35 +08:00
|
|
|
|
log.Info("DeleteRepoDevcontainer: 启动异步资源回收, DevContainer数量=%d", len(devcontainersList))
|
2024-08-30 12:28:59 +00:00
|
|
|
|
go func() {
|
2024-09-30 06:48:01 +00:00
|
|
|
|
// 注意:由于执行删除 k8s 资源 与 数据库交互和Web页面更新是异步的,因此在 goroutine 中必须重新创建 context,否则报错:
|
|
|
|
|
|
// Delete "https://192.168.49.2:8443/apis/devcontainer.devstar.cn/v1/...": context canceled
|
|
|
|
|
|
isolatedContextToPurgeK8sResource, cancelFunc := context.WithCancel(context.Background())
|
|
|
|
|
|
defer cancelFunc()
|
|
|
|
|
|
|
2025-05-20 12:06:35 +08:00
|
|
|
|
err := purgeDevcontainersResource(&isolatedContextToPurgeK8sResource, &devcontainersList)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
log.Error("DeleteRepoDevcontainer: 异步资源回收失败: %v", err)
|
|
|
|
|
|
}
|
2024-08-30 12:28:59 +00:00
|
|
|
|
}()
|
|
|
|
|
|
|
2025-05-20 12:06:35 +08:00
|
|
|
|
log.Info("DeleteRepoDevcontainer: DevContainer 删除操作完成")
|
2024-08-30 12:28:59 +00:00
|
|
|
|
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])
|
|
|
|
|
|
}
|
2024-10-23 03:05:44 +00:00
|
|
|
|
if len(sanitizedRepoName) > 31 {
|
|
|
|
|
|
sanitizedRepoName = strings.ToLower(sanitizedRepoName[:31])
|
2024-08-30 12:28:59 +00:00
|
|
|
|
}
|
|
|
|
|
|
newUUID, _ := uuid.NewUUID()
|
|
|
|
|
|
uuidStr := newUUID.String()
|
2024-10-23 03:05:44 +00:00
|
|
|
|
uuidStr = regexpNonAlphaNum.ReplaceAllString(uuidStr, "")[:15]
|
2024-08-30 12:28:59 +00:00
|
|
|
|
return fmt.Sprintf("%s-%s-%s", sanitizedUsername, sanitizedRepoName, uuidStr)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-09-30 06:48:01 +00:00
|
|
|
|
// purgeDevcontainersResource 辅助函数,用于goroutine后台执行,回收DevContainer资源
|
2025-03-18 15:52:08 +00:00
|
|
|
|
func purgeDevcontainersResource(ctx *context.Context, devcontainersList *[]devcontainer_model.Devcontainer) error {
|
2024-09-30 06:48:01 +00:00
|
|
|
|
// 1. 检查 DevContainer 功能是否启用,若禁用,则直接结束,不会真正执行删除操作
|
2025-01-07 01:25:54 +00:00
|
|
|
|
if !setting.Devcontainer.Enabled {
|
2025-05-20 12:06:35 +08:00
|
|
|
|
log.Warn("purgeDevcontainersResource: DevContainer 功能已全局禁用, 跳过资源回收")
|
2024-09-30 06:48:01 +00:00
|
|
|
|
// 如果用户设置禁用 DevContainer,无法删除资源,会直接忽略,而数据库相关记录会继续清空、不会发生回滚
|
2025-01-07 01:25:54 +00:00
|
|
|
|
log.Warn("Orphan DevContainers in namespace `%s` left undeleted: %v", setting.Devcontainer.Namespace, devcontainersList)
|
2024-09-30 06:48:01 +00:00
|
|
|
|
return nil
|
|
|
|
|
|
}
|
2025-05-20 12:06:35 +08:00
|
|
|
|
|
2024-09-30 06:48:01 +00:00
|
|
|
|
// 2. 根据配置文件中指定的 DevContainer Agent 派遣创建任务
|
2025-01-07 01:25:54 +00:00
|
|
|
|
switch setting.Devcontainer.Agent {
|
2025-05-20 12:06:35 +08:00
|
|
|
|
case setting.KUBERNETES, "k8s":
|
|
|
|
|
|
log.Info("purgeDevcontainersResource: 调用 K8s Operator 删除 %d 个资源", len(*devcontainersList))
|
|
|
|
|
|
err := AssignDevcontainerDeletion2K8sOperator(ctx, devcontainersList)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
log.Error("purgeDevcontainersResource: K8s 资源删除失败: %v", err)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
log.Info("purgeDevcontainersResource: K8s 资源删除成功")
|
|
|
|
|
|
}
|
|
|
|
|
|
return err
|
2025-01-07 01:25:54 +00:00
|
|
|
|
case setting.DOCKER:
|
2025-02-17 05:36:49 +00:00
|
|
|
|
return DeleteDevcontainer(ctx, devcontainersList)
|
2024-09-30 06:48:01 +00:00
|
|
|
|
default:
|
|
|
|
|
|
// 未知 Agent,直接报错
|
2025-03-18 15:52:08 +00:00
|
|
|
|
return devcontainer_models_errors.ErrFailedToOperateDevcontainerDB{
|
2024-09-30 06:48:01 +00:00
|
|
|
|
Action: "dispatch DevContainer deletion",
|
2025-01-07 01:25:54 +00:00
|
|
|
|
Message: fmt.Sprintf("unknown DevContainer agent `%s`, please check your config files", setting.Devcontainer.Agent),
|
2024-09-30 06:48:01 +00:00
|
|
|
|
}
|
2024-08-30 12:28:59 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-09-30 06:48:01 +00:00
|
|
|
|
// claimDevcontainerResource 分发创建 DevContainer 任务到配置文件指定的执行器
|
2025-03-18 15:52:08 +00:00
|
|
|
|
func claimDevcontainerResource(ctx *context.Context, newDevContainer *CreateDevcontainerDTO, devContainerJSON *DevStarJSON) error {
|
2025-05-20 12:06:35 +08:00
|
|
|
|
log.Info("claimDevcontainerResource: 开始分发 DevContainer 创建任务, name=%s", newDevContainer.Name)
|
|
|
|
|
|
|
2024-09-30 06:48:01 +00:00
|
|
|
|
// 1. 检查 DevContainer 功能是否启用,若禁用,则直接结束
|
2025-01-07 01:25:54 +00:00
|
|
|
|
if !setting.Devcontainer.Enabled {
|
2025-05-20 12:06:35 +08:00
|
|
|
|
log.Error("claimDevcontainerResource: DevContainer 功能已全局禁用")
|
2025-03-18 15:52:08 +00:00
|
|
|
|
return devcontainer_models_errors.ErrFailedToOperateDevcontainerDB{
|
2024-09-30 06:48:01 +00:00
|
|
|
|
Action: "Check for DevContainer functionality switch",
|
|
|
|
|
|
Message: "DevContainer is disabled globally, please check your configuration files",
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-05-07 11:10:30 +00:00
|
|
|
|
// 解析仓库 URL
|
|
|
|
|
|
parsedURL, err := url.Parse(newDevContainer.GitRepositoryURL)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
log.Info("解析仓库URL失败: %v", err)
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
hostParts := strings.Split(parsedURL.Host, ":")
|
|
|
|
|
|
port := ""
|
|
|
|
|
|
if len(hostParts) > 1 {
|
|
|
|
|
|
port = hostParts[1]
|
|
|
|
|
|
}
|
|
|
|
|
|
newHost := "host.docker.internal"
|
|
|
|
|
|
if port != "" {
|
|
|
|
|
|
newHost += ":" + port
|
|
|
|
|
|
}
|
|
|
|
|
|
parsedURL.Host = newHost
|
|
|
|
|
|
// 生成git仓库的 URL
|
|
|
|
|
|
newURL := parsedURL.String()
|
2025-08-11 11:29:50 +08:00
|
|
|
|
newDevContainer.GitRepositoryURL = newURL
|
2025-05-07 11:10:30 +00:00
|
|
|
|
// Read the init script from file
|
|
|
|
|
|
var initializeScriptContent, restartScriptContent []byte
|
|
|
|
|
|
_, err = os.Stat("devcontainer_init.sh")
|
|
|
|
|
|
if os.IsNotExist(err) {
|
|
|
|
|
|
_, err = os.Stat("/app/gitea/devcontainer_init.sh")
|
|
|
|
|
|
if os.IsNotExist(err) {
|
|
|
|
|
|
return fmt.Errorf("读取初始化脚本失败: %v", err)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
initializeScriptContent, err = os.ReadFile("/app/gitea/devcontainer_init.sh")
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return fmt.Errorf("读取初始化脚本失败: %v", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
initializeScriptContent, err = os.ReadFile("devcontainer_init.sh")
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return fmt.Errorf("读取初始化脚本失败: %v", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
_, err = os.Stat("devcontainer_restart.sh")
|
|
|
|
|
|
if os.IsNotExist(err) {
|
|
|
|
|
|
_, err = os.Stat("/app/gitea/devcontainer_restart.sh")
|
|
|
|
|
|
if os.IsNotExist(err) {
|
|
|
|
|
|
return fmt.Errorf("读取初始化脚本失败: %v", err)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
restartScriptContent, err = os.ReadFile("/app/gitea/devcontainer_restart.sh")
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return fmt.Errorf("读取初始化脚本失败: %v", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
restartScriptContent, err = os.ReadFile("devcontainer_restart.sh")
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return fmt.Errorf("读取初始化脚本失败: %v", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
log.Error("Failed to load custom conf '%s': %v", setting.CustomConf, err)
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
initializeScript := strings.ReplaceAll(string(initializeScriptContent), "$AUTHORIZED_KEYS", strings.Join(newDevContainer.SSHPublicKeyList, "\n"))
|
|
|
|
|
|
initializeScript = strings.ReplaceAll(initializeScript, "$HOST_DOCKER_INTERNAL", cfg.Section("server").Key("DOMAIN").Value())
|
|
|
|
|
|
initializeScript = strings.ReplaceAll(initializeScript, "$WORKDIR", newDevContainer.DevcontainerWorkDir)
|
|
|
|
|
|
initializeScript = strings.ReplaceAll(initializeScript, "$REPO_URL", newURL)
|
|
|
|
|
|
restartScript := strings.ReplaceAll(string(restartScriptContent), "$WORKDIR", newDevContainer.DevcontainerWorkDir)
|
2024-09-30 06:48:01 +00:00
|
|
|
|
// 2. 根据配置文件中指定的 DevContainer Agent 派遣创建任务
|
2025-05-20 12:06:35 +08:00
|
|
|
|
log.Info("claimDevcontainerResource: 使用 %s Agent 创建 DevContainer", setting.Devcontainer.Agent)
|
2025-01-07 01:25:54 +00:00
|
|
|
|
switch setting.Devcontainer.Agent {
|
|
|
|
|
|
case setting.KUBERNETES, "k8s":
|
2024-09-30 06:48:01 +00:00
|
|
|
|
// k8s Operator
|
2025-05-20 12:06:35 +08:00
|
|
|
|
log.Info("claimDevcontainerResource: 调用 K8s Operator 创建 DevContainer, image=%s",
|
|
|
|
|
|
newDevContainer.Image)
|
|
|
|
|
|
err := AssignDevcontainerCreation2K8sOperator(ctx, newDevContainer)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
log.Error("claimDevcontainerResource: K8s 创建 DevContainer 失败: %v", err)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
log.Info("claimDevcontainerResource: K8s 创建 DevContainer 成功, nodePort=%d",
|
|
|
|
|
|
newDevContainer.DevcontainerPort)
|
|
|
|
|
|
}
|
|
|
|
|
|
return err
|
2025-01-07 01:25:54 +00:00
|
|
|
|
case setting.DOCKER:
|
2025-05-07 11:10:30 +00:00
|
|
|
|
return CreateDevcontainer(ctx, newDevContainer, devContainerJSON, initializeScript, restartScript)
|
2024-09-30 06:48:01 +00:00
|
|
|
|
default:
|
|
|
|
|
|
// 未知 Agent,直接报错
|
2025-03-18 15:52:08 +00:00
|
|
|
|
return devcontainer_models_errors.ErrFailedToOperateDevcontainerDB{
|
2024-09-30 06:48:01 +00:00
|
|
|
|
Action: "dispatch DevContainer creation",
|
2025-01-07 01:25:54 +00:00
|
|
|
|
Message: fmt.Sprintf("unknown DevContainer agent `%s`, please check your config files", setting.Devcontainer.Agent),
|
2024-09-30 06:48:01 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2024-08-30 12:28:59 +00:00
|
|
|
|
}
|
2025-05-07 11:10:30 +00:00
|
|
|
|
func RestartDevcontainer(gitea_ctx gitea_context.Context, opts *RepoDevContainer) error {
|
2025-05-20 12:06:35 +08:00
|
|
|
|
log.Info("RestartDevcontainer: 开始重启 DevContainer, name=%s", opts.DevContainerName)
|
2025-05-07 11:10:30 +00:00
|
|
|
|
|
|
|
|
|
|
switch setting.Devcontainer.Agent {
|
2025-05-20 12:06:35 +08:00
|
|
|
|
case setting.KUBERNETES, "k8s":
|
|
|
|
|
|
log.Info("RestartDevcontainer: 使用 K8s Agent 重启容器 %s", opts.DevContainerName)
|
2025-05-13 10:50:26 +00:00
|
|
|
|
ctx := gitea_ctx.Req.Context()
|
2025-05-20 12:06:35 +08:00
|
|
|
|
err := AssignDevcontainerRestart2K8sOperator(&ctx, opts)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
log.Error("RestartDevcontainer: K8s 重启容器失败: %v", err)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
log.Info("RestartDevcontainer: K8s 重启容器成功")
|
|
|
|
|
|
}
|
|
|
|
|
|
return err
|
2025-05-07 11:10:30 +00:00
|
|
|
|
case setting.DOCKER:
|
|
|
|
|
|
return DockerRestartContainer(&gitea_ctx, opts)
|
|
|
|
|
|
default:
|
|
|
|
|
|
return fmt.Errorf("不支持的Agent")
|
|
|
|
|
|
//默认处理
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
func StopDevcontainer(gitea_ctx context.Context, opts *RepoDevContainer) error {
|
2025-05-20 12:06:35 +08:00
|
|
|
|
log.Info("StopDevcontainer: 开始停止 DevContainer, name=%s", opts.DevContainerName)
|
|
|
|
|
|
|
2025-05-07 11:10:30 +00:00
|
|
|
|
switch setting.Devcontainer.Agent {
|
2025-05-20 12:06:35 +08:00
|
|
|
|
case setting.KUBERNETES, "k8s":
|
|
|
|
|
|
log.Info("StopDevcontainer: 使用 K8s Agent 停止容器 %s", opts.DevContainerName)
|
|
|
|
|
|
err := AssignDevcontainerStop2K8sOperator(&gitea_ctx, opts)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
log.Error("StopDevcontainer: K8s 停止容器失败: %v", err)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
log.Info("StopDevcontainer: K8s 停止容器成功")
|
|
|
|
|
|
}
|
|
|
|
|
|
return err
|
2025-05-07 11:10:30 +00:00
|
|
|
|
case setting.DOCKER:
|
|
|
|
|
|
return DockerStopContainer(&gitea_ctx, opts)
|
|
|
|
|
|
default:
|
|
|
|
|
|
return fmt.Errorf("不支持的Agent")
|
|
|
|
|
|
//默认处理
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
}
|
2025-05-27 23:58:00 +08:00
|
|
|
|
func CanCreateDevcontainer(gitea_ctx context.Context, repoID, userID int64) (bool, error) {
|
|
|
|
|
|
e := db.GetEngine(gitea_ctx)
|
|
|
|
|
|
|
|
|
|
|
|
teamMember, err := e.Table("team_user").
|
|
|
|
|
|
Join("INNER", "team_repo", "`team_repo`.team_id = `team_user`.team_id").
|
|
|
|
|
|
Join("INNER", "team_unit", "`team_unit`.team_id = `team_user`.team_id").
|
|
|
|
|
|
Where("`team_repo`.repo_id = ? AND (`team_unit`.access_mode >= ? OR (`team_unit`.access_mode = ? AND `team_unit`.`type` = ?))",
|
|
|
|
|
|
repoID, perm.AccessModeWrite, perm.AccessModeRead, unit.TypeCode).
|
|
|
|
|
|
And("team_user.uid = ?", userID).Exist()
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return false, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
if teamMember {
|
|
|
|
|
|
return true, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return repo_model.IsCollaborator(gitea_ctx, repoID, userID)
|
|
|
|
|
|
|
|
|
|
|
|
}
|
2025-05-29 11:13:22 +08:00
|
|
|
|
func IsOwner(gitea_ctx context.Context, repoID, userID int64) (bool, error) {
|
|
|
|
|
|
e := db.GetEngine(gitea_ctx)
|
|
|
|
|
|
|
|
|
|
|
|
teamMember, err := e.Table("team_user").
|
|
|
|
|
|
Join("INNER", "team_repo", "`team_repo`.team_id = `team_user`.team_id").
|
|
|
|
|
|
Join("INNER", "team_unit", "`team_unit`.team_id = `team_user`.team_id").
|
|
|
|
|
|
Where("`team_repo`.repo_id = ? AND `team_unit`.access_mode = ? ",
|
|
|
|
|
|
repoID, perm.AccessModeAdmin).
|
|
|
|
|
|
And("team_user.uid = ?", userID).Exist()
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return false, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
if teamMember {
|
|
|
|
|
|
return true, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return e.Get(&repo_model.Collaboration{RepoID: repoID, UserID: userID, Mode: 3})
|
2025-05-27 23:58:00 +08:00
|
|
|
|
}
|