From 404132dac52ec226f11c7e69a93a7da604243a89 Mon Sep 17 00:00:00 2001 From: panshuxiao Date: Tue, 20 May 2025 12:06:35 +0800 Subject: [PATCH 1/5] =?UTF-8?q?=E4=BF=AE=E5=A4=8Ddevcontainer=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E5=8A=A0=E8=BD=BD=E5=92=8Ccontroller-manager=E7=9A=84?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit •增添了日志 --- .../devcontainer/utils/template_utils.go | 43 +--- modules/setting/devcontainer.go | 30 ++- services/devcontainer/devcontainer.go | 232 ++++++++++++++---- services/devcontainer/k8s_agent.go | 52 +++- 4 files changed, 261 insertions(+), 96 deletions(-) diff --git a/modules/k8s/controller/devcontainer/utils/template_utils.go b/modules/k8s/controller/devcontainer/utils/template_utils.go index 99a965d079..689e6f74c9 100644 --- a/modules/k8s/controller/devcontainer/utils/template_utils.go +++ b/modules/k8s/controller/devcontainer/utils/template_utils.go @@ -2,10 +2,6 @@ package utils import ( "bytes" - "fmt" - "os" - "path/filepath" - "runtime" "text/template" devcontainer_apps_v1 "code.gitea.io/gitea/modules/k8s/api/v1" @@ -14,53 +10,24 @@ import ( yaml_util "k8s.io/apimachinery/pkg/util/yaml" ) -// const ( -// TemplatePath = "modules/k8s/controller/devcontainer/templates/" -// ) +const ( + TemplatePath = "modules/k8s/controller/devcontainer/templates/" +) // parseTemplate 解析 Go Template 模板文件 func parseTemplate(templateName string, app *devcontainer_apps_v1.DevcontainerApp) []byte { - // 获取当前代码文件的绝对路径 - _, filename, _, ok := runtime.Caller(0) - if !ok { - panic("无法获取当前文件路径") - } - - // 通过当前代码文件的位置计算模板文件的位置 - // utils 目录 - utilsDir := filepath.Dir(filename) - // controller/devcontainer 目录 - controllerDir := filepath.Dir(utilsDir) - // templates 目录 - templatesDir := filepath.Join(controllerDir, "templates") - // 完整模板文件路径 - templatePath := filepath.Join(templatesDir, templateName+".yaml") - - // 打印调试信息 - fmt.Printf("当前代码文件: %s\n", filename) - fmt.Printf("模板目录: %s\n", templatesDir) - fmt.Printf("使用模板文件: %s\n", templatePath) - - // 检查模板文件是否存在 - if _, err := os.Stat(templatePath); os.IsNotExist(err) { - panic(fmt.Errorf("模板文件不存在: %s", templatePath)) - } - - // 解析模板 tmpl, err := template. - New(filepath.Base(templatePath)). + New(templateName + ".yaml"). Funcs(template.FuncMap{"default": DefaultFunc}). - ParseFiles(templatePath) + ParseFiles(TemplatePath + templateName + ".yaml") if err != nil { panic(err) } - b := new(bytes.Buffer) err = tmpl.Execute(b, app) if err != nil { panic(err) } - return b.Bytes() } diff --git a/modules/setting/devcontainer.go b/modules/setting/devcontainer.go index ce85d2885c..b4928cc33d 100644 --- a/modules/setting/devcontainer.go +++ b/modules/setting/devcontainer.go @@ -163,10 +163,35 @@ func validateDevcontainerCloudSettings() { } +// 修改 loadDevcontainerFrom 函数以更好地处理不同配置节点 func loadDevcontainerFrom(rootCfg ConfigProvider) { - mustMapSetting(rootCfg, "devcontainer", &Devcontainer) + // 检查是否存在新的配置节 + hasNewConfig := true + if _, err := rootCfg.GetSection("devcontainer"); err != nil { + hasNewConfig = false + } + + // 检查是否存在旧的配置节 + hasOldConfig := true + if _, err := rootCfg.GetSection("devstar.devcontainer"); err != nil { + hasOldConfig = false + } + + // 根据存在的配置节处理 + if hasNewConfig { + // 新配置节存在,直接使用 + mustMapSetting(rootCfg, "devcontainer", &Devcontainer) + log.Info("从 [devcontainer] 节加载配置") + } else if hasOldConfig { + // 只有旧配置节存在,直接从旧配置节加载 + mustMapSetting(rootCfg, "devstar.devcontainer", &Devcontainer) + log.Info("从 [devstar.devcontainer] 节加载配置") + } + + // 进行配置验证 validateDevcontainerSettings() + // 加载其他配置 mustMapSetting(rootCfg, "ssh_key_pair", &SSHKeypair) validateSSHKeyPairSettings() @@ -174,4 +199,7 @@ func loadDevcontainerFrom(rootCfg ConfigProvider) { mustMapSetting(rootCfg, "devcontainer.cloud", &Cloud) validateDevcontainerCloudSettings() } + + // 打印最终使用的命名空间 + log.Info("DevContainer 将在命名空间 '%s' 中创建", Devcontainer.Namespace) } diff --git a/services/devcontainer/devcontainer.go b/services/devcontainer/devcontainer.go index 03e8531665..2fd6e5579b 100644 --- a/services/devcontainer/devcontainer.go +++ b/services/devcontainer/devcontainer.go @@ -37,9 +37,12 @@ type OpenDevcontainerAbstractAgent struct { // 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"}, } @@ -47,6 +50,7 @@ func OpenDevcontainerService(ctx *gitea_context.Context, opts *OpenDevcontainerA // 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", @@ -57,15 +61,19 @@ func OpenDevcontainerService(ctx *gitea_context.Context, opts *OpenDevcontainerA apiRequestContext := ctx.Req.Context() openDevcontainerAbstractAgentVO := &OpenDevcontainerAbstractAgent{} switch setting.Devcontainer.Agent { - case setting.KUBERNETES: + 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) @@ -77,6 +85,7 @@ func OpenDevcontainerService(ctx *gitea_context.Context, opts *OpenDevcontainerA } 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", @@ -84,44 +93,32 @@ func OpenDevcontainerService(ctx *gitea_context.Context, opts *OpenDevcontainerA } // 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. 查询数据库 - /* - 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, - 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 devcontainer - INNER JOIN repository on devcontainer.repo_id = repository.id - WHERE - devcontainer.user_id = #{opts.Actor.ID} - AND - devcontainer.repo_id = #{opts.Repository.ID}; - */ _, err := db.GetEngine(ctx). Table("devcontainer"). Select(""+ @@ -143,6 +140,7 @@ func GetRepoDevcontainerDetails(ctx context.Context, opts *RepoDevcontainerOptio // 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(), @@ -162,32 +160,49 @@ func CreateRepoDevcontainer(ctx context.Context, opts *CreateRepoDevcontainerOpt 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("Failed to load custom conf '%s': %v", setting.CustomConf, err) + log.Error("CreateRepoDevcontainer: 加载配置文件失败: %v", err) } + + containerName := getSanitizedDevcontainerName(username, repoName) + log.Info("CreateRepoDevcontainer: 生成 DevContainer 名称: %s", containerName) + newDevcontainer := &CreateDevcontainerDTO{ Devcontainer: devcontainer_model.Devcontainer{ - Name: getSanitizedDevcontainerName(username, repoName), + Name: containerName, DevcontainerHost: cfg.Section("server").Key("DOMAIN").Value(), DevcontainerUsername: "root", DevcontainerWorkDir: "/data/workspace", @@ -200,15 +215,16 @@ func CreateRepoDevcontainer(ctx context.Context, opts *CreateRepoDevcontainerOpt 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公钥 - // (若用户合计 SSH公钥个数为0,拒绝创建DevContainer) - /** - SELECT content FROM public_key where owner_id = #{opts.Actor.ID} - */ + log.Info("CreateRepoDevcontainer: 查询用户 SSH 公钥, userID=%d", opts.Actor.ID) var userSSHPublicKeyList []string err = db.GetEngine(ctx). Table("public_key"). @@ -216,55 +232,78 @@ func CreateRepoDevcontainer(ctx context.Context, opts *CreateRepoDevcontainerOpt 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) - // if len(userSSHPublicKeyList) <= 0 { - // // API没提供临时SSH公钥,用户后台也没有永久SSH公钥,直接结束并回滚事务 - // return devcontainer_service_errors.ErrOperateDevcontainer{ - // Action: "Check SSH Public Key List", - // Message: "禁止创建无法连通的DevContainer:用户未提供 SSH 公钥,请先使用API临时创建SSH密钥对、或在Web端手动添加SSH公钥", - // } - // } - // 1. 调用 k8s Operator Agent,创建 DevContainer 资源,同时更新k8s调度器分配的 NodePort + // 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 }) - return dbTransactionErr + if dbTransactionErr != nil { + log.Error("CreateRepoDevcontainer: 创建失败: %v", dbTransactionErr) + return dbTransactionErr + } + + log.Info("CreateRepoDevcontainer: DevContainer 创建成功, name=%s", newDevcontainer.Name) + return nil } func getDevStarPublicKey() string { // 获取当前用户的主目录 @@ -317,12 +356,16 @@ func fileExists(filename string) bool { } func GetWebTerminalURL(ctx context.Context, devcontainerName string) (string, error) { switch setting.Devcontainer.Agent { - case setting.KUBERNETES: + 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{ @@ -331,22 +374,30 @@ func GetWebTerminalURL(ctx context.Context, devcontainerName string) (string, er 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("Found ttyd port: %d for port named: %s", ttydNodePort, portInfo.Name) + log.Info("GetWebTerminalURL: 找到 ttyd 端口: %d, 名称: %s", ttydNodePort, portInfo.Name) break } } @@ -355,17 +406,18 @@ func GetWebTerminalURL(ctx context.Context, devcontainerName string) (string, er if ttydNodePort > 0 { cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf) if err != nil { - log.Error("Failed to load custom conf '%s': %v", setting.CustomConf, err) + log.Error("GetWebTerminalURL: 加载配置文件失败: %v", err) return "", err } domain := cfg.Section("server").Key("DOMAIN").Value() + log.Info("GetWebTerminalURL: 生成 ttyd URL: http://%s:%d/", domain, ttydNodePort) return fmt.Sprintf("http://%s:%d/", domain, ttydNodePort), nil } // 如果没有找到ttyd端口,记录详细的调试信息 - log.Info("Available extra ports for %s: %v", devcontainerName, devcontainerApp.Status.ExtraPortsAssigned) + 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 { @@ -383,6 +435,7 @@ func GetWebTerminalURL(ctx context.Context, devcontainerName string) (string, er 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: @@ -447,7 +500,7 @@ func Get_IDE_TerminalURL(ctx *gitea_context.Context, devcontainer *RepoDevContai func AddPublicKeyToAllRunningDevContainer(ctx context.Context, user *user_model.User, publicKey string) error { switch setting.Devcontainer.Agent { - case setting.KUBERNETES: + case setting.KUBERNETES, "k8s": return fmt.Errorf("unsupported agent") case setting.DOCKER: cli, err := docker.CreateDockerClient(&ctx) @@ -497,7 +550,13 @@ func AddPublicKeyToAllRunningDevContainer(ctx context.Context, user *user_model. // 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", @@ -508,29 +567,39 @@ func DeleteRepoDevcontainer(ctx context.Context, opts *RepoDevcontainerOptions) 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", @@ -538,37 +607,55 @@ func DeleteRepoDevcontainer(ctx context.Context, opts *RepoDevcontainerOptions) } // 2.3 条件删除: user_id 和/或 repo_id - _, err = db.GetEngine(ctx). + 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(), } } - _, err = db.GetEngine(ctx). + 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() - _ = purgeDevcontainersResource(&isolatedContextToPurgeK8sResource, &devcontainersList) + err := purgeDevcontainersResource(&isolatedContextToPurgeK8sResource, &devcontainersList) + if err != nil { + log.Error("DeleteRepoDevcontainer: 异步资源回收失败: %v", err) + } }() + log.Info("DeleteRepoDevcontainer: DevContainer 删除操作完成") return dbTransactionErr } @@ -594,14 +681,23 @@ func getSanitizedDevcontainerName(username, repoName string) string { 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: - return AssignDevcontainerDeletion2K8sOperator(ctx, devcontainersList) + 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: @@ -615,8 +711,11 @@ func purgeDevcontainersResource(ctx *context.Context, devcontainersList *[]devco // 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", @@ -691,10 +790,20 @@ func claimDevcontainerResource(ctx *context.Context, newDevContainer *CreateDevc 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 - return AssignDevcontainerCreation2K8sOperator(ctx, newDevContainer) + 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: @@ -706,12 +815,19 @@ func claimDevcontainerResource(ctx *context.Context, newDevContainer *CreateDevc } } 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处理 + case setting.KUBERNETES, "k8s": + log.Info("RestartDevcontainer: 使用 K8s Agent 重启容器 %s", opts.DevContainerName) ctx := gitea_ctx.Req.Context() - return AssignDevcontainerRestart2K8sOperator(&ctx, opts) + 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: @@ -721,10 +837,18 @@ func RestartDevcontainer(gitea_ctx gitea_context.Context, opts *RepoDevContainer } 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处理 - return AssignDevcontainerStop2K8sOperator(&gitea_ctx, opts) + 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: diff --git a/services/devcontainer/k8s_agent.go b/services/devcontainer/k8s_agent.go index 746654b8cb..2a54442e66 100644 --- a/services/devcontainer/k8s_agent.go +++ b/services/devcontainer/k8s_agent.go @@ -37,6 +37,8 @@ func (err ErrIllegalK8sAgentParams) Error() string { // AssignDevcontainerGetting2K8sOperator 获取 k8s CRD 资源 DevcontainerApp 最新状态(需要根据用户传入的 wait 参数决定是否要阻塞等待 DevContainer 就绪) func AssignDevcontainerGetting2K8sOperator(ctx *context.Context, opts *OpenDevcontainerAppDispatcherOptions) (*k8s_api_v1.DevcontainerApp, error) { + log.Info("AssignDevcontainerGetting2K8sOperator: Starting lookup for container: %s, wait=%v", + opts.Name, opts.Wait) // 0. 检查参数 if ctx == nil || opts == nil || len(opts.Name) == 0 { @@ -54,6 +56,7 @@ func AssignDevcontainerGetting2K8sOperator(ctx *context.Context, opts *OpenDevco Message: err.Error(), } } + log.Info("AssignDevcontainerGetting2K8sOperator: K8s client created successfully") // 2. 调用 modules 层 k8s Agent 获取 k8s CRD 资源 DevcontainerApp optsGetDevcontainer := &devcontainer_dto.GetDevcontainerOptions{ @@ -62,13 +65,28 @@ func AssignDevcontainerGetting2K8sOperator(ctx *context.Context, opts *OpenDevco Namespace: setting.Devcontainer.Namespace, Wait: opts.Wait, } + log.Info("AssignDevcontainerGetting2K8sOperator: Retrieving DevcontainerApp %s in namespace %s (wait=%v)", + opts.Name, setting.Devcontainer.Namespace, opts.Wait) devcontainerApp, err := devcontainer_k8s_agent_module.GetDevcontainer(ctx, client, optsGetDevcontainer) if err != nil { + log.Error("AssignDevcontainerGetting2K8sOperator: Failed to get DevcontainerApp: %v", err) return nil, errors.ErrOperateDevcontainer{ Action: fmt.Sprintf("Open Devcontainer '%s' (wait=%v)", opts.Name, opts.Wait), Message: err.Error(), } } + log.Info("AssignDevcontainerGetting2K8sOperator: DevcontainerApp retrieved successfully - Name: %s, NodePort: %d, Ready: %v", + devcontainerApp.Name, devcontainerApp.Status.NodePortAssigned, devcontainerApp.Status.Ready) + + // 添加额外端口的日志 + if len(devcontainerApp.Status.ExtraPortsAssigned) > 0 { + for i, port := range devcontainerApp.Status.ExtraPortsAssigned { + log.Info("AssignDevcontainerGetting2K8sOperator: Extra port %d - Name: %s, NodePort: %d, ContainerPort: %d", + i, port.Name, port.NodePort, port.ContainerPort) + } + } else { + log.Info("AssignDevcontainerGetting2K8sOperator: No extra ports found for DevcontainerApp %s", devcontainerApp.Name) + } // 3. 成功获取最新的 DevcontainerApp,返回 return devcontainerApp, nil @@ -79,7 +97,7 @@ func AssignDevcontainerGetting2K8sOperator(ctx *context.Context, opts *OpenDevco // - services/ 进行了封装,简化用户界面使用 func AssignDevcontainerDeletion2K8sOperator(ctx *context.Context, devcontainersList *[]devcontainer_model.Devcontainer) error { - + log.Info("AssignDevcontainerDeletion2K8sOperator: Starting Deletion for containers") // 1. 获取 Dynamic Client client, err := devcontainer_k8s_agent_module.GetKubernetesClient(ctx) if err != nil { @@ -119,6 +137,9 @@ func AssignDevcontainerDeletion2K8sOperator(ctx *context.Context, devcontainersL // // 注意:本方法仍然在数据库事务中,因此不适合执行长时间操作,故需要后期异步判断 DevContainer 是否就绪 func AssignDevcontainerCreation2K8sOperator(ctx *context.Context, newDevContainer *CreateDevcontainerDTO) error { + log.Info("AssignDevcontainerCreation2K8sOperator: Starting creation for container: %s", newDevContainer.Name) + log.Info("AssignDevcontainerCreation2K8sOperator: Container details - Image: %s, RepoURL: %s, SSHKeys: %d", + newDevContainer.Image, newDevContainer.GitRepositoryURL, len(newDevContainer.SSHPublicKeyList)) // 1. 获取 Dynamic Client client, err := devcontainer_k8s_agent_module.GetKubernetesClient(ctx) @@ -183,6 +204,7 @@ func AssignDevcontainerCreation2K8sOperator(ctx *context.Context, newDevContaine } // 添加 ttyd 端口配置 - WebTerminal 功能 + log.Info("AssignDevcontainerCreation2K8sOperator: Adding ttyd port configuration (7681)") extraPorts := []k8s_api_v1.ExtraPortSpec{ { Name: "ttyd", @@ -202,6 +224,7 @@ func AssignDevcontainerCreation2K8sOperator(ctx *context.Context, newDevContaine "nohup ttyd -p 7681 -W bash > /dev/null 2>&1 & " + "while true; do sleep 60; done", } + log.Info("AssignDevcontainerCreation2K8sOperator: Command includes ttyd installation and startup") // 2. 调用 modules 层 k8s Agent,执行创建资源 opts := &devcontainer_dto.CreateDevcontainerOptions{ @@ -238,10 +261,15 @@ func AssignDevcontainerCreation2K8sOperator(ctx *context.Context, newDevContaine } // 2. 创建成功,取回集群中的 DevContainer + log.Info("AssignDevcontainerCreation2K8sOperator: Creating DevcontainerApp %s in namespace %s", + opts.Name, opts.Namespace) devcontainerInCluster, err := devcontainer_k8s_agent_module.CreateDevcontainer(ctx, client, opts) if err != nil { + log.Error("AssignDevcontainerCreation2K8sOperator: Failed to create DevcontainerApp: %v", err) return err } + log.Info("AssignDevcontainerCreation2K8sOperator: DevcontainerApp created successfully - Name: %s", + devcontainerInCluster.Name) // // 3. 将分配的 NodePort Service 写回 newDevcontainer,供写入数据库进行下一步操作 // newDevContainer.DevcontainerPort = devcontainerInCluster.Status.NodePortAssigned @@ -250,7 +278,8 @@ func AssignDevcontainerCreation2K8sOperator(ctx *context.Context, newDevContaine nodePort := devcontainerInCluster.Status.NodePortAssigned if nodePort == 0 { - log.Info("NodePort not yet assigned by K8s controller, setting temporary port") + log.Info("AssignDevcontainerCreation2K8sOperator: NodePort not yet assigned, starting async updater for %s", + devcontainerInCluster.Name) // 将端口设为0,数据库中记录特殊标记 newDevContainer.DevcontainerPort = 0 @@ -265,6 +294,8 @@ func AssignDevcontainerCreation2K8sOperator(ctx *context.Context, newDevContaine newDevContainer.UserId, newDevContainer.RepoId) } else { + log.Info("AssignDevcontainerCreation2K8sOperator: NodePort %d assigned immediately to %s", + nodePort, devcontainerInCluster.Name) // 端口已分配,直接使用 newDevContainer.DevcontainerPort = nodePort log.Info("DevContainer created in cluster - Name: %s, NodePort: %d", @@ -280,6 +311,8 @@ func AssignDevcontainerCreation2K8sOperator(ctx *context.Context, newDevContaine // AssignDevcontainerRestart2K8sOperator 将 DevContainer 重启任务派遣至 K8s 控制器 func AssignDevcontainerRestart2K8sOperator(ctx *context.Context, opts *RepoDevContainer) error { + log.Info("AssignDevcontainerRestart2K8sOperator: Starting restart for container: %s", opts.DevContainerName) + // 1. 获取 Dynamic Client client, err := devcontainer_k8s_agent_module.GetKubernetesClient(ctx) if err != nil { @@ -297,6 +330,9 @@ func AssignDevcontainerRestart2K8sOperator(ctx *context.Context, opts *RepoDevCo } } }`, time.Now().Format(time.RFC3339)) + log.Info("AssignDevcontainerRestart2K8sOperator: Applying patch to restart container %s", + opts.DevContainerName) + log.Debug("AssignDevcontainerRestart2K8sOperator: Patch data: %s", patchData) // 应用补丁到 DevcontainerApp CRD _, err = client.Resource(k8sGroupVersionResource). @@ -313,6 +349,8 @@ func AssignDevcontainerRestart2K8sOperator(ctx *context.Context, opts *RepoDevCo // 记录重启操作日志 log.Info("DevContainer restarted: %s", opts.DevContainerName) + log.Info("AssignDevcontainerRestart2K8sOperator: Restart patch applied successfully for %s", + opts.DevContainerName) // 将重启操作记录到数据库 dbEngine := db.GetEngine(*ctx) @@ -387,6 +425,9 @@ func AssignDevcontainerStop2K8sOperator(ctx *context.Context, opts *RepoDevConta // 异步更新 NodePort 的辅助函数 func updateNodePortAsync(containerName string, namespace string, userId, repoId int64) { + log.Info("updateNodePortAsync: Starting for container: %s in namespace: %s", containerName, namespace) + log.Info("updateNodePortAsync: Waiting 20 seconds for K8s controller to assign port") + // 等待K8s控制器完成端口分配 time.Sleep(20 * time.Second) @@ -397,9 +438,11 @@ func updateNodePortAsync(containerName string, namespace string, userId, repoId log.Error("Failed to get K8s client in async updater: %v", err) return } + log.Info("updateNodePortAsync: K8s client created successfully") // 尝试最多5次获取端口 for i := 0; i < 5; i++ { + log.Info("updateNodePortAsync: Attempt %d/5 to retrieve NodePort for %s", i+1, containerName) getOpts := &devcontainer_k8s_agent_module.GetDevcontainerOptions{ GetOptions: metav1.GetOptions{}, Name: containerName, @@ -409,6 +452,8 @@ func updateNodePortAsync(containerName string, namespace string, userId, repoId devcontainer, err := devcontainer_k8s_agent_module.GetDevcontainer(&ctx, client, getOpts) if err == nil && devcontainer != nil && devcontainer.Status.NodePortAssigned > 0 { + log.Info("updateNodePortAsync: Success! Found NodePort %d for %s", + devcontainer.Status.NodePortAssigned, containerName) // 获取到正确的端口,更新数据库 realNodePort := devcontainer.Status.NodePortAssigned @@ -439,8 +484,9 @@ func updateNodePortAsync(containerName string, namespace string, userId, repoId return } + log.Info("updateNodePortAsync: Port not yet assigned, waiting 5 seconds before next attempt") time.Sleep(5 * time.Second) } - log.Warn("Failed to retrieve real NodePort after multiple attempts") + log.Warn("updateNodePortAsync: Failed to retrieve real NodePort after multiple attempts") } From 475b1d0c2264c079e7bcf09f4052d122ececa24c Mon Sep 17 00:00:00 2001 From: panshuxiao Date: Mon, 26 May 2025 10:20:57 +0800 Subject: [PATCH 2/5] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E4=BA=86devcontainer?= =?UTF-8?q?=E7=9A=84NAT=E6=98=A0=E5=B0=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/setting/devcontainer.go | 76 ++++++++++++++++++++++++++++-- services/devcontainer/k8s_agent.go | 39 +++++++++++++-- 2 files changed, 107 insertions(+), 8 deletions(-) diff --git a/modules/setting/devcontainer.go b/modules/setting/devcontainer.go index b4928cc33d..d3b3b98fd5 100644 --- a/modules/setting/devcontainer.go +++ b/modules/setting/devcontainer.go @@ -43,7 +43,7 @@ type SSHKeyPairType struct { type CloudType struct { Enabled bool Provider string - Tencent CloudProviderTencentType `ini:"devcontainer.cloud.tencent"` + Tencent CloudProviderTencentType // 移除 ini 标签,通过代码处理 } type CloudProviderTencentType struct { @@ -127,7 +127,7 @@ func validateDevcontainerCloudSettings() { Cloud.Enabled = false } - if Cloud.Tencent.IpProtocol != "TCP" && Cloud.Tencent.IpProtocol != "UDP" { + if Cloud.Tencent.IpProtocol != "TCP" && Cloud.Tencent.IpProtocol != "UDP" && Cloud.Tencent.IpProtocol != "tcp" && Cloud.Tencent.IpProtocol != "udp" { log.Warn("INVALID IP Protocol '%v' for DevStar Cloud Provider Tencent", Cloud.Tencent.IpProtocol) Cloud.Enabled = false } @@ -163,7 +163,7 @@ func validateDevcontainerCloudSettings() { } -// 修改 loadDevcontainerFrom 函数以更好地处理不同配置节点 +// 修改 loadDevcontainerFrom 函数以支持新旧配置节 func loadDevcontainerFrom(rootCfg ConfigProvider) { // 检查是否存在新的配置节 hasNewConfig := true @@ -196,10 +196,76 @@ func loadDevcontainerFrom(rootCfg ConfigProvider) { validateSSHKeyPairSettings() if Devcontainer.Agent == "k8s" || Devcontainer.Agent == KUBERNETES { - mustMapSetting(rootCfg, "devcontainer.cloud", &Cloud) - validateDevcontainerCloudSettings() + // 调用新的云配置加载函数 + loadCloudConfigWithFallback(rootCfg) } // 打印最终使用的命名空间 log.Info("DevContainer 将在命名空间 '%s' 中创建", Devcontainer.Namespace) } + +// 新增: 处理云配置加载的函数,支持新旧两种配置节 +func loadCloudConfigWithFallback(rootCfg ConfigProvider) { + // 1. 先尝试加载主配置节 + hasDevcontainerCloud := true + if _, err := rootCfg.GetSection("devcontainer.cloud"); err != nil { + hasDevcontainerCloud = false + } + + hasDevstarCloud := true + if _, err := rootCfg.GetSection("devstar.cloud"); err != nil { + hasDevstarCloud = false + } + + // 2. 优先使用新配置节,不存在则使用旧配置节 + var cloudSectionName string + if hasDevcontainerCloud { + cloudSectionName = "devcontainer.cloud" + log.Info("从 [devcontainer.cloud] 节加载云配置") + } else if hasDevstarCloud { + cloudSectionName = "devstar.cloud" + log.Info("从 [devstar.cloud] 节加载云配置") + } else { + log.Warn("未找到云配置节,Cloud 功能将被禁用") + Cloud.Enabled = false + return + } + + // 3. 加载基本云配置 + if err := rootCfg.Section(cloudSectionName).MapTo(&Cloud); err != nil { + log.Error("加载云配置时出错: %v", err) + Cloud.Enabled = false + return + } + + // 4. 根据选择的配置节路径,决定腾讯云配置节路径 + var tencentSectionName string + if cloudSectionName == "devcontainer.cloud" { + tencentSectionName = "devcontainer.cloud.tencent" + } else { + tencentSectionName = "devstar.cloud.tencent" + } + + // 5. 检查腾讯云配置节是否存在 + if _, err := rootCfg.GetSection(tencentSectionName); err != nil { + log.Warn("未找到腾讯云配置节 [%s]", tencentSectionName) + if Cloud.Provider == CLOUD_PROVIDER_TENCENT { + log.Error("虽然指定使用腾讯云,但未找到对应配置,Cloud 功能将被禁用") + Cloud.Enabled = false + } + return + } + + // 6. 加载腾讯云配置 + if Cloud.Provider == CLOUD_PROVIDER_TENCENT { + log.Info("从 [%s] 节加载腾讯云配置", tencentSectionName) + if err := rootCfg.Section(tencentSectionName).MapTo(&Cloud.Tencent); err != nil { + log.Error("加载腾讯云配置时出错: %v", err) + Cloud.Enabled = false + return + } + } + + // 7. 验证云配置 + validateDevcontainerCloudSettings() +} diff --git a/services/devcontainer/k8s_agent.go b/services/devcontainer/k8s_agent.go index 2a54442e66..ea95a3fc23 100644 --- a/services/devcontainer/k8s_agent.go +++ b/services/devcontainer/k8s_agent.go @@ -296,10 +296,27 @@ func AssignDevcontainerCreation2K8sOperator(ctx *context.Context, newDevContaine } else { log.Info("AssignDevcontainerCreation2K8sOperator: NodePort %d assigned immediately to %s", nodePort, devcontainerInCluster.Name) + // 端口已分配,直接使用 newDevContainer.DevcontainerPort = nodePort log.Info("DevContainer created in cluster - Name: %s, NodePort: %d", devcontainerInCluster.Name, nodePort) + + // 创建NAT端口映射 - 使用统一的描述格式 + devcontainerNatRuleDescription := setting.DEVCONTAINER_CLOUD_NAT_RULE_DESCRIPTION_PREFIX + newDevContainer.Name + privatePort := uint64(nodePort) + publicPort := privatePort // 使用相同的端口号 + + log.Info("AssignDevcontainerCreation2K8sOperator: Creating NAT port mapping: private=%d, public=%d, desc=%s", + privatePort, publicPort, devcontainerNatRuleDescription) + + err = devstar_cloud_provider.CreateNATRulePort(privatePort, publicPort, devcontainerNatRuleDescription) + if err != nil { + log.Error("AssignDevcontainerCreation2K8sOperator: Failed to create NAT port mapping: %v", err) + // 记录错误但不中断流程 + } else { + log.Info("AssignDevcontainerCreation2K8sOperator: NAT port mapping created successfully") + } } log.Info("DevContainer created in cluster - Name: %s, NodePort: %d", @@ -440,9 +457,9 @@ func updateNodePortAsync(containerName string, namespace string, userId, repoId } log.Info("updateNodePortAsync: K8s client created successfully") - // 尝试最多5次获取端口 - for i := 0; i < 5; i++ { - log.Info("updateNodePortAsync: Attempt %d/5 to retrieve NodePort for %s", i+1, containerName) + // 尝试最多10次获取端口 + for i := 0; i < 10; i++ { + log.Info("updateNodePortAsync: Attempt %d/10 to retrieve NodePort for %s", i+1, containerName) getOpts := &devcontainer_k8s_agent_module.GetDevcontainerOptions{ GetOptions: metav1.GetOptions{}, Name: containerName, @@ -457,6 +474,22 @@ func updateNodePortAsync(containerName string, namespace string, userId, repoId // 获取到正确的端口,更新数据库 realNodePort := devcontainer.Status.NodePortAssigned + // 创建NAT端口映射 - 使用统一的描述格式 + devcontainerNatRuleDescription := setting.DEVCONTAINER_CLOUD_NAT_RULE_DESCRIPTION_PREFIX + containerName + privatePort := uint64(realNodePort) + publicPort := privatePort // 使用相同的端口号 + + log.Info("updateNodePortAsync: Creating NAT port mapping: private=%d, public=%d, desc=%s", + privatePort, publicPort, devcontainerNatRuleDescription) + + err = devstar_cloud_provider.CreateNATRulePort(privatePort, publicPort, devcontainerNatRuleDescription) + if err != nil { + log.Error("updateNodePortAsync: Failed to create NAT port mapping: %v", err) + // 记录错误但继续更新数据库 + } else { + log.Info("updateNodePortAsync: NAT port mapping created successfully") + } + // 记录 ttyd 端口信息到日志 if len(devcontainer.Status.ExtraPortsAssigned) > 0 { for _, portInfo := range devcontainer.Status.ExtraPortsAssigned { From 72a526c65aee6cbb15db71dd71ea1ca19df6a34f Mon Sep 17 00:00:00 2001 From: panshuxiao Date: Wed, 28 May 2025 01:30:51 +0800 Subject: [PATCH 3/5] =?UTF-8?q?=E5=AE=8C=E5=96=84=E4=BA=86webterminal?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../devcontainerapp_controller.go | 86 +++++++ services/devcontainer/devcontainer.go | 102 +++++++-- services/devcontainer/k8s_agent.go | 211 +++++++++++++++++- 3 files changed, 377 insertions(+), 22 deletions(-) diff --git a/modules/k8s/controller/devcontainer/devcontainerapp_controller.go b/modules/k8s/controller/devcontainer/devcontainerapp_controller.go index 5ff8110cec..3a634523f9 100644 --- a/modules/k8s/controller/devcontainer/devcontainerapp_controller.go +++ b/modules/k8s/controller/devcontainer/devcontainerapp_controller.go @@ -19,6 +19,7 @@ package devcontainer import ( "context" "strconv" + "strings" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/types" @@ -46,6 +47,7 @@ type DevcontainerAppReconciler struct { // +kubebuilder:rbac:groups=devcontainer.devstar.cn,resources=devcontainerapps/finalizers,verbs=update // +kubebuilder:rbac:groups=apps,resources=statefulsets,verbs=create;delete;get;list;watch // +kubebuilder:rbac:groups="",resources=services,verbs=create;delete;get;list;watch +// +kubebuilder:rbac:groups="",resources=persistentvolumeclaims,verbs=get;list;watch;delete // Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the cluster closer to the desired state. @@ -69,6 +71,44 @@ func (r *DevcontainerAppReconciler) Reconcile(ctx context.Context, req ctrl.Requ return ctrl.Result{}, client.IgnoreNotFound(err) } + // 添加 finalizer 处理逻辑 + finalizerName := "devcontainer.devstar.cn/finalizer" + + // 检查对象是否正在被删除 + if !app.ObjectMeta.DeletionTimestamp.IsZero() { + // 对象正在被删除 - 处理 finalizer + if k8s_sigs_controller_runtime_utils.ContainsFinalizer(app, finalizerName) { + // 执行清理操作 + logger.Info("Cleaning up resources before deletion", "name", app.Name) + + // 查找并删除关联的 PVC + if err := r.cleanupPersistentVolumeClaims(ctx, app); err != nil { + logger.Error(err, "Failed to clean up PVCs") + return ctrl.Result{}, err + } + + // 删除完成后移除 finalizer + k8s_sigs_controller_runtime_utils.RemoveFinalizer(app, finalizerName) + if err := r.Update(ctx, app); err != nil { + logger.Error(err, "Failed to remove finalizer") + return ctrl.Result{}, err + } + } + + // 已标记为删除且处理完成,允许继续删除流程 + return ctrl.Result{}, nil + } + + // 如果对象不包含 finalizer,就添加它 + if !k8s_sigs_controller_runtime_utils.ContainsFinalizer(app, finalizerName) { + logger.Info("Adding finalizer", "name", app.Name) + k8s_sigs_controller_runtime_utils.AddFinalizer(app, finalizerName) + if err := r.Update(ctx, app); err != nil { + logger.Error(err, "Failed to add finalizer") + return ctrl.Result{}, err + } + } + // 检查停止容器的注解 if desiredReplicas, exists := app.Annotations["devstar.io/desiredReplicas"]; exists && desiredReplicas == "0" { logger.Info("DevContainer stop requested via annotation", "name", app.Name) @@ -353,6 +393,52 @@ func (r *DevcontainerAppReconciler) Reconcile(ctx context.Context, req ctrl.Requ return ctrl.Result{}, nil } +// cleanupPersistentVolumeClaims 查找并删除与 DevcontainerApp 关联的所有 PVC +func (r *DevcontainerAppReconciler) cleanupPersistentVolumeClaims(ctx context.Context, app *devcontainer_v1.DevcontainerApp) error { + logger := log.FromContext(ctx) + + // 查找关联的 PVC + pvcList := &core_v1.PersistentVolumeClaimList{} + + // 按标签筛选 + labelSelector := client.MatchingLabels{ + "app": app.Name, + } + if err := r.List(ctx, pvcList, client.InNamespace(app.Namespace), labelSelector); err != nil { + return err + } + + // 如果按标签没找到,尝试按名称模式查找 + if len(pvcList.Items) == 0 { + if err := r.List(ctx, pvcList, client.InNamespace(app.Namespace)); err != nil { + return err + } + + // 筛选出名称包含 DevcontainerApp 名称的 PVC + var filteredItems []core_v1.PersistentVolumeClaim + for _, pvc := range pvcList.Items { + // StatefulSet PVC 命名格式通常为: --<序号> + // 检查是否包含 app 名称作为名称的一部分 + if strings.Contains(pvc.Name, app.Name+"-") { + filteredItems = append(filteredItems, pvc) + logger.Info("Found PVC to delete", "name", pvc.Name) + } + } + pvcList.Items = filteredItems + } + + // 删除找到的 PVC + for i := range pvcList.Items { + logger.Info("Deleting PVC", "name", pvcList.Items[i].Name) + if err := r.Delete(ctx, &pvcList.Items[i]); err != nil && !errors.IsNotFound(err) { + logger.Error(err, "Failed to delete PVC", "name", pvcList.Items[i].Name) + return err + } + } + + return nil +} + // SetupWithManager sets up the controller with the Manager. func (r *DevcontainerAppReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). diff --git a/services/devcontainer/devcontainer.go b/services/devcontainer/devcontainer.go index 2fd6e5579b..8209cd909b 100644 --- a/services/devcontainer/devcontainer.go +++ b/services/devcontainer/devcontainer.go @@ -409,9 +409,25 @@ func GetWebTerminalURL(ctx context.Context, devcontainerName string) (string, er log.Error("GetWebTerminalURL: 加载配置文件失败: %v", err) return "", err } + + // 检查是否启用了基于路径的访问方式 + domain := cfg.Section("server").Key("DOMAIN").Value() - log.Info("GetWebTerminalURL: 生成 ttyd URL: http://%s:%d/", domain, ttydNodePort) - return fmt.Sprintf("http://%s:%d/", domain, ttydNodePort), nil + 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端口,记录详细的调试信息 @@ -444,26 +460,12 @@ func GetWebTerminalURL(ctx context.Context, devcontainerName string) (string, er } func Get_IDE_TerminalURL(ctx *gitea_context.Context, devcontainer *RepoDevContainer) (string, error) { var access_token string - 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 - } - port, err := docker.GetMappedPort(cli, containerID, "22") - if err != nil { - return "", err - } - // 检查session + + // 检查 session 中是否已存在 token if ctx.Session.Get("access_token") != nil { access_token = ctx.Session.Get("access_token").(string) } else { - // 生成token + // 生成 token token := &auth_model.AccessToken{ UID: devcontainer.UserId, Name: "terminal_login_token", @@ -486,8 +488,67 @@ func Get_IDE_TerminalURL(ctx *gitea_context.Context, devcontainer *RepoDevContai } 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 + @@ -495,7 +556,6 @@ func Get_IDE_TerminalURL(ctx *gitea_context.Context, devcontainer *RepoDevContai "&path=" + devcontainer.DevContainerWorkDir + "&access_token=" + access_token + "&devstar_username=" + devcontainer.RepoOwnerName, nil - } func AddPublicKeyToAllRunningDevContainer(ctx context.Context, user *user_model.User, publicKey string) error { diff --git a/services/devcontainer/k8s_agent.go b/services/devcontainer/k8s_agent.go index ea95a3fc23..dbf2f22fb0 100644 --- a/services/devcontainer/k8s_agent.go +++ b/services/devcontainer/k8s_agent.go @@ -3,6 +3,7 @@ package devcontainer import ( "context" "fmt" + "strings" "time" "code.gitea.io/gitea/models/db" @@ -16,9 +17,14 @@ import ( "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/services/devcontainer/errors" "code.gitea.io/gitea/services/devstar_cloud_provider" + networkingv1 "k8s.io/api/networking/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" ) var k8sGroupVersionResource = schema.GroupVersionResource{ @@ -105,6 +111,32 @@ func AssignDevcontainerDeletion2K8sOperator(ctx *context.Context, devcontainersL return err } + // 获取标准 Kubernetes 客户端,用于删除 Ingress + stdClient, err := getStandardKubernetesClient() + if err != nil { + log.Warn("AssignDevcontainerDeletion2K8sOperator: 获取标准 K8s 客户端失败: %v", err) + // 继续执行,不阻止主流程 + } else { + // 先删除与 DevContainer 相关的 Ingress 资源 + for _, devcontainer := range *devcontainersList { + ingressName := fmt.Sprintf("%s-ttyd-ingress", devcontainer.Name) + log.Info("AssignDevcontainerDeletion2K8sOperator: 删除 Ingress %s", ingressName) + + err := stdClient.NetworkingV1().Ingresses(setting.Devcontainer.Namespace).Delete(*ctx, ingressName, metav1.DeleteOptions{}) + if err != nil { + if k8serrors.IsNotFound(err) { + // Ingress 已经不存在,视为正常情况 + log.Info("AssignDevcontainerDeletion2K8sOperator: Ingress %s 不存在,跳过删除", ingressName) + } else { + log.Warn("AssignDevcontainerDeletion2K8sOperator: 删除 Ingress %s 失败: %v", ingressName, err) + // 继续执行,不阻止主流程 + } + } else { + log.Info("AssignDevcontainerDeletion2K8sOperator: 成功删除 Ingress %s", ingressName) + } + } + } + // 2. 调用 modules 层 k8s Agent,执行删除资源 opts := &devcontainer_dto.DeleteDevcontainerOptions{ DeleteOptions: metav1.DeleteOptions{}, @@ -120,11 +152,20 @@ func AssignDevcontainerDeletion2K8sOperator(ctx *context.Context, devcontainersL for _, devcontainer := range *devcontainersList { opts.Name = devcontainer.Name _ = devcontainer_k8s_agent_module.DeleteDevcontainer(ctx, client, opts) + + // 删除SSH端口的NAT规则 devcontainerNatRuleDescription := setting.DEVCONTAINER_CLOUD_NAT_RULE_DESCRIPTION_PREFIX + devcontainer.Name err := devstar_cloud_provider.DeleteNATRulePort(devcontainerNatRuleDescription) if err != nil { log.Warn("[Cloud NAT DELETION Error]: " + err.Error()) } + + // 删除ttyd端口的NAT规则 + ttydNatRuleDescription := setting.DEVCONTAINER_CLOUD_NAT_RULE_DESCRIPTION_PREFIX + devcontainer.Name + "-ttyd" + err = devstar_cloud_provider.DeleteNATRulePort(ttydNatRuleDescription) + if err != nil { + log.Warn("[Cloud ttyd NAT DELETION Error]: " + err.Error()) + } } return nil } @@ -317,11 +358,43 @@ func AssignDevcontainerCreation2K8sOperator(ctx *context.Context, newDevContaine } else { log.Info("AssignDevcontainerCreation2K8sOperator: NAT port mapping created successfully") } + + // 为ttyd端口创建NAT映射 - 查找extraPorts中的ttyd端口 + for _, portInfo := range devcontainerInCluster.Status.ExtraPortsAssigned { + if portInfo.Name == "ttyd" || strings.Contains(portInfo.Name, "ttyd") || + portInfo.Name == "port-7681" || portInfo.ContainerPort == 7681 { + // 找到ttyd端口,创建NAT映射 + ttydPrivatePort := uint64(portInfo.NodePort) + ttydPublicPort := ttydPrivatePort + ttydNatRuleDescription := setting.DEVCONTAINER_CLOUD_NAT_RULE_DESCRIPTION_PREFIX + newDevContainer.Name + "-ttyd" + + log.Info("AssignDevcontainerCreation2K8sOperator: Creating NAT port mapping for ttyd: private=%d, public=%d, desc=%s", + ttydPrivatePort, ttydPublicPort, ttydNatRuleDescription) + + err = devstar_cloud_provider.CreateNATRulePort(ttydPrivatePort, ttydPublicPort, ttydNatRuleDescription) + if err != nil { + log.Error("AssignDevcontainerCreation2K8sOperator: Failed to create NAT port mapping for ttyd: %v", err) + } else { + log.Info("AssignDevcontainerCreation2K8sOperator: NAT port mapping for ttyd created successfully") + } + break + } + } } log.Info("DevContainer created in cluster - Name: %s, NodePort: %d", devcontainerInCluster.Name, devcontainerInCluster.Status.NodePortAssigned) + + // 为 ttyd 服务创建 Ingress + err = createDevContainerTTYDIngress(ctx, devcontainerInCluster.Name) + if err != nil { + log.Warn("AssignDevcontainerCreation2K8sOperator: 创建 ttyd Ingress 失败: %v", err) + // 不阻止主流程,只记录警告 + } else { + log.Info("AssignDevcontainerCreation2K8sOperator: 创建 ttyd Ingress 成功") + } + // 4. 层层返回 nil,自动提交数据库事务,完成 DevContainer 创建 return nil } @@ -490,11 +563,29 @@ func updateNodePortAsync(containerName string, namespace string, userId, repoId log.Info("updateNodePortAsync: NAT port mapping created successfully") } - // 记录 ttyd 端口信息到日志 + // 记录 ttyd 端口信息到日志并创建NAT映射 if len(devcontainer.Status.ExtraPortsAssigned) > 0 { for _, portInfo := range devcontainer.Status.ExtraPortsAssigned { log.Info("Found extra port for %s: name=%s, nodePort=%d, containerPort=%d", containerName, portInfo.Name, portInfo.NodePort, portInfo.ContainerPort) + + // 为ttyd端口创建NAT映射 + if portInfo.Name == "ttyd" || strings.Contains(portInfo.Name, "ttyd") || + portInfo.Name == "port-7681" || portInfo.ContainerPort == 7681 { + ttydPrivatePort := uint64(portInfo.NodePort) + ttydPublicPort := ttydPrivatePort + ttydNatRuleDescription := setting.DEVCONTAINER_CLOUD_NAT_RULE_DESCRIPTION_PREFIX + containerName + "-ttyd" + + log.Info("updateNodePortAsync: Creating NAT port mapping for ttyd: private=%d, public=%d, desc=%s", + ttydPrivatePort, ttydPublicPort, ttydNatRuleDescription) + + err := devstar_cloud_provider.CreateNATRulePort(ttydPrivatePort, ttydPublicPort, ttydNatRuleDescription) + if err != nil { + log.Error("updateNodePortAsync: Failed to create NAT port mapping for ttyd: %v", err) + } else { + log.Info("updateNodePortAsync: NAT port mapping for ttyd created successfully") + } + } } } @@ -523,3 +614,121 @@ func updateNodePortAsync(containerName string, namespace string, userId, repoId log.Warn("updateNodePortAsync: Failed to retrieve real NodePort after multiple attempts") } + +// 创建 DevContainer ttyd 服务的 Ingress 资源 +func createDevContainerTTYDIngress(ctx *context.Context, devcontainerName string) error { + log.Info("createDevContainerTTYDIngress: 开始为 %s 创建 ttyd Ingress", devcontainerName) + + // 获取标准 Kubernetes 客户端 + stdClient, err := getStandardKubernetesClient() + if err != nil { + log.Error("createDevContainerTTYDIngress: 获取 K8s 客户端失败: %v", err) + return err + } + + // 从配置中读取域名 + cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf) + if err != nil { + log.Error("createDevContainerTTYDIngress: 加载配置文件失败: %v", err) + return err + } + + domain := cfg.Section("server").Key("DOMAIN").Value() + + // 从容器名称中提取用户名和仓库名 + parts := strings.Split(devcontainerName, "-") + var username, repoName string + if len(parts) >= 2 { + username = parts[0] + repoName = parts[1] + } else { + username = "unknown" + repoName = "unknown" + } + + // 构建 PATH 和 Ingress 配置 + path := fmt.Sprintf("/%s/%s/dev-container-webterminal", username, repoName) + ingressName := fmt.Sprintf("%s-ttyd-ingress", devcontainerName) + serviceName := fmt.Sprintf("%s-svc", devcontainerName) + + // 创建 Ingress 资源 + pathType := networkingv1.PathTypePrefix + ingressClassName := "nginx" + + ingress := &networkingv1.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Name: ingressName, + Namespace: setting.Devcontainer.Namespace, + Annotations: map[string]string{ + "nginx.ingress.kubernetes.io/proxy-send-timeout": "3600", + "nginx.ingress.kubernetes.io/proxy-read-timeout": "3600", + "nginx.ingress.kubernetes.io/websocket-services": serviceName, + "nginx.ingress.kubernetes.io/proxy-http-version": "1.1", + "nginx.ingress.kubernetes.io/rewrite-target": "/", + "nginx.ingress.kubernetes.io/configuration-snippet": ` + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header X-Forwarded-For $remote_addr; + `, + }, + }, + Spec: networkingv1.IngressSpec{ + IngressClassName: &ingressClassName, + Rules: []networkingv1.IngressRule{ + { + Host: domain, + IngressRuleValue: networkingv1.IngressRuleValue{ + HTTP: &networkingv1.HTTPIngressRuleValue{ + Paths: []networkingv1.HTTPIngressPath{ + { + Path: path, + PathType: &pathType, + Backend: networkingv1.IngressBackend{ + Service: &networkingv1.IngressServiceBackend{ + Name: serviceName, + Port: networkingv1.ServiceBackendPort{ + Number: 7681, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + + // 创建 Ingress 资源 + _, err = stdClient.NetworkingV1().Ingresses(setting.Devcontainer.Namespace).Create(*ctx, ingress, metav1.CreateOptions{}) + if err != nil { + log.Error("createDevContainerTTYDIngress: 创建 Ingress 失败: %v", err) + return err + } + + log.Info("createDevContainerTTYDIngress: 成功创建 Ingress %s,ttyd 可通过 https://%s%s 访问", ingressName, domain, path) + return nil +} + +// 获取标准 Kubernetes 客户端 +func getStandardKubernetesClient() (*kubernetes.Clientset, error) { + // 使用与 GetKubernetesClient 相同的逻辑获取配置 + config, err := clientcmd.BuildConfigFromFlags("", clientcmd.RecommendedHomeFile) + if err != nil { + // 如果集群外配置失败,尝试集群内配置 + log.Warn("Failed to obtain Kubernetes config outside of cluster: %v", err) + config, err = rest.InClusterConfig() + if err != nil { + return nil, fmt.Errorf("获取 K8s 配置失败 (集群内外均失败): %v", err) + } + } + + // 创建标准客户端 + stdClient, err := kubernetes.NewForConfig(config) + if err != nil { + return nil, fmt.Errorf("创建标准 K8s 客户端失败: %v", err) + } + + return stdClient, nil +} From f69aaa099d8f9129c80ff44b27ebfc76b8e9cd7f Mon Sep 17 00:00:00 2001 From: panshuxiao Date: Tue, 17 Jun 2025 13:15:06 +0800 Subject: [PATCH 4/5] =?UTF-8?q?=E5=88=A0=E9=99=A4k8s-agent=E7=9A=84NAT?= =?UTF-8?q?=E7=9B=B8=E5=85=B3=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- services/devcontainer/k8s_agent.go | 92 +----------------------------- 1 file changed, 1 insertion(+), 91 deletions(-) diff --git a/services/devcontainer/k8s_agent.go b/services/devcontainer/k8s_agent.go index dbf2f22fb0..37cb9c1638 100644 --- a/services/devcontainer/k8s_agent.go +++ b/services/devcontainer/k8s_agent.go @@ -16,7 +16,6 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/services/devcontainer/errors" - "code.gitea.io/gitea/services/devstar_cloud_provider" networkingv1 "k8s.io/api/networking/v1" k8serrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -152,20 +151,6 @@ func AssignDevcontainerDeletion2K8sOperator(ctx *context.Context, devcontainersL for _, devcontainer := range *devcontainersList { opts.Name = devcontainer.Name _ = devcontainer_k8s_agent_module.DeleteDevcontainer(ctx, client, opts) - - // 删除SSH端口的NAT规则 - devcontainerNatRuleDescription := setting.DEVCONTAINER_CLOUD_NAT_RULE_DESCRIPTION_PREFIX + devcontainer.Name - err := devstar_cloud_provider.DeleteNATRulePort(devcontainerNatRuleDescription) - if err != nil { - log.Warn("[Cloud NAT DELETION Error]: " + err.Error()) - } - - // 删除ttyd端口的NAT规则 - ttydNatRuleDescription := setting.DEVCONTAINER_CLOUD_NAT_RULE_DESCRIPTION_PREFIX + devcontainer.Name + "-ttyd" - err = devstar_cloud_provider.DeleteNATRulePort(ttydNatRuleDescription) - if err != nil { - log.Warn("[Cloud ttyd NAT DELETION Error]: " + err.Error()) - } } return nil } @@ -312,9 +297,6 @@ func AssignDevcontainerCreation2K8sOperator(ctx *context.Context, newDevContaine log.Info("AssignDevcontainerCreation2K8sOperator: DevcontainerApp created successfully - Name: %s", devcontainerInCluster.Name) - // // 3. 将分配的 NodePort Service 写回 newDevcontainer,供写入数据库进行下一步操作 - // newDevContainer.DevcontainerPort = devcontainerInCluster.Status.NodePortAssigned - // 3. 处理 NodePort - 检查是否为0(尚未分配) nodePort := devcontainerInCluster.Status.NodePortAssigned @@ -342,44 +324,6 @@ func AssignDevcontainerCreation2K8sOperator(ctx *context.Context, newDevContaine newDevContainer.DevcontainerPort = nodePort log.Info("DevContainer created in cluster - Name: %s, NodePort: %d", devcontainerInCluster.Name, nodePort) - - // 创建NAT端口映射 - 使用统一的描述格式 - devcontainerNatRuleDescription := setting.DEVCONTAINER_CLOUD_NAT_RULE_DESCRIPTION_PREFIX + newDevContainer.Name - privatePort := uint64(nodePort) - publicPort := privatePort // 使用相同的端口号 - - log.Info("AssignDevcontainerCreation2K8sOperator: Creating NAT port mapping: private=%d, public=%d, desc=%s", - privatePort, publicPort, devcontainerNatRuleDescription) - - err = devstar_cloud_provider.CreateNATRulePort(privatePort, publicPort, devcontainerNatRuleDescription) - if err != nil { - log.Error("AssignDevcontainerCreation2K8sOperator: Failed to create NAT port mapping: %v", err) - // 记录错误但不中断流程 - } else { - log.Info("AssignDevcontainerCreation2K8sOperator: NAT port mapping created successfully") - } - - // 为ttyd端口创建NAT映射 - 查找extraPorts中的ttyd端口 - for _, portInfo := range devcontainerInCluster.Status.ExtraPortsAssigned { - if portInfo.Name == "ttyd" || strings.Contains(portInfo.Name, "ttyd") || - portInfo.Name == "port-7681" || portInfo.ContainerPort == 7681 { - // 找到ttyd端口,创建NAT映射 - ttydPrivatePort := uint64(portInfo.NodePort) - ttydPublicPort := ttydPrivatePort - ttydNatRuleDescription := setting.DEVCONTAINER_CLOUD_NAT_RULE_DESCRIPTION_PREFIX + newDevContainer.Name + "-ttyd" - - log.Info("AssignDevcontainerCreation2K8sOperator: Creating NAT port mapping for ttyd: private=%d, public=%d, desc=%s", - ttydPrivatePort, ttydPublicPort, ttydNatRuleDescription) - - err = devstar_cloud_provider.CreateNATRulePort(ttydPrivatePort, ttydPublicPort, ttydNatRuleDescription) - if err != nil { - log.Error("AssignDevcontainerCreation2K8sOperator: Failed to create NAT port mapping for ttyd: %v", err) - } else { - log.Info("AssignDevcontainerCreation2K8sOperator: NAT port mapping for ttyd created successfully") - } - break - } - } } log.Info("DevContainer created in cluster - Name: %s, NodePort: %d", @@ -547,45 +491,11 @@ func updateNodePortAsync(containerName string, namespace string, userId, repoId // 获取到正确的端口,更新数据库 realNodePort := devcontainer.Status.NodePortAssigned - // 创建NAT端口映射 - 使用统一的描述格式 - devcontainerNatRuleDescription := setting.DEVCONTAINER_CLOUD_NAT_RULE_DESCRIPTION_PREFIX + containerName - privatePort := uint64(realNodePort) - publicPort := privatePort // 使用相同的端口号 - - log.Info("updateNodePortAsync: Creating NAT port mapping: private=%d, public=%d, desc=%s", - privatePort, publicPort, devcontainerNatRuleDescription) - - err = devstar_cloud_provider.CreateNATRulePort(privatePort, publicPort, devcontainerNatRuleDescription) - if err != nil { - log.Error("updateNodePortAsync: Failed to create NAT port mapping: %v", err) - // 记录错误但继续更新数据库 - } else { - log.Info("updateNodePortAsync: NAT port mapping created successfully") - } - - // 记录 ttyd 端口信息到日志并创建NAT映射 + // 记录 ttyd 端口信息到日志 if len(devcontainer.Status.ExtraPortsAssigned) > 0 { for _, portInfo := range devcontainer.Status.ExtraPortsAssigned { log.Info("Found extra port for %s: name=%s, nodePort=%d, containerPort=%d", containerName, portInfo.Name, portInfo.NodePort, portInfo.ContainerPort) - - // 为ttyd端口创建NAT映射 - if portInfo.Name == "ttyd" || strings.Contains(portInfo.Name, "ttyd") || - portInfo.Name == "port-7681" || portInfo.ContainerPort == 7681 { - ttydPrivatePort := uint64(portInfo.NodePort) - ttydPublicPort := ttydPrivatePort - ttydNatRuleDescription := setting.DEVCONTAINER_CLOUD_NAT_RULE_DESCRIPTION_PREFIX + containerName + "-ttyd" - - log.Info("updateNodePortAsync: Creating NAT port mapping for ttyd: private=%d, public=%d, desc=%s", - ttydPrivatePort, ttydPublicPort, ttydNatRuleDescription) - - err := devstar_cloud_provider.CreateNATRulePort(ttydPrivatePort, ttydPublicPort, ttydNatRuleDescription) - if err != nil { - log.Error("updateNodePortAsync: Failed to create NAT port mapping for ttyd: %v", err) - } else { - log.Info("updateNodePortAsync: NAT port mapping for ttyd created successfully") - } - } } } From 9e568020ed3e18c5effe52be38eb803730ad790e Mon Sep 17 00:00:00 2001 From: panshuxiao Date: Sat, 28 Jun 2025 09:34:02 +0800 Subject: [PATCH 5/5] =?UTF-8?q?devcontainer=E5=8A=A0=E5=9B=9E=E7=AB=AF?= =?UTF-8?q?=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- models/devcontainer/devcontainer.go | 1 + 1 file changed, 1 insertion(+) diff --git a/models/devcontainer/devcontainer.go b/models/devcontainer/devcontainer.go index 80cf860d10..32936217de 100644 --- a/models/devcontainer/devcontainer.go +++ b/models/devcontainer/devcontainer.go @@ -13,6 +13,7 @@ type Devcontainer struct { Id int64 `xorm:"BIGINT pk NOT NULL autoincr 'id' comment('主键,devContainerId')"` Name string `xorm:"VARCHAR(64) charset=utf8mb4 collate=utf8mb4_bin UNIQUE NOT NULL 'name' comment('devContainer名称,自动生成')"` DevcontainerHost string `xorm:"VARCHAR(256) charset=utf8mb4 collate=utf8mb4_bin NOT NULL 'devcontainer_host' comment('SSH Host')"` + DevcontainerPort uint16 `xorm:"SMALLINT UNSIGNED NOT NULL 'devcontainer_port' comment('SSH Port')"` DevcontainerStatus uint16 `xorm:"SMALLINT UNSIGNED NOT NULL 'devcontainer_status' comment('SSH Status')"` DevcontainerUsername string `xorm:"VARCHAR(32) charset=utf8mb4 collate=utf8mb4_bin NOT NULL 'devcontainer_username' comment('SSH Username')"` DevcontainerWorkDir string `xorm:"VARCHAR(256) charset=utf8mb4 collate=utf8mb4_bin NOT NULL 'devcontainer_work_dir' comment('SSH 工作路径,典型值 ~/${project_name},256字节以内')"`