package devcontainer import ( "archive/tar" "bytes" "context" "fmt" "math" "net" "net/url" "os" "time" "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" 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" 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 { jsonString := `{ "name":"template", "image":"mcr.microsoft.com/devcontainers/base:dev-ubuntu-20.04", "forwardPorts": ["8080"], "containerEnv": { "NODE_ENV": "development" }, "initializeCommand": "echo \"init\";", "postCreateCommand": [ "echo \"created\"", "echo \"test\"" ], "runArgs": [ "-p 8888" ] }` _, 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(jsonString)), }, }, 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的逻辑 } return "", nil } /* -1不存在 0 已创建数据库记录 1 正在拉取镜像 2 正在创建和启动容器 3 容器安装必要工具 4 容器正在运行 5 正在提交容器更新 6 正在重启 7 正在停止 8 容器已停止 9 正在删除 10已删除 */ func GetDevContainerStatus(ctx context.Context, userID, repoID string) (string, error) { var id int var containerName string var status uint16 var realTimeStatus uint16 cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf) if err != nil { 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 { return "", err } if id == 0 { return fmt.Sprintf("%d", -1), nil } realTimeStatus = status switch status { //正在重启 case 6: if cfg.Section("k8s").Key("ENABLE").Value() == "true" { //k8s的逻辑 } 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的逻辑 } else { containerRealTimeStatus, err := GetDevContainerStatusFromDocker(ctx, containerName) if err != nil { return "", err } else if containerRealTimeStatus == "exited" { realTimeStatus = 8 } } break case 9: if cfg.Section("k8s").Key("ENABLE").Value() == "true" { //k8s的逻辑 } else { isContainerNotFound, err := IsContainerNotFound(ctx, containerName) if err != nil { return "", err } else if isContainerNotFound { realTimeStatus = 10 } } break default: log.Info("other status") } //状态更新 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 } } 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的逻辑 } 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的逻辑 } else { err = DeleteDevContainerByDocker(otherCtx, &devContainerInfo) 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的逻辑 } else { err = RestartDevContainerByDocker(otherCtx, &devContainerInfo) 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的逻辑 } else { err = StopDevContainerByDocker(otherCtx, &devContainerInfo) 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 } } 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 == "running" { //添加脚本文件 if cfg.Section("k8s").Key("ENABLE").Value() == "true" { } else { var scriptContent []byte _, err = os.Stat("webTerminal.sh") if os.IsNotExist(err) { _, err = os.Stat("/app/gitea/webTerminal.sh") if os.IsNotExist(err) { return "", "", err } else { scriptContent, err = os.ReadFile("/app/gitea/webTerminal.sh") if err != nil { return "", "", err } } } else { scriptContent, err = os.ReadFile("webTerminal.sh") 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 }