diff --git a/models/devcontainer/devcontainer.go b/models/devcontainer/devcontainer.go index 26fc91ffaa..80cf860d10 100644 --- a/models/devcontainer/devcontainer.go +++ b/models/devcontainer/devcontainer.go @@ -13,7 +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字节以内')"` RepoId int64 `xorm:"BIGINT NOT NULL 'repo_id' comment('repository表主键')"` diff --git a/options/locale/locale_zh-CN.ini b/options/locale/locale_zh-CN.ini index e56c8cdfac..d502940981 100644 --- a/options/locale/locale_zh-CN.ini +++ b/options/locale/locale_zh-CN.ini @@ -1020,8 +1020,8 @@ visibility.private_tooltip=仅对您已加入的组织的成员可见。 [repo] dev_container = 开发容器 -dev_container_empty = 您还没有该仓库的开发容器配置 -dev_container_invalid_config_prompt = 开发容器配置无效:请上传有效的 devcontainer.json 至默认分支,且确保仓库未处于存档状态 +dev_container_empty = 本仓库没有开发容器配置 +dev_container_invalid_config_prompt = 开发容器配置无效:需要上传有效的 devcontainer.json 至默认分支,且确保仓库未处于存档状态 dev_container_control.update = 保存开发容器 dev_container_control.create = 创建开发容器 dev_container_control.creation_success_for_user = 用户 '%s' 已成功创建开发容器 diff --git a/public/assets/install.sh b/public/assets/install.sh index c5e91ea80b..252f7904c3 100755 --- a/public/assets/install.sh +++ b/public/assets/install.sh @@ -6,7 +6,7 @@ NAME=DevStar-Studio IMAGE_REGISTRY_USER=mengning997 IMAGE_NAME=devstar-studio VERSION=latest # DevStar Studio的默认版本为最新版本 -PORT=8080 # 设置端口默认值为 8080 +PORT=80 # 设置端口默认值为 80 SSH_PORT=2222 # 设置ssh默认端口号2222 DATA_DIR=${HOME}/devstar_data APP_INI=${DATA_DIR}/app.ini @@ -151,7 +151,7 @@ function usage { success "DevStar usage help:" success " help, -h, --help, Help information" success " start Start DevStar Studio" - success " --port= Specify the port number (default port is 8080)" + success " --port= Specify the port number (default port is 80)" success " --ssh-port= Specify the ssh-port number (default ssh-port is 2222)" success " --version= Specify the DevStar Studio Image Version (default verson is latest)" success " --image= Specify the DevStar Studio Image example: devstar-studio:latest " diff --git a/routers/api/devcontainer/devcontainer.go b/routers/api/devcontainer/devcontainer.go index f2a9da7970..e73310b0eb 100644 --- a/routers/api/devcontainer/devcontainer.go +++ b/routers/api/devcontainer/devcontainer.go @@ -3,6 +3,7 @@ package devcontainer import ( "strconv" + "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/modules/log" web_module "code.gitea.io/gitea/modules/web" Result "code.gitea.io/gitea/routers/entity" @@ -58,6 +59,20 @@ func CreateRepoDevcontainer(ctx *context.Context) { } // 4. 调用 API Service 层创建 DevContainer + repo, err := repo.GetRepositoryByID(ctx, repoId) + if err != nil { + errCreateDevcontainer := Result.ResultType{ + Code: Result.RespFailedCreateDevcontainer.Code, + Msg: Result.RespFailedCreateDevcontainer.Msg, + Data: map[string]string{ + "ErrorMsg": "repo not found", + }, + } + errCreateDevcontainer.RespondJson2HttpResponseWriter(ctx.Resp) + return + } + devcontainer_service.CreateDevcontainerJSON(ctx, repo, ctx.Doer) + opts := &devcontainer_service.CreateDevcontainerOptions{ Actor: ctx.Doer, RepoId: repoId, diff --git a/routers/web/devcontainer/devcontainer.go b/routers/web/devcontainer/devcontainer.go index 596142a62e..67d50bebda 100644 --- a/routers/web/devcontainer/devcontainer.go +++ b/routers/web/devcontainer/devcontainer.go @@ -48,6 +48,16 @@ func GetRepoDevContainerDetails(ctx *context.Context) { ctx.Data["InitializedContainer"] = false } } + ctx.Data["isAdmin"] = false + if ctx.Doer.IsAdmin { + ctx.Data["isAdmin"] = true + ctx.Data["canRead"] = true + } else { + canRead, _ := devcontainer_service.CanCreateDevcontainer(ctx, ctx.Repo.Repository.ID, ctx.Doer.ID) + ctx.Data["canRead"] = canRead + isAdmin, _ := devcontainer_service.IsOwner(ctx, ctx.Repo.Repository.ID, ctx.Doer.ID) + ctx.Data["isAdmin"] = isAdmin + } //ctx.Repo.RepoLink == ctx.Repo.Repository.Link() devContainerMetadata, err := devcontainer_service.GetRepoDevcontainerDetails(ctx, opts) @@ -58,12 +68,41 @@ func GetRepoDevContainerDetails(ctx *context.Context) { if hasDevContainer { ctx.Data["DevContainer"] = devContainerMetadata } + ctx.Data["PullImage"] = false + ctx.Data["CreateContainer"] = false + ctx.Data["Init"] = false + ctx.Data["Running"] = false + ctx.Data["RestartOrStop"] = false + ctx.Data["Save"] = false + switch devContainerMetadata.DevContainerStatus { + case 1: + ctx.Data["PullImage"] = true + case 2: + ctx.Data["CreateContainer"] = true + case 3: + ctx.Data["Init"] = true + case 4: + ctx.Data["Running"] = true + case 5: + ctx.Data["Restart"] = true + case 6: + ctx.Data["Stop"] = true + case 7: + ctx.Data["Save"] = true + default: + log.Info("unknown status") + } // 2. 检查当前仓库的当前分支是否存在有效的 /.devcontainer/devcontainer.json isValidRepoDevcontainerJson := isValidRepoDevcontainerJsonFile(ctx) + hasDockerfile := isValidRepoDevcontainerDockerfile(ctx) if !hasDevContainer && !isValidRepoDevcontainerJson { ctx.Flash.Error(ctx.Tr("repo.dev_container_invalid_config_prompt"), true) } + ctx.Data["HasDockerfile"] = false + if hasDockerfile { + ctx.Data["HasDockerfile"] = true + } // 从devcontainer.json文件提取image字段解析成仓库地址、命名空间、镜像名 devcontainerJson, err := devcontainer_service.GetDevcontainerJsonModel(ctx, ctx.Repo.Repository) if err == nil { @@ -112,7 +151,11 @@ func GetRepoDevContainerDetails(ctx *context.Context) { } func CreateRepoDevContainerConfiguration(ctx *context.Context) { - devcontainer_service.CreateDevcontainerJSON(ctx) + if !ctx.Doer.IsAdmin { + ctx.Flash.Error("permisson denied", true) + return + } + devcontainer_service.CreateDevcontainerJSON(ctx, ctx.Repo.Repository, ctx.Doer) ctx.Redirect(path.Join(ctx.Repo.RepoLink, "/dev-container")) } func ParseImageName(imageName string) (registry, namespace, repo, tag string) { @@ -173,6 +216,28 @@ func isValidRepoDevcontainerJsonFile(ctx *context.Context) bool { return true } +// 辅助判断当前仓库的当前分支是否存在有效的 /.devcontainer/Dockerfile +func isValidRepoDevcontainerDockerfile(ctx *context.Context) bool { + + // 1. 仓库非空,且非 Archived 状态 + if ctx.Repo.Repository.IsEmpty || ctx.Repo.Repository.IsArchived { + return false + } + + // 2. 当前分支的目录 .devcontainer 下存在 devcontainer.json 文件 + dockerfilePath, err := devcontainer_service.GetDockerfilePath(ctx, ctx.Repo.Repository) + if err != nil { + return false + } + dockerfileExists, err := ctx.Repo.FileExists(".devcontainer/"+dockerfilePath, ctx.Repo.BranchName) + if err != nil || !dockerfileExists { + return false + } + + // 3. TODO: DevContainer 格式正确 + return true +} + // 辅助判断当前用户在当前仓库是否已有 Dev Container func isUserDevcontainerAlreadyInRepository(ctx *context.Context) bool { @@ -186,7 +251,10 @@ func isUserDevcontainerAlreadyInRepository(ctx *context.Context) bool { } func UpdateRepoDevContainerForCurrentActor(ctx *context.Context) { - + if !ctx.Doer.IsAdmin { + ctx.Flash.Error("permisson denied", true) + return + } opt := &devcontainer_service.RepoDevcontainerOptions{ Actor: ctx.Doer, Repository: ctx.Repo.Repository, @@ -224,7 +292,6 @@ func UpdateRepoDevContainerForCurrentActor(ctx *context.Context) { // 删除仓库 当前用户 Dev Container func DeleteRepoDevContainerForCurrentActor(ctx *context.Context) { - if isUserDevcontainerAlreadyInRepository(ctx) { opts := &devcontainer_service.RepoDevcontainerOptions{ Actor: ctx.Doer, @@ -315,6 +382,7 @@ func RestartContainer(ctx *context.Context) { if err != nil { ctx.Flash.Error("fail to restart container") } + ctx.JSON(http.StatusOK, map[string]string{}) } func StopContainer(ctx *context.Context) { @@ -328,5 +396,6 @@ func StopContainer(ctx *context.Context) { if err != nil { ctx.Flash.Error("fail to stop container") } + ctx.JSON(http.StatusOK, map[string]string{}) } diff --git a/routers/web/web.go b/routers/web/web.go index f9b2985566..d0a1ddca79 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -1353,7 +1353,7 @@ func registerRoutes(m *web.Router) { m.Group("/{username}/{reponame}/dev-container", func() { // repo Dev Container m.Group("", func() { m.Combo("").Get(devcontainer_web.GetRepoDevContainerDetails) - }, reqSignIn, context.RepoAssignment, reqRepoCodeReader, context.RepoRef(), canEnableEditor, context.RepoMustNotBeArchived(), repo.MustBeEditable) + }, context.RepoRef()) m.Get("/createConfiguration", devcontainer_web.CreateRepoDevContainerConfiguration) m.Get("/create", devcontainer_web.CreateRepoDevContainer, context.RepoMustNotBeArchived()) // 仓库状态非 Archived 才可以创建 DevContainer @@ -1367,7 +1367,7 @@ func registerRoutes(m *web.Router) { // 1. 已登录 // 2. repo 信息已加载到 Gitea Web Context (否则无法判定当前repo是否有写入Code权限,从而返回无权访问错误码 HTTP 404) // 3. 具有code写入权限 - reqSignIn, context.RepoAssignment, reqRepoCodeWriter, + reqSignIn, context.RepoAssignment, reqRepoCodeReader, ) m.Group("/{username}/{reponame}", func() { // repo tags diff --git a/services/devcontainer/devcontainer.go b/services/devcontainer/devcontainer.go index 8209cd909b..218bc0f2e9 100644 --- a/services/devcontainer/devcontainer.go +++ b/services/devcontainer/devcontainer.go @@ -15,15 +15,17 @@ import ( "code.gitea.io/gitea/models/db" devcontainer_model "code.gitea.io/gitea/models/devcontainer" devcontainer_models_errors "code.gitea.io/gitea/models/devcontainer/errors" + "code.gitea.io/gitea/models/perm" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/docker" devcontainer_k8s_agent_module "code.gitea.io/gitea/modules/k8s" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - gitea_context "code.gitea.io/gitea/services/context" devcontainer_service_errors "code.gitea.io/gitea/services/devcontainer/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "code.gitea.io/gitea/services/devstar_ssh_key_pair/api_service" "github.com/google/uuid" @@ -125,7 +127,7 @@ func GetRepoDevcontainerDetails(ctx context.Context, opts *RepoDevcontainerOptio "devcontainer.id AS devcontainer_id,"+ "devcontainer.name AS devcontainer_name,"+ "devcontainer.devcontainer_host AS devcontainer_host,"+ - "devcontainer.devcontainer_port AS devcontainer_port,"+ + "devcontainer.devcontainer_status AS devcontainer_status,"+ "devcontainer.devcontainer_username AS devcontainer_username,"+ "devcontainer.devcontainer_work_dir AS devcontainer_work_dir,"+ "devcontainer.repo_id AS repo_id,"+ @@ -206,6 +208,7 @@ func CreateRepoDevcontainer(ctx context.Context, opts *CreateRepoDevcontainerOpt DevcontainerHost: cfg.Section("server").Key("DOMAIN").Value(), DevcontainerUsername: "root", DevcontainerWorkDir: "/data/workspace", + DevcontainerStatus: 0, RepoId: opts.Repository.ID, UserId: opts.Actor.ID, CreatedUnix: unixTimestamp, @@ -217,6 +220,22 @@ func CreateRepoDevcontainer(ctx context.Context, opts *CreateRepoDevcontainerOpt } log.Info("CreateRepoDevcontainer: 初始化 DevContainer 对象, host=%s, workDir=%s, gitURL=%s", newDevcontainer.DevcontainerHost, newDevcontainer.DevcontainerWorkDir, newDevcontainer.GitRepositoryURL) + if setting.Devcontainer.Agent == setting.DOCKER { + rowsAffect, err := db.GetEngine(ctx). + Table("devcontainer"). + Insert(newDevcontainer.Devcontainer) + if err != nil { + return devcontainer_models_errors.ErrFailedToOperateDevcontainerDB{ + Action: fmt.Sprintf("insert new DevContainer for user '%s' in repo '%s'", username, repoName), + Message: err.Error(), + } + } else if rowsAffect == 0 { + return devcontainer_models_errors.ErrFailedToOperateDevcontainerDB{ + Action: fmt.Sprintf("insert new DevContainer for user '%s' in repo '%s'", username, repoName), + Message: "expected 1 row to be inserted, but got 0", + } + } + } // 在数据库事务中创建 Dev Container 分配资源,出错时自动回滚相对应数据库字段,保证数据一致 dbTransactionErr := db.WithTx(ctx, func(ctx context.Context) error { @@ -272,28 +291,28 @@ func CreateRepoDevcontainer(ctx context.Context, opts *CreateRepoDevcontainerOpt 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) } - // 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 }) @@ -550,7 +569,8 @@ func Get_IDE_TerminalURL(ctx *gitea_context.Context, devcontainer *RepoDevContai // 构建并返回 URL return "://mengning.devstar/" + - "openProject?host=" + devcontainer.DevContainerHost + + "openProject?host=" + devcontainer.RepoName + + "&hostname=" + devcontainer.DevContainerHost + "&port=" + port + "&username=" + devcontainer.DevContainerUsername + "&path=" + devcontainer.DevContainerWorkDir + @@ -917,3 +937,40 @@ func StopDevcontainer(gitea_ctx context.Context, opts *RepoDevContainer) error { } } +func CanCreateDevcontainer(gitea_ctx context.Context, repoID, userID int64) (bool, error) { + e := db.GetEngine(gitea_ctx) + + teamMember, err := e.Table("team_user"). + Join("INNER", "team_repo", "`team_repo`.team_id = `team_user`.team_id"). + Join("INNER", "team_unit", "`team_unit`.team_id = `team_user`.team_id"). + Where("`team_repo`.repo_id = ? AND (`team_unit`.access_mode >= ? OR (`team_unit`.access_mode = ? AND `team_unit`.`type` = ?))", + repoID, perm.AccessModeWrite, perm.AccessModeRead, unit.TypeCode). + And("team_user.uid = ?", userID).Exist() + if err != nil { + return false, nil + } + if teamMember { + return true, nil + } + + return repo_model.IsCollaborator(gitea_ctx, repoID, userID) + +} +func IsOwner(gitea_ctx context.Context, repoID, userID int64) (bool, error) { + e := db.GetEngine(gitea_ctx) + + teamMember, err := e.Table("team_user"). + Join("INNER", "team_repo", "`team_repo`.team_id = `team_user`.team_id"). + Join("INNER", "team_unit", "`team_unit`.team_id = `team_user`.team_id"). + Where("`team_repo`.repo_id = ? AND `team_unit`.access_mode = ? ", + repoID, perm.AccessModeAdmin). + And("team_user.uid = ?", userID).Exist() + if err != nil { + return false, nil + } + if teamMember { + return true, nil + } + + return e.Get(&repo_model.Collaboration{RepoID: repoID, UserID: userID, Mode: 3}) +} diff --git a/services/devcontainer/devcontainer_json.go b/services/devcontainer/devcontainer_json.go index a4754b42f2..657f3c56e3 100644 --- a/services/devcontainer/devcontainer_json.go +++ b/services/devcontainer/devcontainer_json.go @@ -12,6 +12,7 @@ import ( "code.gitea.io/gitea/models/db" devcontainer_model "code.gitea.io/gitea/models/devcontainer" "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/charset" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" @@ -33,7 +34,7 @@ type DevStarJSON struct { RunArgs []string } -func CreateDevcontainerJSON(ctx *gitea_context.Context) { +func CreateDevcontainerJSON(ctx *gitea_context.Context, repo *repo.Repository, doer *user.User) { jsonString := `{ "image":"mcr.microsoft.com/devcontainers/base:dev-ubuntu-20.04", "forwardPorts": [ @@ -55,7 +56,7 @@ func CreateDevcontainerJSON(ctx *gitea_context.Context) { "8888:8888" ] }` - _, err := files_service.ChangeRepoFiles(db.DefaultContext, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{ + _, err := files_service.ChangeRepoFiles(db.DefaultContext, repo, doer, &files_service.ChangeRepoFilesOptions{ Files: []*files_service.ChangeRepoFile{ { Operation: "create", @@ -210,8 +211,16 @@ func GetFileContentByPath(ctx context.Context, repo *repo.Repository, path strin func GetDevcontainerJsonString(ctx context.Context, repo *repo.Repository) (string, error) { return GetFileContentByPath(ctx, repo, ".devcontainer/devcontainer.json") } - func GetDockerfileContent(ctx context.Context, repo *repo.Repository) (string, error) { + dockerfilePath, err := GetDockerfilePath(ctx, repo) + if err != nil { + return "", err + } + + return GetFileContentByPath(ctx, repo, ".devcontainer/"+dockerfilePath) +} + +func GetDockerfilePath(ctx context.Context, repo *repo.Repository) (string, error) { devcontainerJSONContent, err := GetDevcontainerJsonString(ctx, repo) var devContainerJson *devcontainer_model.DevContainerJSON if err != nil { @@ -230,10 +239,11 @@ func GetDockerfileContent(ctx context.Context, repo *repo.Repository) (string, e log.Error("Failed to unmarshal .devcontainer/devcontainer.json: %v", err) return "", err } - if devContainerJson.Build.Dockerfile == "" { - return "", nil + if devContainerJson.Build == nil || devContainerJson.Build.Dockerfile == "" { + return "", fmt.Errorf("devcontainer.json error") } - return GetFileContentByPath(ctx, repo, ".devcontainer/"+devContainerJson.Build.Dockerfile) + log.Info("%vsdadasdsa", devContainerJson.Build.Dockerfile) + return devContainerJson.Build.Dockerfile, nil } // 移除 JSON 文件中的注释 diff --git a/services/devcontainer/devcontainer_type.go b/services/devcontainer/devcontainer_type.go index ace4709a22..dbb53c43b5 100644 --- a/services/devcontainer/devcontainer_type.go +++ b/services/devcontainer/devcontainer_type.go @@ -18,12 +18,11 @@ type RepoDevContainer struct { RepoId int64 `json:"repoId" xorm:"repo_id"` RepoName string `json:"repoName" xorm:"repo_name"` //RepoOwnerID int64 `json:"repo_owner_id" xorm:"repo_owner_id"` - RepoOwnerName string `json:"repo_owner_name" xorm:"repo_owner_name"` - RepoLink string `json:"repo_link" xorm:"repo_link"` - RepoDescription string `json:"repoDescription,omitempty" xorm:"repo_description"` - - // 实时查询获取,不再从数据库获取 - DevContainerPort uint16 `json:"devContainerPort,omitempty"` + RepoOwnerName string `json:"repo_owner_name" xorm:"repo_owner_name"` + RepoLink string `json:"repo_link" xorm:"repo_link"` + RepoDescription string `json:"repoDescription,omitempty" xorm:"repo_description"` + DevContainerPort uint16 `json:"devContainerPort,omitempty"` + DevContainerStatus uint16 `json:"devContainerStatus,omitempty" xorm:"devcontainer_status"` } // RepoDevcontainerOptions 仓库 Dev Container 条件,注意仓库的所有者可能与当前操作用户不一致! @@ -75,6 +74,7 @@ type CreateDevcontainerOptions struct { type OpenDevcontainerAppDispatcherOptions struct { Name string `json:"name"` Wait bool `json:"wait"` + Status uint16 Port uint16 UserPublicKey string RepoID int64 @@ -111,4 +111,5 @@ type CreateDevcontainerDTO struct { GitRepositoryURL string Image string DockerfileContent string + DevcontainerPort uint16 } diff --git a/services/devcontainer/docker_agent.go b/services/devcontainer/docker_agent.go index f7009e3bfa..b1fbb2c24c 100644 --- a/services/devcontainer/docker_agent.go +++ b/services/devcontainer/docker_agent.go @@ -173,18 +173,14 @@ func SaveDevcontainer(ctx *gitea_web_context.Context, opts *UpdateDevcontainerOp return fmt.Errorf("创建docker client失败 %v", err) } defer cli.Close() - if opts.SaveMethod == "Container" { - // 获取容器ID - containerID, err := docker_module.GetContainerID(cli, opts.DevContainerName) - if err != nil { - return fmt.Errorf("获取容器ID失败 %v", err) - } - // 提交容器 - _, err = cli.ContainerCommit(ctx, containerID, types.ContainerCommitOptions{Reference: imageRef}) - if err != nil { - return fmt.Errorf("提交容器失败 %v", err) - } - } else if opts.SaveMethod == "DockerFile" { + dbEngine := db.GetEngine(*ctx) + _, err = dbEngine.Table("devcontainer"). + Where("user_id = ? AND repo_id = ? ", opts.Actor.ID, opts.Repository.ID). + Update(&devcontainer_model.Devcontainer{DevcontainerStatus: 6}) + if err != nil { + log.Info("err %v", err) + } + if opts.SaveMethod == "on" { // 创建构建上下文(包含Dockerfile的tar包) var buf bytes.Buffer tw := tar.NewWriter(&buf) @@ -218,6 +214,18 @@ func SaveDevcontainer(ctx *gitea_web_context.Context, opts *UpdateDevcontainerOp log.Info(err.Error()) return err } + + } else { + // 获取容器ID + containerID, err := docker_module.GetContainerID(cli, opts.DevContainerName) + if err != nil { + return fmt.Errorf("获取容器ID失败 %v", err) + } + // 提交容器 + _, err = cli.ContainerCommit(ctx, containerID, types.ContainerCommitOptions{Reference: imageRef}) + if err != nil { + return fmt.Errorf("提交容器失败 %v", err) + } } // 推送到仓库 @@ -232,13 +240,26 @@ func SaveDevcontainer(ctx *gitea_web_context.Context, opts *UpdateDevcontainerOp // 使用正则表达式查找并替换 image 字段的值 newJSONStr := re.ReplaceAllString(devcontainerJson, `"image": "`+imageRef+`"`) + _, err = dbEngine.Table("devcontainer"). + Where("user_id = ? AND repo_id = ? ", opts.Actor.ID, opts.Repository.ID). + Update(&devcontainer_model.Devcontainer{DevcontainerStatus: 4}) + if err != nil { + log.Info("err %v", err) + } return UpdateDevcontainerJSON(ctx, newJSONStr) } func PullImageAsyncAndStartContainer(ctx *context.Context, cli *client.Client, dockerHost string, opts *docker_module.CreateDevcontainerOptions) error { var stdoutScanner, stderrScanner *bufio.Scanner // 创建扫描器来读取输出 + dbEngine := db.GetEngine(*ctx) + _, err := dbEngine.Table("devcontainer"). + Where("user_id = ? AND repo_id = ? ", opts.UserId, opts.RepoId). + Update(&devcontainer_model.Devcontainer{DevcontainerStatus: 1}) + if err != nil { + log.Info("err %v", err) + } if opts.DockerfileContent != "" { // 创建构建上下文(包含Dockerfile的tar包) @@ -295,7 +316,6 @@ func PullImageAsyncAndStartContainer(ctx *context.Context, cli *client.Client, d stderrScanner = bufio.NewScanner(stderr) } - dbEngine := db.GetEngine(*ctx) var pullImageOutput = devcontainer_models.DevcontainerOutput{ Output: "", ListId: 0, @@ -308,7 +328,8 @@ func PullImageAsyncAndStartContainer(ctx *context.Context, cli *client.Client, d log.Info("Failed to insert record: %v", err) return err } - _, err := dbEngine.Table("devcontainer_output"). + + _, err = dbEngine.Table("devcontainer_output"). Where("user_id = ? AND repo_id = ? ", opts.UserId, opts.RepoId). Get(&pullImageOutput) if err != nil { @@ -401,7 +422,13 @@ func PullImageAsyncAndStartContainer(ctx *context.Context, cli *client.Client, d Update(&devcontainer_models.DevcontainerOutput{Status: "success"}) // 创建并启动容器 - output, err := docker_module.CreateAndStartContainer(cli, opts) + _, err := dbEngine.Table("devcontainer"). + Where("user_id = ? AND repo_id = ? ", opts.UserId, opts.RepoId). + Update(&devcontainer_model.Devcontainer{DevcontainerStatus: 2}) + if err != nil { + log.Info("err %v", err) + } + output, err = docker_module.CreateAndStartContainer(cli, opts) if err != nil { log.Info("创建或启动容器失败: %v", err) } @@ -423,7 +450,12 @@ func PullImageAsyncAndStartContainer(ctx *context.Context, cli *client.Client, d log.Info("Error storing output for command %v: %v\n", opts.CommandList[2], err) } // 创建 exec 实例 - + _, err = dbEngine.Table("devcontainer"). + Where("user_id = ? AND repo_id = ? ", opts.UserId, opts.RepoId). + Update(&devcontainer_model.Devcontainer{DevcontainerStatus: 3}) + if err != nil { + log.Info("err %v", err) + } var buffer string = "" var state int = 2 for index, cmd := range opts.PostCreateCommand { @@ -465,6 +497,12 @@ func PullImageAsyncAndStartContainer(ctx *context.Context, cli *client.Client, d if err != nil { log.Info("Error storing output for command pull image: %v\n", err) } + _, err = dbEngine.Table("devcontainer"). + Where("user_id = ? AND repo_id = ? ", opts.UserId, opts.RepoId). + Update(&devcontainer_model.Devcontainer{DevcontainerStatus: 4}) + if err != nil { + log.Info("err %v", err) + } }() return nil @@ -482,6 +520,13 @@ func DockerRestartContainer(gitea_ctx *gitea_web_context.Context, opts *RepoDevC if err != nil { return fmt.Errorf("获取容器ID失败 %v", err) } + dbEngine := db.GetEngine(ctx) + _, err = dbEngine.Table("devcontainer"). + Where("user_id = ? AND repo_id = ? ", opts.UserId, opts.RepoId). + Update(&devcontainer_model.Devcontainer{DevcontainerStatus: 5}) + if err != nil { + log.Info("err %v", err) + } timeout := 10 // 超时时间(秒) err = cli.ContainerRestart(context.Background(), containerID, container.StopOptions{ Timeout: &timeout, @@ -499,7 +544,7 @@ func DockerRestartContainer(gitea_ctx *gitea_web_context.Context, opts *RepoDevC cmd := []string{"/home/devcontainer_restart.sh"} postCreateCommand := append(cmd, devContainerJson.PostCreateCommand...) // 创建 exec 实例 - dbEngine := db.GetEngine(ctx) + var buffer string = "" var state int = 2 _, err = dbEngine.Table("devcontainer_output"). @@ -572,6 +617,13 @@ func DockerRestartContainer(gitea_ctx *gitea_web_context.Context, opts *RepoDevC return err } } + _, err = dbEngine.Table("devcontainer"). + Where("user_id = ? AND repo_id = ? ", opts.UserId, opts.RepoId). + Update(&devcontainer_model.Devcontainer{DevcontainerStatus: 4}) + log.Info("DockerRestartContainerDockerRestartContainerDockerRestartContainer") + if err != nil { + log.Info("err %v", err) + } return nil } func DockerStopContainer(ctx *context.Context, opts *RepoDevContainer) error { @@ -586,6 +638,13 @@ func DockerStopContainer(ctx *context.Context, opts *RepoDevContainer) error { if err != nil { return fmt.Errorf("获取容器ID失败 %v", err) } + dbEngine := db.GetEngine(*ctx) + _, err = dbEngine.Table("devcontainer"). + Where("user_id = ? AND repo_id = ? ", opts.UserId, opts.RepoId). + Update(&devcontainer_model.Devcontainer{DevcontainerStatus: 6}) + if err != nil { + log.Info("err %v", err) + } timeout := 10 // 超时时间(秒) err = cli.ContainerStop(context.Background(), containerID, container.StopOptions{ Timeout: &timeout, diff --git a/templates/devstar-home-vscode-js.tmpl b/templates/devstar-home-vscode-js.tmpl new file mode 100644 index 0000000000..8e16720a9d --- /dev/null +++ b/templates/devstar-home-vscode-js.tmpl @@ -0,0 +1,1097 @@ + + \ No newline at end of file diff --git a/templates/devstar-home-vscode.tmpl b/templates/devstar-home-vscode.tmpl index dd07a8c743..6766d803c6 100644 --- a/templates/devstar-home-vscode.tmpl +++ b/templates/devstar-home-vscode.tmpl @@ -134,18 +134,18 @@
X
-

Login

+

- +
- +
- +
@@ -155,10 +155,10 @@