package devcontainer import ( "context" "fmt" "io/ioutil" "net/url" "os" "path/filepath" "regexp" "strings" "time" auth_model "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/db" devcontainer_model "code.gitea.io/gitea/models/devcontainer" devcontainer_models_errors "code.gitea.io/gitea/models/devcontainer/errors" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/docker" devcontainer_k8s_agent_module "code.gitea.io/gitea/modules/k8s" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" gitea_context "code.gitea.io/gitea/services/context" devcontainer_service_errors "code.gitea.io/gitea/services/devcontainer/errors" "code.gitea.io/gitea/services/devstar_ssh_key_pair/api_service" "github.com/google/uuid" "xorm.io/builder" ) // 封装查询 DevContainer 实时信息,与具体 agent 无关,返回前端 type OpenDevcontainerAbstractAgent struct { NodePortAssigned uint16 } // OpenDevcontainerService 获取 DevContainer 连接信息,抽象方法,适配多种 DevContainer Agent func OpenDevcontainerService(ctx *gitea_context.Context, opts *OpenDevcontainerAppDispatcherOptions) (*OpenDevcontainerAbstractAgent, error) { log.Info("OpenDevcontainerService: 开始获取 DevContainer 连接信息 name=%s, wait=%v", opts.Name, opts.Wait) // 0. 检查参数 if ctx == nil || opts == nil || len(opts.Name) == 0 { log.Error("OpenDevcontainerService: 参数无效 ctx=%v, opts=%v", ctx != nil, opts != nil) return nil, devcontainer_service_errors.ErrIllegalParams{ FieldNameList: []string{"ctx", "opts.Name"}, } } // 1. 检查 DevContainer 功能是否开启 if setting.Devcontainer.Enabled == false { log.Warn("OpenDevcontainerService: DevContainer 功能已全局关闭") 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 { case setting.KUBERNETES, "k8s": log.Info("OpenDevcontainerService: 使用 K8s Agent 获取 DevContainer: %s", opts.Name) devcontainerApp, err := AssignDevcontainerGetting2K8sOperator(&apiRequestContext, opts) if err != nil { log.Error("OpenDevcontainerService: K8s DevContainer 获取失败: %v", err) return nil, devcontainer_service_errors.ErrOperateDevcontainer{ Action: "Open DevContainer in k8s", Message: err.Error(), } } openDevcontainerAbstractAgentVO.NodePortAssigned = devcontainerApp.Status.NodePortAssigned log.Info("OpenDevcontainerService: K8s DevContainer 获取成功, name=%s, nodePort=%d, ready=%v", opts.Name, devcontainerApp.Status.NodePortAssigned, devcontainerApp.Status.Ready) 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: log.Error("OpenDevcontainerService: 未知的 DevContainer Agent 类型: %s", setting.Devcontainer.Agent) return nil, devcontainer_service_errors.ErrOperateDevcontainer{ Action: "Open DevContainer", Message: "No Valid DevContainer Agent Found", } } // 3. 封装返回结果 log.Info("OpenDevcontainerService: 获取 DevContainer 连接信息完成, nodePort=%d", openDevcontainerAbstractAgentVO.NodePortAssigned) return openDevcontainerAbstractAgentVO, nil } // GetRepoDevcontainerDetails 获取仓库对应 DevContainer 信息 func GetRepoDevcontainerDetails(ctx context.Context, opts *RepoDevcontainerOptions) (RepoDevContainer, error) { log.Info("GetRepoDevcontainerDetails: 开始查询仓库 DevContainer 信息") // 0. 构造异常返回时候的空数据 resultRepoDevcontainerDetail := RepoDevContainer{} // 1. 检查参数是否有效 if opts == nil || opts.Actor == nil || opts.Repository == nil { log.Error("GetRepoDevcontainerDetails: 参数无效 opts=%v, actor=%v, repo=%v", opts != nil, opts != nil && opts.Actor != nil, opts != nil && opts.Repository != nil) return resultRepoDevcontainerDetail, devcontainer_models_errors.ErrFailedToOperateDevcontainerDB{ Action: "construct query condition for devContainer user list", Message: "invalid search condition", } } log.Info("GetRepoDevcontainerDetails: 查询用户=%s (ID=%d) 的仓库=%s (ID=%d) 的 DevContainer", opts.Actor.Name, opts.Actor.ID, opts.Repository.Name, opts.Repository.ID) // 2. 查询数据库 _, err := db.GetEngine(ctx). Table("devcontainer"). Select(""+ "devcontainer.id AS devcontainer_id,"+ "devcontainer.name AS devcontainer_name,"+ "devcontainer.devcontainer_host AS devcontainer_host,"+ "devcontainer.devcontainer_port AS devcontainer_port,"+ "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,"+ "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", "devcontainer.repo_id = repository.id"). Where("devcontainer.user_id = ? AND devcontainer.repo_id = ?", opts.Actor.ID, opts.Repository.ID). Get(&resultRepoDevcontainerDetail) // 3. 返回 if err != nil { log.Error("GetRepoDevcontainerDetails: 数据库查询失败: %v", err) return resultRepoDevcontainerDetail, devcontainer_models_errors.ErrFailedToOperateDevcontainerDB{ 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 *CreateRepoDevcontainerOptions) error { username := opts.Actor.Name repoName := opts.Repository.Name log.Info("CreateRepoDevcontainer: 开始创建 DevContainer, user=%s, repo=%s, repoID=%d", username, repoName, opts.Repository.ID) // unixTimestamp is the number of seconds elapsed since January 1, 1970 UTC. unixTimestamp := time.Now().Unix() log.Info("CreateRepoDevcontainer: 获取 DevContainer JSON 模型") devContainerJson, err := GetDevcontainerJsonModel(ctx, opts.Repository) if err != nil { log.Error("CreateRepoDevcontainer: 获取 DevContainer JSON 失败: %v", err) return devcontainer_service_errors.ErrOperateDevcontainer{ Action: "Get DevContainer Error", Message: err.Error(), } } log.Info("CreateRepoDevcontainer: DevContainer JSON 获取成功, image=%s, dockerfilePath=%s", devContainerJson.Image, devContainerJson.DockerfilePath) var dockerfileContent string if devContainerJson.DockerfilePath != "" { log.Info("CreateRepoDevcontainer: 获取 Dockerfile 内容, path=%s", devContainerJson.DockerfilePath) dockerfileContent, err = GetDockerfileContent(ctx, opts.Repository) if err != nil { log.Error("CreateRepoDevcontainer: 获取 Dockerfile 内容失败: %v", err) return devcontainer_service_errors.ErrOperateDevcontainer{ Action: "Get DockerFileContent Error", Message: err.Error(), } } log.Debug("CreateRepoDevcontainer: Dockerfile 内容获取成功, 长度=%d", len(dockerfileContent)) } cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf) if err != nil { log.Error("CreateRepoDevcontainer: 加载配置文件失败: %v", err) } containerName := getSanitizedDevcontainerName(username, repoName) log.Info("CreateRepoDevcontainer: 生成 DevContainer 名称: %s", containerName) newDevcontainer := &CreateDevcontainerDTO{ Devcontainer: devcontainer_model.Devcontainer{ Name: containerName, DevcontainerHost: cfg.Section("server").Key("DOMAIN").Value(), DevcontainerUsername: "root", DevcontainerWorkDir: "/data/workspace", RepoId: opts.Repository.ID, UserId: opts.Actor.ID, CreatedUnix: unixTimestamp, UpdatedUnix: unixTimestamp, }, DockerfileContent: dockerfileContent, Image: devContainerJson.Image, GitRepositoryURL: strings.TrimSuffix(setting.AppURL, "/") + opts.Repository.Link(), } log.Info("CreateRepoDevcontainer: 初始化 DevContainer 对象, host=%s, workDir=%s, gitURL=%s", newDevcontainer.DevcontainerHost, newDevcontainer.DevcontainerWorkDir, newDevcontainer.GitRepositoryURL) // 在数据库事务中创建 Dev Container 分配资源,出错时自动回滚相对应数据库字段,保证数据一致 dbTransactionErr := db.WithTx(ctx, func(ctx context.Context) error { var err error log.Info("CreateRepoDevcontainer: 开始数据库事务") // 0. 查询数据库,收集用户 SSH 公钥,合并用户临时填入SSH公钥 log.Info("CreateRepoDevcontainer: 查询用户 SSH 公钥, userID=%d", opts.Actor.ID) var userSSHPublicKeyList []string err = db.GetEngine(ctx). Table("public_key"). Select("content"). Where("owner_id = ?", opts.Actor.ID). Find(&userSSHPublicKeyList) if err != nil { log.Error("CreateRepoDevcontainer: 查询用户 SSH 公钥失败: %v", err) return devcontainer_service_errors.ErrOperateDevcontainer{ Action: fmt.Sprintf("query SSH Public Key List for User %s", opts.Actor.Name), Message: err.Error(), } } log.Info("CreateRepoDevcontainer: 找到用户 SSH 公钥 %d 个", len(userSSHPublicKeyList)) newDevcontainer.SSHPublicKeyList = append(userSSHPublicKeyList, opts.SSHPublicKeyList...) log.Info("CreateRepoDevcontainer: 合并 SSH 公钥后共 %d 个", len(newDevcontainer.SSHPublicKeyList)) devstarPublicKey := getDevStarPublicKey() if devstarPublicKey == "" { log.Error("CreateRepoDevcontainer: 获取 DevStar SSH 公钥失败") return devcontainer_service_errors.ErrOperateDevcontainer{ Action: fmt.Sprintf("devstar SSH Public Key Error "), Message: err.Error(), } } log.Info("CreateRepoDevcontainer: 获取 DevStar SSH 公钥成功") newDevcontainer.SSHPublicKeyList = append(newDevcontainer.SSHPublicKeyList) // 1. 调用 k8s Agent,创建 DevContainer 资源,同时更新k8s调度器分配的 NodePort if setting.Devcontainer.Agent == setting.KUBERNETES || setting.Devcontainer.Agent == "k8s" { log.Info("CreateRepoDevcontainer: 调用 K8s controller 创建 DevContainer 资源") } err = claimDevcontainerResource(&ctx, newDevcontainer, devContainerJson) if err != nil { if setting.Devcontainer.Agent == setting.KUBERNETES || setting.Devcontainer.Agent == "k8s" { log.Error("CreateRepoDevcontainer: K8s controller 创建失败: %v", err) } return devcontainer_models_errors.ErrFailedToOperateDevcontainerDB{ Action: fmt.Sprintf("claim resource for Dev Container %v", newDevcontainer), Message: err.Error(), } } if setting.Devcontainer.Agent == setting.KUBERNETES || setting.Devcontainer.Agent == "k8s" { log.Info("CreateRepoDevcontainer: K8s controller 创建成功, nodePort=%d", newDevcontainer.DevcontainerPort) } // 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", } } log.Info("CreateRepoDevcontainer: 数据库插入成功, 影响行数=%d", rowsAffect) return nil }) if dbTransactionErr != nil { log.Error("CreateRepoDevcontainer: 创建失败: %v", dbTransactionErr) return dbTransactionErr } log.Info("CreateRepoDevcontainer: DevContainer 创建成功, name=%s", newDevcontainer.Name) return nil } 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() } func GetWebTerminalURL(ctx context.Context, devcontainerName string) (string, error) { switch setting.Devcontainer.Agent { case setting.KUBERNETES, "k8s": log.Info("GetWebTerminalURL: 开始查找 K8s DevContainer ttyd 端口, name=%s", devcontainerName) // 创建 K8s 客户端,直接查询 CRD 以获取 ttyd 端口 k8sClient, err := devcontainer_k8s_agent_module.GetKubernetesClient(&ctx) if err != nil { log.Error("GetWebTerminalURL: 获取 K8s 客户端失败: %v", err) return "", err } log.Info("GetWebTerminalURL: K8s 客户端创建成功") // 直接从K8s获取CRD信息,不依赖数据库 opts := &devcontainer_k8s_agent_module.GetDevcontainerOptions{ GetOptions: metav1.GetOptions{}, Name: devcontainerName, Namespace: setting.Devcontainer.Namespace, Wait: false, } log.Info("GetWebTerminalURL: 从 K8s 获取 DevcontainerApp %s, namespace=%s", devcontainerName, setting.Devcontainer.Namespace) devcontainerApp, err := devcontainer_k8s_agent_module.GetDevcontainer(&ctx, k8sClient, opts) if err != nil { log.Error("GetWebTerminalURL: 获取 DevcontainerApp 失败: %v", err) return "", err } log.Info("GetWebTerminalURL: 成功获取 DevcontainerApp, extraPorts=%d", len(devcontainerApp.Status.ExtraPortsAssigned)) // 在额外端口中查找 ttyd 端口,使用多个条件匹配 var ttydNodePort uint16 = 0 for _, portInfo := range devcontainerApp.Status.ExtraPortsAssigned { // 检查各种可能的情况:名称为ttyd、名称包含ttyd、名称为port-7681、端口为7681 log.Debug("GetWebTerminalURL: 检查端口 name=%s, containerPort=%d, nodePort=%d", portInfo.Name, portInfo.ContainerPort, portInfo.NodePort) if portInfo.Name == "ttyd" || strings.Contains(portInfo.Name, "ttyd") || portInfo.Name == "port-7681" || portInfo.ContainerPort == 7681 { ttydNodePort = portInfo.NodePort log.Info("GetWebTerminalURL: 找到 ttyd 端口: %d, 名称: %s", ttydNodePort, portInfo.Name) break } } // 如果找到 ttyd 端口,构建 URL if ttydNodePort > 0 { cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf) if err != nil { log.Error("GetWebTerminalURL: 加载配置文件失败: %v", err) return "", err } // 检查是否启用了基于路径的访问方式 domain := cfg.Section("server").Key("DOMAIN").Value() 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 } } // 如果没有找到ttyd端口,记录详细的调试信息 log.Warn("GetWebTerminalURL: 未找到 ttyd 端口 (7681), 可用的额外端口: %v", devcontainerApp.Status.ExtraPortsAssigned) return "", fmt.Errorf("ttyd port (7681) not found for container: %s", devcontainerName) case setting.DOCKER: cli, err := docker.CreateDockerClient(&ctx) if err != nil { return "", err } defer cli.Close() containerID, err := docker.GetContainerID(cli, devcontainerName) if err != nil { return "", err } port, err := docker.GetMappedPort(cli, containerID, "7681") if err != nil { return "", err } cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf) if err != nil { log.Error("Failed to load custom conf '%s': %v", setting.CustomConf, err) return "", err } return "http://" + cfg.Section("server").Key("DOMAIN").Value() + ":" + port + "/", nil default: return "", fmt.Errorf("unknown agent") } } func Get_IDE_TerminalURL(ctx *gitea_context.Context, devcontainer *RepoDevContainer) (string, error) { var access_token string // 检查 session 中是否已存在 token if ctx.Session.Get("access_token") != nil { access_token = ctx.Session.Get("access_token").(string) } else { // 生成 token 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 } // 根据不同的代理类型获取 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 } // 使用 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) } // 构建并返回 URL return "://mengning.devstar/" + "openProject?host=" + devcontainer.DevContainerHost + "&port=" + port + "&username=" + devcontainer.DevContainerUsername + "&path=" + devcontainer.DevContainerWorkDir + "&access_token=" + access_token + "&devstar_username=" + devcontainer.RepoOwnerName, nil } func AddPublicKeyToAllRunningDevContainer(ctx context.Context, user *user_model.User, publicKey string) error { switch setting.Devcontainer.Agent { case setting.KUBERNETES, "k8s": return fmt.Errorf("unsupported agent") 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 default: return fmt.Errorf("unknown agent") } } // DeleteRepoDevcontainer 按照 仓库 和/或 用户信息删除 DevContainer(s) func DeleteRepoDevcontainer(ctx context.Context, opts *RepoDevcontainerOptions) error { log.Info("DeleteRepoDevcontainer: 开始删除 DevContainer") if ctx == nil || opts == nil || (opts.Actor == nil && opts.Repository == nil) { 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) return devcontainer_models_errors.ErrFailedToOperateDevcontainerDB{ 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}) log.Info("DeleteRepoDevcontainer: 添加用户条件, userID=%d", opts.Actor.ID) } if opts.Repository != nil { sqlDevcontainerCondition = sqlDevcontainerCondition.And(builder.Eq{"repo_id": opts.Repository.ID}) log.Info("DeleteRepoDevcontainer: 添加仓库条件, repoID=%d", opts.Repository.ID) } log.Info("DeleteRepoDevcontainer: 查询条件构建完成: %v", sqlDevcontainerCondition) var devcontainersList []devcontainer_model.Devcontainer // 2. 开启事务:先获取 devcontainer列表,后删除 dbTransactionErr := db.WithTx(ctx, func(ctx context.Context) error { var err error log.Info("DeleteRepoDevcontainer: 开始数据库事务") // 2.1 条件查询: user_id 和/或 repo_id log.Info("DeleteRepoDevcontainer: 查询符合条件的 DevContainer") err = db.GetEngine(ctx). Table("devcontainer"). Where(sqlDevcontainerCondition). Find(&devcontainersList) if err != nil { log.Error("DeleteRepoDevcontainer: 查询失败: %v", err) return devcontainer_models_errors.ErrFailedToOperateDevcontainerDB{ Action: fmt.Sprintf("find devcontainer(s) with condition '%v'", sqlDevcontainerCondition), Message: err.Error(), } } log.Info("DeleteRepoDevcontainer: 找到 %d 个符合条件的 DevContainer", len(devcontainersList)) // 2.2 空列表,直接结束事务(由于前一个操作只是查询,所以回滚事务不会导致数据不一致问题) if len(devcontainersList) == 0 { log.Warn("DeleteRepoDevcontainer: 未找到符合条件的 DevContainer") return devcontainer_models_errors.ErrFailedToOperateDevcontainerDB{ Action: fmt.Sprintf("find devcontainer(s) with condition '%v'", sqlDevcontainerCondition), Message: "No DevContainer found", } } // 2.3 条件删除: user_id 和/或 repo_id log.Info("DeleteRepoDevcontainer: 从数据库删除 DevContainer 记录") rowsAffected, err := db.GetEngine(ctx). Table("devcontainer"). Where(sqlDevcontainerCondition). Delete() if err != nil { log.Error("DeleteRepoDevcontainer: 删除 DevContainer 记录失败: %v", err) return devcontainer_models_errors.ErrFailedToOperateDevcontainerDB{ Action: fmt.Sprintf("MARK devcontainer(s) as DELETED with condition '%v'", sqlDevcontainerCondition), Message: err.Error(), } } log.Info("DeleteRepoDevcontainer: DevContainer 记录删除成功, 影响行数=%d", rowsAffected) // 删除对应的输出记录 log.Info("DeleteRepoDevcontainer: 删除 DevContainer 输出记录") outputRowsAffected, err := db.GetEngine(ctx). Table("devcontainer_output"). Where(sqlDevcontainerCondition). Delete() if err != nil { log.Error("DeleteRepoDevcontainer: 删除输出记录失败: %v", err) return err } log.Info("DeleteRepoDevcontainer: DevContainer 输出记录删除成功, 影响行数=%d", outputRowsAffected) return nil }) if dbTransactionErr != nil { log.Error("DeleteRepoDevcontainer: 数据库操作失败: %v", dbTransactionErr) return dbTransactionErr } // 3. 后台启动一个goroutine慢慢回收 Dev Container 资源 (如果回收失败,将会产生孤儿 Dev Container,只能管理员手动识别、删除) log.Info("DeleteRepoDevcontainer: 启动异步资源回收, DevContainer数量=%d", len(devcontainersList)) go func() { // 注意:由于执行删除 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() err := purgeDevcontainersResource(&isolatedContextToPurgeK8sResource, &devcontainersList) if err != nil { log.Error("DeleteRepoDevcontainer: 异步资源回收失败: %v", err) } }() log.Info("DeleteRepoDevcontainer: DevContainer 删除操作完成") 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) > 31 { sanitizedRepoName = strings.ToLower(sanitizedRepoName[:31]) } newUUID, _ := uuid.NewUUID() uuidStr := newUUID.String() uuidStr = regexpNonAlphaNum.ReplaceAllString(uuidStr, "")[:15] return fmt.Sprintf("%s-%s-%s", sanitizedUsername, sanitizedRepoName, uuidStr) } // purgeDevcontainersResource 辅助函数,用于goroutine后台执行,回收DevContainer资源 func purgeDevcontainersResource(ctx *context.Context, devcontainersList *[]devcontainer_model.Devcontainer) error { // 1. 检查 DevContainer 功能是否启用,若禁用,则直接结束,不会真正执行删除操作 if !setting.Devcontainer.Enabled { log.Warn("purgeDevcontainersResource: DevContainer 功能已全局禁用, 跳过资源回收") // 如果用户设置禁用 DevContainer,无法删除资源,会直接忽略,而数据库相关记录会继续清空、不会发生回滚 log.Warn("Orphan DevContainers in namespace `%s` left undeleted: %v", setting.Devcontainer.Namespace, devcontainersList) return nil } // 2. 根据配置文件中指定的 DevContainer Agent 派遣创建任务 switch setting.Devcontainer.Agent { 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 case setting.DOCKER: return DeleteDevcontainer(ctx, devcontainersList) default: // 未知 Agent,直接报错 return devcontainer_models_errors.ErrFailedToOperateDevcontainerDB{ Action: "dispatch DevContainer deletion", Message: fmt.Sprintf("unknown DevContainer agent `%s`, please check your config files", setting.Devcontainer.Agent), } } } // claimDevcontainerResource 分发创建 DevContainer 任务到配置文件指定的执行器 func claimDevcontainerResource(ctx *context.Context, newDevContainer *CreateDevcontainerDTO, devContainerJSON *DevStarJSON) error { log.Info("claimDevcontainerResource: 开始分发 DevContainer 创建任务, name=%s", newDevContainer.Name) // 1. 检查 DevContainer 功能是否启用,若禁用,则直接结束 if !setting.Devcontainer.Enabled { log.Error("claimDevcontainerResource: DevContainer 功能已全局禁用") return devcontainer_models_errors.ErrFailedToOperateDevcontainerDB{ Action: "Check for DevContainer functionality switch", Message: "DevContainer is disabled globally, please check your configuration files", } } // 解析仓库 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() // 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) // 2. 根据配置文件中指定的 DevContainer Agent 派遣创建任务 log.Info("claimDevcontainerResource: 使用 %s Agent 创建 DevContainer", setting.Devcontainer.Agent) switch setting.Devcontainer.Agent { case setting.KUBERNETES, "k8s": // k8s Operator 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 case setting.DOCKER: return CreateDevcontainer(ctx, newDevContainer, devContainerJSON, initializeScript, restartScript) default: // 未知 Agent,直接报错 return devcontainer_models_errors.ErrFailedToOperateDevcontainerDB{ Action: "dispatch DevContainer creation", Message: fmt.Sprintf("unknown DevContainer agent `%s`, please check your config files", setting.Devcontainer.Agent), } } } func RestartDevcontainer(gitea_ctx gitea_context.Context, opts *RepoDevContainer) error { log.Info("RestartDevcontainer: 开始重启 DevContainer, name=%s", opts.DevContainerName) switch setting.Devcontainer.Agent { case setting.KUBERNETES, "k8s": log.Info("RestartDevcontainer: 使用 K8s Agent 重启容器 %s", opts.DevContainerName) ctx := gitea_ctx.Req.Context() err := AssignDevcontainerRestart2K8sOperator(&ctx, opts) if err != nil { log.Error("RestartDevcontainer: K8s 重启容器失败: %v", err) } else { log.Info("RestartDevcontainer: K8s 重启容器成功") } return err case setting.DOCKER: return DockerRestartContainer(&gitea_ctx, opts) default: return fmt.Errorf("不支持的Agent") //默认处理 } } func StopDevcontainer(gitea_ctx context.Context, opts *RepoDevContainer) error { log.Info("StopDevcontainer: 开始停止 DevContainer, name=%s", opts.DevContainerName) switch setting.Devcontainer.Agent { 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 case setting.DOCKER: return DockerStopContainer(&gitea_ctx, opts) default: return fmt.Errorf("不支持的Agent") //默认处理 } }