package devcontainer import ( "archive/tar" "bytes" "context" "fmt" "math" "net" "net/url" "regexp" "strconv" "strings" "time" auth_model "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/db" devcontainer_models "code.gitea.io/gitea/models/devcontainer" "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/docker" docker_module "code.gitea.io/gitea/modules/docker" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/templates" gitea_context "code.gitea.io/gitea/services/context" files_service "code.gitea.io/gitea/services/repository/files" "github.com/docker/docker/api/types" "xorm.io/builder" ) func HasDevContainer(ctx context.Context, userID, repoID int64) (bool, error) { var hasDevContainer bool dbEngine := db.GetEngine(ctx) hasDevContainer, err := dbEngine. Table("devcontainer"). Select("*"). Where("user_id = ? AND repo_id = ?", userID, repoID). Exist() if err != nil { return hasDevContainer, err } return hasDevContainer, nil } func HasDevContainerConfiguration(ctx context.Context, repo *gitea_context.Repository) (bool, error) { _, err := FileExists(".devcontainer/devcontainer.json", repo) if err != nil { if git.IsErrNotExist(err) { return false, nil } return false, err } configurationString, err := GetDevcontainerConfigurationString(ctx, repo.Repository) if err != nil { return true, err } configurationModel, err := UnmarshalDevcontainerConfigContent(configurationString) if err != nil { return true, err } // 执行验证 if errs := configurationModel.Validate(); len(errs) > 0 { log.Info("配置验证失败:") for _, err := range errs { fmt.Printf(" - %s\n", err.Error()) } return true, fmt.Errorf("配置格式错误") } else { return true, nil } } func HasDevContainerDockerFile(ctx context.Context, repo *gitea_context.Repository) (bool, error) { _, err := FileExists(".devcontainer/devcontainer.json", repo) if err != nil { if git.IsErrNotExist(err) { return false, nil } return false, err } configurationString, err := GetDevcontainerConfigurationString(ctx, repo.Repository) if err != nil { return false, err } configurationModel, err := UnmarshalDevcontainerConfigContent(configurationString) if err != nil { return false, err } // 执行验证 if errs := configurationModel.Validate(); len(errs) > 0 { log.Info("配置验证失败:") for _, err := range errs { fmt.Printf(" - %s\n", err.Error()) } return false, fmt.Errorf("配置格式错误") } else { log.Info("%v", configurationModel) if configurationModel.Build == nil || configurationModel.Build.Dockerfile == "" { return false, nil } _, err := FileExists(".devcontainer/"+configurationModel.Build.Dockerfile, repo) if err != nil { if git.IsErrNotExist(err) { return false, nil } return false, err } return true, nil } } func CreateDevcontainerConfiguration(repo *repo.Repository, doer *user.User) error { jsonContent, err := templates.AssetFS().ReadFile("repo/devcontainer/default_devcontainer.json") if err != nil { return err } _, err = files_service.ChangeRepoFiles(db.DefaultContext, repo, doer, &files_service.ChangeRepoFilesOptions{ Files: []*files_service.ChangeRepoFile{ { Operation: "create", TreePath: ".devcontainer/devcontainer.json", ContentReader: bytes.NewReader([]byte(jsonContent)), }, }, OldBranch: "main", NewBranch: "main", Message: "add container configuration", }) if err != nil { return err } return nil } func GetWebTerminalURL(ctx context.Context, userID, repoID int64) (string, error) { var devcontainerName string cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf) if err != nil { return "", err } dbEngine := db.GetEngine(ctx) _, err = dbEngine. Table("devcontainer"). Select("name"). Where("user_id = ? AND repo_id = ?", userID, repoID). Get(&devcontainerName) if err != nil { return "", err } if cfg.Section("k8s").Key("ENABLE").Value() == "true" { // K8s 模式:使用 Istio Gateway + VirtualService log.Info("GetWebTerminalURL: 使用 Istio 模式获取 WebTerminal URL for DevContainer: %s", devcontainerName) // 从配置中读取域名 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" } // 构建基于 Istio Gateway 的 URL path := fmt.Sprintf("/%s/%s/dev-container-webterminal", username, repoName) webTerminalURL := fmt.Sprintf("http://%s%s", domain, path) log.Info("GetWebTerminalURL: 生成 Istio WebTerminal URL: %s", webTerminalURL) return webTerminalURL, nil } return "", nil } /* -1不存在 0 已创建数据库记录 1 正在拉取镜像 2 正在创建和启动容器 3 容器安装必要工具 4 容器正在运行 5 正在提交容器更新 6 正在重启 7 正在停止 8 容器已停止 9 正在删除 10已删除 */ func GetDevContainerStatus(ctx context.Context, userID, repoID string) (string, error) { log.Info("GetDevContainerStatus: Starting - userID: %s, repoID: %s", userID, repoID) var id int var containerName string var status uint16 var realTimeStatus uint16 cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf) if err != nil { log.Error("GetDevContainerStatus: Failed to load config: %v", err) return "", err } dbEngine := db.GetEngine(ctx) _, err = dbEngine. Table("devcontainer"). Select("devcontainer_status, id, name"). Where("user_id = ? AND repo_id = ?", userID, repoID). Get(&status, &id, &containerName) if err != nil { log.Error("GetDevContainerStatus: Failed to query database: %v", err) return "", err } log.Info("GetDevContainerStatus: Database query result - id: %d, containerName: %s, status: %d", id, containerName, status) if id == 0 { log.Info("GetDevContainerStatus: No devcontainer found, returning -1") return fmt.Sprintf("%d", -1), nil } realTimeStatus = status log.Info("GetDevContainerStatus: Initial realTimeStatus: %d", realTimeStatus) switch status { //正在重启 case 6: if cfg.Section("k8s").Key("ENABLE").Value() == "true" { // k8s 逻辑:检查 Pod 是否已恢复运行 log.Info("GetDevContainerStatus: K8s branch for case 6 (restarting), container: %s", containerName) opts := &OpenDevcontainerAppDispatcherOptions{ Name: containerName, Wait: false, } log.Info("GetDevContainerStatus: Calling AssignDevcontainerGetting2K8sOperator with opts: %+v", opts) devcontainerApp, err := AssignDevcontainerGetting2K8sOperator(&ctx, opts) if err != nil { log.Error("GetDevContainerStatus: AssignDevcontainerGetting2K8sOperator failed: %v", err) } else if devcontainerApp != nil { log.Info("GetDevContainerStatus: DevcontainerApp retrieved - Name: %s, Ready: %v", devcontainerApp.Name, devcontainerApp.Status.Ready) if devcontainerApp.Status.Ready { realTimeStatus = 4 // 已恢复运行 log.Info("GetDevContainerStatus: Container %s is ready, updating status to 4", containerName) } } else { log.Warn("GetDevContainerStatus: DevcontainerApp is nil for container: %s", containerName) } } else { containerRealTimeStatus, err := GetDevContainerStatusFromDocker(ctx, containerName) if err != nil { return "", err } else if containerRealTimeStatus == "running" { realTimeStatus = 4 } } break //正在关闭 case 7: if cfg.Section("k8s").Key("ENABLE").Value() == "true" { // k8s 逻辑:检查 Pod 是否已停止 log.Info("GetDevContainerStatus: K8s branch for case 7 (stopping), container: %s", containerName) opts := &OpenDevcontainerAppDispatcherOptions{ Name: containerName, Wait: false, } log.Info("GetDevContainerStatus: Calling AssignDevcontainerGetting2K8sOperator for stop check with opts: %+v", opts) devcontainerApp, err := AssignDevcontainerGetting2K8sOperator(&ctx, opts) if err != nil { log.Info("GetDevContainerStatus: DevcontainerApp not found or error, considering stopped: %v", err) realTimeStatus = 8 // 已停止 } else if devcontainerApp == nil || !devcontainerApp.Status.Ready { log.Info("GetDevContainerStatus: DevcontainerApp is nil or not ready, considering stopped") realTimeStatus = 8 // 已停止 } else { log.Info("GetDevContainerStatus: DevcontainerApp still running - Name: %s, Ready: %v", devcontainerApp.Name, devcontainerApp.Status.Ready) } // 已在外部通过 StopDevContainer 触发,此处仅检查状态 } else { containerRealTimeStatus, err := GetDevContainerStatusFromDocker(ctx, containerName) if err != nil { return "", err } else if containerRealTimeStatus == "exited" { realTimeStatus = 8 } else { err = StopDevContainerByDocker(ctx, containerName) if err != nil { log.Info(err.Error()) } } } break case 9: if cfg.Section("k8s").Key("ENABLE").Value() == "true" { // k8s 逻辑:检查 Pod 是否已删除 log.Info("GetDevContainerStatus: K8s branch for case 9 (deleting), container: %s", containerName) opts := &OpenDevcontainerAppDispatcherOptions{ Name: containerName, Wait: false, } log.Info("GetDevContainerStatus: Calling AssignDevcontainerGetting2K8sOperator for delete check with opts: %+v", opts) _, err := AssignDevcontainerGetting2K8sOperator(&ctx, opts) if err != nil { log.Info("GetDevContainerStatus: DevcontainerApp not found, considering deleted: %v", err) realTimeStatus = 10 // 已删除 } else { log.Info("GetDevContainerStatus: DevcontainerApp still exists, not deleted yet") } // 已在外部通过 DeleteDevContainer 触发,此处仅检查状态 } else { isContainerNotFound, err := IsContainerNotFound(ctx, containerName) if err != nil { return "", err } else if isContainerNotFound { realTimeStatus = 10 } else { err = DeleteDevContainerByDocker(ctx, containerName) if err != nil { log.Info(err.Error()) } } } break default: log.Info("other status") } // K8s: 仅在 Ready 后才返回 4;否则维持/降为 3 if cfg.Section("k8s").Key("ENABLE").Value() == "true" && (status == 3 || status == 4) { opts := &OpenDevcontainerAppDispatcherOptions{ Name: containerName, Wait: false, } app, err := AssignDevcontainerGetting2K8sOperator(&ctx, opts) if err != nil || app == nil { // 获取不到 CR 或出错时,保守认为未就绪 realTimeStatus = 3 } else if app.Status.Ready { realTimeStatus = 4 } else { realTimeStatus = 3 } } //状态更新 if realTimeStatus != status { if realTimeStatus == 10 { _, err = dbEngine.Table("devcontainer"). Where("user_id = ? AND repo_id = ? ", userID, repoID). Delete() if err != nil { return "", err } _, err = dbEngine.Table("devcontainer_output"). Where("user_id = ? AND repo_id = ? ", userID, repoID). Delete() if err != nil { return "", err } return "-1", nil } _, err = dbEngine.Table("devcontainer"). Where("user_id = ? AND repo_id = ? ", userID, repoID). Update(&devcontainer_models.Devcontainer{DevcontainerStatus: realTimeStatus}) if err != nil { return "", err } _, err = dbEngine.Table("devcontainer_output"). Where("user_id = ? AND repo_id = ? AND list_id = ?", userID, repoID, status). Update(&devcontainer_models.DevcontainerOutput{Status: "finished"}) if err != nil { return "", err } } log.Info("GetDevContainerStatus: Final realTimeStatus: %d, returning status string", realTimeStatus) return fmt.Sprintf("%d", realTimeStatus), nil } func CreateDevContainer(ctx context.Context, repo *repo.Repository, doer *user.User, publicKeyList []string, isWebTerminal bool) error { containerName := getSanitizedDevcontainerName(doer.Name, repo.Name) cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf) if err != nil { return err } unixTimestamp := time.Now().Unix() newDevcontainer := devcontainer_models.Devcontainer{ Name: containerName, DevcontainerHost: cfg.Section("server").Key("DOMAIN").Value(), DevcontainerUsername: "root", DevcontainerWorkDir: "/workspace", DevcontainerStatus: 0, RepoId: repo.ID, UserId: doer.ID, CreatedUnix: unixTimestamp, UpdatedUnix: unixTimestamp, } dbEngine := db.GetEngine(ctx) _, err = dbEngine. Table("devcontainer"). Insert(newDevcontainer) if err != nil { return err } _, err = dbEngine. Table("devcontainer"). Select("*"). Where("user_id = ? AND repo_id = ?", doer.ID, repo.ID). Get(&newDevcontainer) if err != nil { return err } go func() { otherCtx := context.Background() if cfg.Section("k8s").Key("ENABLE").Value() == "true" { // K8s 模式:直接调用 K8s Operator 创建 DevContainer configurationString, err := GetDevcontainerConfigurationString(otherCtx, repo) if err != nil { log.Info("CreateDevContainer: 读取 devcontainer 配置失败: %v", err) return } configurationModel, err := UnmarshalDevcontainerConfigContent(configurationString) if err != nil { log.Info("CreateDevContainer: 解析 devcontainer 配置失败: %v", err) return } newDTO := &CreateDevcontainerDTO{ Devcontainer: newDevcontainer, SSHPublicKeyList: publicKeyList, GitRepositoryURL: strings.TrimSuffix(setting.AppURL, "/") + repo.Link(), Image: configurationModel.Image, } if err := AssignDevcontainerCreation2K8sOperator(&otherCtx, newDTO); err != nil { log.Error("CreateDevContainer: K8s 创建失败: %v", err) return } } else { imageName, err := CreateDevContainerByDockerCommand(otherCtx, &newDevcontainer, repo, publicKeyList) if err != nil { return } if !isWebTerminal { CreateDevContainerByDockerAPI(otherCtx, &newDevcontainer, imageName, repo, publicKeyList) } } }() return nil } func DeleteDevContainer(ctx context.Context, userID, repoID int64) error { dbEngine := db.GetEngine(ctx) var devContainerInfo devcontainer_models.Devcontainer cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf) if err != nil { return err } _, err = dbEngine. Table("devcontainer"). Select("*"). Where("user_id = ? AND repo_id = ?", userID, repoID). Get(&devContainerInfo) if err != nil { return err } _, err = dbEngine.Table("devcontainer"). Where("user_id = ? AND repo_id = ? ", userID, repoID). Update(&devcontainer_models.Devcontainer{DevcontainerStatus: 9}) if err != nil { return err } go func() { otherCtx := context.Background() if cfg.Section("k8s").Key("ENABLE").Value() == "true" { // k8s 模式:调用 K8s Operator 删除 DevContainer 资源 devList := []devcontainer_models.Devcontainer{devContainerInfo} _ = AssignDevcontainerDeletion2K8sOperator(&otherCtx, &devList) } else { err = DeleteDevContainerByDocker(otherCtx, devContainerInfo.Name) if err != nil { log.Info(err.Error()) } } }() return nil } func RestartDevContainer(ctx context.Context, userID, repoID int64) error { dbEngine := db.GetEngine(ctx) var devContainerInfo devcontainer_models.Devcontainer cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf) if err != nil { return err } _, err = dbEngine. Table("devcontainer"). Select("*"). Where("user_id = ? AND repo_id = ?", userID, repoID). Get(&devContainerInfo) if err != nil { return err } _, err = dbEngine.Table("devcontainer"). Where("user_id = ? AND repo_id = ? ", userID, repoID). Update(&devcontainer_models.Devcontainer{DevcontainerStatus: 6}) if err != nil { return err } go func() { otherCtx := context.Background() if cfg.Section("k8s").Key("ENABLE").Value() == "true" { // k8s 模式:调用 K8s Operator 重启 DevContainer vo := &DevcontainerVO{ DevContainerName: devContainerInfo.Name, UserId: userID, RepoId: repoID, } if err := AssignDevcontainerRestart2K8sOperator(&otherCtx, vo); err != nil { log.Error("RestartDevContainer: K8s 重启失败: %v", err) } } else { err = RestartDevContainerByDocker(otherCtx, devContainerInfo.Name) if err != nil { log.Info(err.Error()) } } }() return nil } func StopDevContainer(ctx context.Context, userID, repoID int64) error { dbEngine := db.GetEngine(ctx) var devContainerInfo devcontainer_models.Devcontainer cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf) if err != nil { return err } _, err = dbEngine. Table("devcontainer"). Select("*"). Where("user_id = ? AND repo_id = ?", userID, repoID). Get(&devContainerInfo) if err != nil { return err } _, err = dbEngine.Table("devcontainer"). Where("user_id = ? AND repo_id = ? ", userID, repoID). Update(&devcontainer_models.Devcontainer{DevcontainerStatus: 7}) if err != nil { return err } go func() { otherCtx := context.Background() if cfg.Section("k8s").Key("ENABLE").Value() == "true" { // k8s 模式:调用 K8s Operator 停止 DevContainer vo := &DevcontainerVO{ DevContainerName: devContainerInfo.Name, UserId: userID, RepoId: repoID, } if err := AssignDevcontainerStop2K8sOperator(&otherCtx, vo); err != nil { log.Error("StopDevContainer: K8s 停止失败: %v", err) } } else { err = StopDevContainerByDocker(otherCtx, devContainerInfo.Name) if err != nil { log.Info(err.Error()) } } }() return nil } func UpdateDevContainer(ctx context.Context, doer *user.User, repo *repo.Repository, updateInfo *UpdateInfo) error { dbEngine := db.GetEngine(ctx) var devContainerInfo devcontainer_models.Devcontainer cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf) if err != nil { return err } _, err = dbEngine. Table("devcontainer"). Select("*"). Where("user_id = ? AND repo_id = ?", doer.ID, repo.ID). Get(&devContainerInfo) if err != nil { return err } _, err = dbEngine.Table("devcontainer"). Where("user_id = ? AND repo_id = ? ", doer.ID, repo.ID). Update(&devcontainer_models.Devcontainer{DevcontainerStatus: 5}) if err != nil { return err } otherCtx := context.Background() if cfg.Section("k8s").Key("ENABLE").Value() == "true" { //k8s的逻辑 } else { updateErr := UpdateDevContainerByDocker(otherCtx, &devContainerInfo, updateInfo, repo, doer) _, err = dbEngine.Table("devcontainer"). Where("user_id = ? AND repo_id = ? ", doer.ID, repo.ID). Update(&devcontainer_models.Devcontainer{DevcontainerStatus: 4}) if err != nil { return err } if updateErr != nil { return updateErr } } return nil } func GetTerminalCommand(ctx context.Context, userID string, repo *repo.Repository) (string, string, error) { dbEngine := db.GetEngine(ctx) var devContainerInfo devcontainer_models.Devcontainer cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf) if err != nil { return "", "", err } _, err = dbEngine. Table("devcontainer"). Select("*"). Where("user_id = ? AND repo_id = ?", userID, repo.ID). Get(&devContainerInfo) if err != nil { return "", "", err } realTimeStatus := devContainerInfo.DevcontainerStatus var cmd string switch devContainerInfo.DevcontainerStatus { case 0: if devContainerInfo.Id > 0 { realTimeStatus = 1 } break case 1: //正在拉取镜像,当镜像拉取成功,则状态转移 if cfg.Section("k8s").Key("ENABLE").Value() == "true" { //k8s的逻辑 } else { configurationString, err := GetDevcontainerConfigurationString(ctx, repo) if err != nil { return "", "", err } configurationModel, err := UnmarshalDevcontainerConfigContent(configurationString) if err != nil { return "", "", err } var imageName string if configurationModel.Build == nil || configurationModel.Build.Dockerfile == "" { imageName = configurationModel.Image } else { imageName = userID + "-" + fmt.Sprintf("%d", repo.ID) + "-dockerfile" } isExist, err := ImageExists(ctx, imageName) if err != nil { return "", "", err } if isExist { realTimeStatus = 2 } else { _, err = dbEngine.Table("devcontainer_output"). Select("command"). Where("user_id = ? AND repo_id = ? AND list_id = ?", userID, repo.ID, realTimeStatus). Get(&cmd) if err != nil { return "", "", err } } } break case 2: //正在创建容器,创建容器成功,则状态转移 if cfg.Section("k8s").Key("ENABLE").Value() == "true" { //k8s的逻辑 } else { status, err := GetDevContainerStatusFromDocker(ctx, devContainerInfo.Name) if err != nil { return "", "", err } if status == "created" { //添加脚本文件 if cfg.Section("k8s").Key("ENABLE").Value() == "true" { } else { userNum, err := strconv.ParseInt(userID, 10, 64) if err != nil { return "", "", err } var scriptContent string scriptContent, err = GetCommandContent(ctx, userNum, repo) log.Info("command: %s", scriptContent) if err != nil { return "", "", err } // 创建 tar 归档文件 var buf bytes.Buffer tw := tar.NewWriter(&buf) defer tw.Close() // 添加文件到 tar 归档 AddFileToTar(tw, "webTerminal.sh", string(scriptContent), 0777) // 创建 Docker 客户端 cli, err := docker_module.CreateDockerClient(ctx) if err != nil { return "", "", err } // 获取容器 ID containerID, err := docker_module.GetContainerID(cli, devContainerInfo.Name) if err != nil { return "", "", err } err = cli.CopyToContainer(ctx, containerID, "/home", bytes.NewReader(buf.Bytes()), types.CopyToContainerOptions{}) if err != nil { log.Info("%v", err) return "", "", err } } realTimeStatus = 3 } } break case 3: //正在初始化容器,初始化容器成功,则状态转移 if cfg.Section("k8s").Key("ENABLE").Value() == "true" { //k8s的逻辑 } else { status, err := CheckDirExistsFromDocker(ctx, devContainerInfo.Name, devContainerInfo.DevcontainerWorkDir) if err != nil { return "", "", err } if status { realTimeStatus = 4 } } break case 4: //正在连接容器 if cfg.Section("k8s").Key("ENABLE").Value() == "true" { //k8s的逻辑 } else { _, err = dbEngine.Table("devcontainer_output"). Select("command"). Where("user_id = ? AND repo_id = ? AND list_id = ?", userID, repo.ID, realTimeStatus). Get(&cmd) if err != nil { return "", "", err } } break } if realTimeStatus != devContainerInfo.DevcontainerStatus { //下一条指令 _, err = dbEngine.Table("devcontainer_output"). Select("command"). Where("user_id = ? AND repo_id = ? AND list_id = ?", userID, repo.ID, realTimeStatus). Get(&cmd) if err != nil { return "", "", err } _, err = dbEngine.Table("devcontainer"). Where("user_id = ? AND repo_id = ? ", userID, repo.ID). Update(&devcontainer_models.Devcontainer{DevcontainerStatus: realTimeStatus}) if err != nil { return "", "", err } } return cmd, fmt.Sprintf("%d", realTimeStatus), nil } func GetDevContainerOutput(ctx context.Context, doer *user.User, repo *repo.Repository) (OutputResponse, error) { var devContainerOutput []devcontainer_models.DevcontainerOutput dbEngine := db.GetEngine(ctx) resp := OutputResponse{} var status string var containerName string _, err := dbEngine. Table("devcontainer"). Select("devcontainer_status, name"). Where("user_id = ? AND repo_id = ?", doer.ID, repo.ID). Get(&status, &containerName) if err != nil { return resp, err } err = dbEngine.Table("devcontainer_output"). Where("user_id = ? AND repo_id = ?", doer.ID, repo.ID). Find(&devContainerOutput) if err != nil { return resp, err } if len(devContainerOutput) > 0 { resp.CurrentJob.Title = repo.Name + " Devcontainer Info" resp.CurrentJob.Detail = status if status == "4" { // 获取WebSSH服务端口 webTerminalURL, err := GetWebTerminalURL(ctx, doer.ID, repo.ID) if err == nil { return resp, err } // 解析URL u, err := url.Parse(webTerminalURL) if err != nil { return resp, err } // 分离主机和端口 terminalHost, terminalPort, err := net.SplitHostPort(u.Host) resp.CurrentJob.IP = terminalHost resp.CurrentJob.Port = terminalPort if err != nil { return resp, err } } for _, item := range devContainerOutput { logLines := []ViewStepLogLine{} logLines = append(logLines, ViewStepLogLine{ Index: 1, Message: item.Output, }) resp.CurrentJob.Steps = append(resp.CurrentJob.Steps, &ViewJobStep{ Summary: item.Command, Status: item.Status, Logs: logLines, }) } } return resp, nil } func GetMappedPort(ctx context.Context, containerName string, port string) (uint16, error) { cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf) if err != nil { return 0, err } if cfg.Section("k8s").Key("ENABLE").Value() == "true" { //k8s的逻辑 return 0, nil } else { port, err := docker_module.GetMappedPort(ctx, containerName, port) if err != nil { return 0, err } return port, nil } } func GetDevcontainersList(ctx context.Context, doer *user.User, pageNum, pageSize int) (DevcontainerList, error) { // 0. 构造异常返回时的空数据 var resultDevContainerListVO = DevcontainerList{ Page: 0, PageSize: 50, PageTotalNum: 0, ItemTotalNum: 0, DevContainers: []devcontainer_models.Devcontainer{}, } resultDevContainerListVO.UserID = doer.ID resultDevContainerListVO.Username = doer.Name paginationOption := db.ListOptions{ Page: pageNum, PageSize: pageSize, } paginationOption.ListAll = false // 强制使用分页查询,禁止一次性列举所有 devContainers if paginationOption.Page <= 0 { // 未指定页码/无效页码:查询第 1 页 paginationOption.Page = 1 } if paginationOption.PageSize <= 0 || paginationOption.PageSize > 50 { paginationOption.PageSize = 50 // /无效页面大小/超过每页最大限制:自动调整到系统最大开发容器页面大小 } resultDevContainerListVO.Page = paginationOption.Page resultDevContainerListVO.PageSize = paginationOption.PageSize // 2. SQL 条件构建 sqlCondition := builder.Eq{"user_id": doer.ID} // 执行数据库事务 err := db.WithTx(ctx, func(ctx context.Context) error { // 查询总数 count, err := db.GetEngine(ctx). Table("devcontainer"). Where(sqlCondition). Count() if err != nil { return err } resultDevContainerListVO.ItemTotalNum = count // 无记录直接返回 if count == 0 { return nil } // 计算分页参数 pageSize := int64(resultDevContainerListVO.PageSize) resultDevContainerListVO.PageTotalNum = int(math.Ceil(float64(count) / float64(pageSize))) // 查询分页数据 sess := db.GetEngine(ctx). Table("devcontainer"). Join("INNER", "repository", "devcontainer.repo_id = repository.id"). Where(sqlCondition). OrderBy("devcontainer_id DESC"). Select(`devcontainer.id AS devcontainer_id, devcontainer.name AS devcontainer_name, devcontainer.devcontainer_host AS devcontainer_host, 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`) resultDevContainerListVO.DevContainers = make([]devcontainer_models.Devcontainer, 0, pageSize) err = db.SetSessionPagination(sess, &paginationOption). Find(&resultDevContainerListVO.DevContainers) if err != nil { return err } return nil }) if err != nil { return resultDevContainerListVO, err } return resultDevContainerListVO, nil } func Get_IDE_TerminalURL(ctx *gitea_context.Context, doer *user.User, repo *gitea_context.Repository) (string, error) { dbEngine := db.GetEngine(ctx) var devContainerInfo devcontainer_models.Devcontainer _, err := dbEngine. Table("devcontainer"). Select("*"). Where("user_id = ? AND repo_id = ?", doer.ID, repo.Repository.ID). Get(&devContainerInfo) if err != nil { return "", err } // 加载配置文件 cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf) if err != nil { log.Error("Get_IDE_TerminalURL: 加载配置文件失败: %v", err) return "", err } log.Info("Get_IDE_TerminalURL: 配置文件加载成功, ROOT_URL=%s", cfg.Section("server").Key("ROOT_URL").Value()) 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: devContainerInfo.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 = ?", doer.ID, "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 if cfg.Section("k8s").Key("ENABLE").Value() == "true" { // K8s 环境:通过 DevcontainerApp 的 NodePort 作为 SSH 端口 apiRequestCtx := ctx.Req.Context() opts := &OpenDevcontainerAppDispatcherOptions{ Name: devContainerInfo.Name, Wait: false, } devcontainerApp, err := AssignDevcontainerGetting2K8sOperator(&apiRequestCtx, opts) if err != nil { return "", err } if devcontainerApp == nil || devcontainerApp.Status.NodePortAssigned == 0 { return "", fmt.Errorf("k8s DevcontainerApp 未就绪或未分配 NodePort: %s", devContainerInfo.Name) } port = fmt.Sprintf("%d", devcontainerApp.Status.NodePortAssigned) } else { mappedPort, err := docker_module.GetMappedPort(ctx, devContainerInfo.Name, "22") if err != nil { return "", err } port = fmt.Sprintf("%d", mappedPort) } // 构建并返回 URL return "://mengning.devstar/" + "openProject?host=" + repo.Repository.Name + "&hostname=" + devContainerInfo.DevcontainerHost + "&port=" + port + "&username=" + doer.Name + "&path=" + devContainerInfo.DevcontainerWorkDir + "&access_token=" + access_token + "&devstar_username=" + repo.Repository.OwnerName + "&devstar_domain=" + cfg.Section("server").Key("ROOT_URL").Value(), nil } func GetCommandContent(ctx context.Context, userId int64, repo *repo.Repository) (string, error) { configurationString, err := GetDevcontainerConfigurationString(ctx, repo) if err != nil { return "", err } configurationModel, err := UnmarshalDevcontainerConfigContent(configurationString) if err != nil { return "", err } onCreateCommand := strings.TrimSpace(strings.Join(configurationModel.ParseCommand(configurationModel.OnCreateCommand), "\n")) if _, ok := configurationModel.OnCreateCommand.(map[string]interface{}); ok { // 是 map[string]interface{} 类型 cmdObj := configurationModel.OnCreateCommand.(map[string]interface{}) if pathValue, hasPath := cmdObj["path"]; hasPath { fileCommand, err := GetFileContentByPath(ctx, repo, ".devcontainer/"+pathValue.(string)) if err != nil { return "", err } onCreateCommand += "\n" + fileCommand } } updateCommand := strings.TrimSpace(strings.Join(configurationModel.ParseCommand(configurationModel.UpdateContentCommand), "\n")) if _, ok := configurationModel.UpdateContentCommand.(map[string]interface{}); ok { // 是 map[string]interface{} 类型 cmdObj := configurationModel.UpdateContentCommand.(map[string]interface{}) if pathValue, hasPath := cmdObj["path"]; hasPath { fileCommand, err := GetFileContentByPath(ctx, repo, ".devcontainer/"+pathValue.(string)) if err != nil { return "", err } updateCommand += "\n" + fileCommand } } postCreateCommand := strings.TrimSpace(strings.Join(configurationModel.ParseCommand(configurationModel.PostCreateCommand), "\n")) if _, ok := configurationModel.PostCreateCommand.(map[string]interface{}); ok { // 是 map[string]interface{} 类型 cmdObj := configurationModel.PostCreateCommand.(map[string]interface{}) if pathValue, hasPath := cmdObj["path"]; hasPath { fileCommand, err := GetFileContentByPath(ctx, repo, ".devcontainer/"+pathValue.(string)) if err != nil { return "", err } postCreateCommand += "\n" + fileCommand } } postStartCommand := strings.TrimSpace(strings.Join(configurationModel.ParseCommand(configurationModel.PostStartCommand), "\n")) if _, ok := configurationModel.PostStartCommand.(map[string]interface{}); ok { // 是 map[string]interface{} 类型 cmdObj := configurationModel.PostStartCommand.(map[string]interface{}) if pathValue, hasPath := cmdObj["path"]; hasPath { fileCommand, err := GetFileContentByPath(ctx, repo, ".devcontainer/"+pathValue.(string)) if err != nil { return "", err } postStartCommand += "\n" + fileCommand } } var script []string scripts, err := devcontainer_models.GetScript(ctx, userId, repo.ID) for _, v := range scripts { script = append(script, v) } scriptCommand := strings.TrimSpace(strings.Join(script, "\n")) userCommand := scriptCommand + "\n" + onCreateCommand + "\n" + updateCommand + "\n" + postCreateCommand + "\n" + postStartCommand + "\n" assetFS := templates.AssetFS() Content_tmpl, err := assetFS.ReadFile("repo/devcontainer/devcontainer_tmpl.sh") if err != nil { return "", err } Content_start, err := assetFS.ReadFile("repo/devcontainer/devcontainer_start.sh") if err != nil { return "", err } Content_restart, err := assetFS.ReadFile("repo/devcontainer/devcontainer_restart.sh") if err != nil { return "", err } final_command := string(Content_tmpl) re1 := regexp.MustCompile(`\$\{` + regexp.QuoteMeta("START") + `\}|` + `\$` + regexp.QuoteMeta("START") + `\b`) escapedContentStart := strings.ReplaceAll(string(Content_start), `$`, `$$`) escapedUserCommand := strings.ReplaceAll(userCommand, `$`, `$$`) final_command = re1.ReplaceAllString(final_command, escapedContentStart+"\n"+escapedUserCommand) re1 = regexp.MustCompile(`\$RESTART\b`) escapedContentRestart := strings.ReplaceAll(string(Content_restart), `$`, `$$`) escapedPostStartCommand := strings.ReplaceAll(postStartCommand, `$`, `$$`) final_command = re1.ReplaceAllString(final_command, escapedContentRestart+"\n"+escapedPostStartCommand) return parseCommand(ctx, final_command, userId, repo) } func AddPublicKeyToAllRunningDevContainer(ctx context.Context, userId int64, publicKey string) error { // 加载配置文件 cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf) if err != nil { log.Error("Get_IDE_TerminalURL: 加载配置文件失败: %v", err) return err } if cfg.Section("k8s").Key("ENABLE").Value() == "true" { } else { cli, err := docker.CreateDockerClient(ctx) if err != nil { return err } defer cli.Close() var devcontainerList []devcontainer_models.Devcontainer // 查询所有打开的容器 err = db.GetEngine(ctx). Table("devcontainer"). Where("user_id = ? AND devcontainer_status = ?", userId, 4). Find(&devcontainerList) if err != nil { return err } if len(devcontainerList) > 0 { // 将公钥写入这些打开的容器中 for _, repoDevContainer := range devcontainerList { containerID, err := docker.GetContainerID(cli, repoDevContainer.Name) if err != nil { return err } log.Info("container id: %s, name: %s", containerID, repoDevContainer.Name) // 检查容器状态 containerStatus, err := docker.GetContainerStatus(cli, containerID) if err != nil { continue } if containerStatus == "running" { // 只为处于运行状态的容器添加公钥 _, err = docker.ExecCommandInContainer(ctx, cli, repoDevContainer.Name, fmt.Sprintf("echo '%s' >> ~/.ssh/authorized_keys", publicKey)) if err != nil { return err } } } } return nil } return fmt.Errorf("unknown agent") } func parseCommand(ctx context.Context, command string, userId int64, repo *repo.Repository) (string, error) { variables, err := devcontainer_models.GetVariables(ctx, userId, repo.ID) var variablesName []string variablesCircle := checkEachVariable(variables) for key := range variables { if !variablesCircle[key] { variablesName = append(variablesName, key) } } for ContainsAnySubstring(command, variablesName) { for key, value := range variables { if variablesCircle[key] == true { continue } log.Info("key: %s, value: %s", key, value) re1 := regexp.MustCompile(`\$\{` + regexp.QuoteMeta(key) + `\}|` + `\$` + regexp.QuoteMeta(key) + `\b`) escapedValue := strings.ReplaceAll(value, `$`, `$$`) command = re1.ReplaceAllString(command, escapedValue) variablesName = append(variablesName, key) } } var userSSHPublicKeyList []string err = db.GetEngine(ctx). Table("public_key"). Select("content"). Where("owner_id = ?", userId). Find(&userSSHPublicKeyList) if err != nil { return "", err } re1 := regexp.MustCompile(`\$\{` + regexp.QuoteMeta("PUBLIC_KEY_LIST") + `\}|` + `\$` + regexp.QuoteMeta("PUBLIC_KEY_LIST") + `\b`) command = re1.ReplaceAllString(command, strings.Join(userSSHPublicKeyList, "\n")) return command, nil }