diff --git a/Makefile b/Makefile index a1c3c961e8..33bc5fdfc3 100644 --- a/Makefile +++ b/Makefile @@ -917,12 +917,31 @@ generate-manpage: ## generate manpage .PHONY: devstar devstar: + @if docker pull devstar.cn/devstar/devstar-dev-container:v1.0; then \ + docker tag devstar.cn/devstar/devstar-dev-container:v1.0 devstar.cn/devstar/devstar-dev-container:latest && \ + echo "Successfully pulled devstar.cn/devstar/devstar-dev-container:v1.0 taged to latest"; \ + else \ + docker build -t devstar.cn/devstar/devstar-dev-container:latest -f docker/Dockerfile.devContainer . && \ + echo "Successfully build devstar.cn/devstar/devstar-dev-container:latest"; \ + fi + @if docker pull devstar.cn/devstar/devstar-runtime-container:v1.0; then \ + docker tag devstar.cn/devstar/devstar-runtime-container:v1.0 devstar.cn/devstar/devstar-runtime-container:latest && \ + echo "Successfully pulled devstar.cn/devstar/devstar-runtime-container:v1.0 taged to latest"; \ + else \ + docker build -t devstar.cn/devstar/devstar-runtime-container:latest -f docker/Dockerfile.runtimeContainer . && \ + echo "Successfully build devstar.cn/devstar/devstar-runtime-container:latest"; \ + fi + @if docker pull devstar.cn/devstar/webterminal:v1.0; then \ + docker tag devstar.cn/devstar/webterminal:v1.0 devstar.cn/devstar/webterminal:latest && \ + echo "Successfully pulled devstar.cn/devstar/webterminal:v1.0 taged to latest"; \ + else \ + docker build --no-cache -t devstar.cn/devstar/webterminal:latest -f docker/Dockerfile.webTerminal . && \ + echo "Successfully build devstar.cn/devstar/devstar-runtime-container:latest"; \ + fi docker build -t devstar-studio:latest -f docker/Dockerfile.devstar . .PHONY: docker docker: - docker build -t devstar.cn/devstar/webterminal:latest -f docker/Dockerfile.webTerminal . - docker build --disable-content-trust=false -t $(DOCKER_REF) . # support also build args docker build --build-arg GITEA_VERSION=v1.2.3 --build-arg TAGS="bindata sqlite sqlite_unlock_notify" . diff --git a/docker/Dockerfile.devContainer b/docker/Dockerfile.devContainer index 44430e18c5..c24a946c34 100644 --- a/docker/Dockerfile.devContainer +++ b/docker/Dockerfile.devContainer @@ -12,6 +12,12 @@ RUN apk --no-cache add \ && rm -rf /var/cache/apk/* # To acquire Gitea dev container: -# $ docker build -t devstar.cn/devstar/devstar-dev-container:latest -f docker/Dockerfile.devContainer . +# $ docker build -t devstar.cn/devstar/devstar-dev-container:v1.0 -f docker/Dockerfile.devContainer . # $ docker login devstar.cn +# $ docker push devstar.cn/devstar/devstar-dev-container:v1.0 +# $ docker tag devstar.cn/devstar/devstar-dev-container:v1.0 devstar.cn/devstar/devstar-dev-container:latest # $ docker push devstar.cn/devstar/devstar-dev-container:latest + + +# Release Notes: +# v1.0 - Initial release diff --git a/docker/Dockerfile.runtimeContainer b/docker/Dockerfile.runtimeContainer index f04e0d0d30..6b01b85ce2 100644 --- a/docker/Dockerfile.runtimeContainer +++ b/docker/Dockerfile.runtimeContainer @@ -19,6 +19,12 @@ RUN apk --no-cache add \ && rm -rf /var/cache/apk/* # To acquire Gitea base runtime container: -# $ docker build -t devstar.cn/devstar/devstar-runtime-container:latest -f docker/Dockerfile.runtimeContainer . +# $ docker build -t devstar.cn/devstar/devstar-runtime-container:v1.0 -f docker/Dockerfile.runtimeContainer . # $ docker login devstar.cn +# $ docker push devstar.cn/devstar/devstar-runtime-container:v1.0 +# $ docker tag devstar.cn/devstar/devstar-runtime-container:v1.0 devstar.cn/devstar/devstar-runtime-container:latest # $ docker push devstar.cn/devstar/devstar-runtime-container:latest + + +# Release Notes: +# v1.0 - Initial release diff --git a/docker/Dockerfile.webTerminal b/docker/Dockerfile.webTerminal index 4fb65b05de..0abfd86c1e 100644 --- a/docker/Dockerfile.webTerminal +++ b/docker/Dockerfile.webTerminal @@ -37,4 +37,14 @@ RUN apt-get update && \ apt remove --purge curl -y && apt autoremove -y && apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* ENTRYPOINT ["/usr/bin/tini", "--"] -CMD ["/home/webTerminal/build/ttyd", "-W", "bash"] \ No newline at end of file +CMD ["/home/webTerminal/build/ttyd", "-W", "bash"] + +# To acquire devstar.cn/devstar/webterminal:latest: +# $ docker build --no-cache -t devstar.cn/devstar/webterminal:v1.0 -f docker/Dockerfile.webTerminal . +# $ docker login devstar.cn +# $ docker push devstar.cn/devstar/webterminal:v1.0 +# $ docker tag devstar.cn/devstar/webterminal:v1.0 devstar.cn/devstar/webterminal:latest +# $ docker push devstar.cn/devstar/webterminal:latest + +# Release Notes: +# v1.0 - Initial release https://devstar.cn/devstar/webTerminal/commit/2bf050cff984d6e64c4f9753d64e1124fc152ad7 \ No newline at end of file diff --git a/models/user/user.go b/models/user/user.go index 61abc6bb59..1c51348b28 100644 --- a/models/user/user.go +++ b/models/user/user.go @@ -667,6 +667,7 @@ func createUser(ctx context.Context, u *User, meta *Meta, createdByAdmin bool, o u.KeepEmailPrivate = setting.Service.DefaultKeepEmailPrivate u.Visibility = setting.Service.DefaultUserVisibilityMode u.AllowCreateOrganization = setting.Service.DefaultAllowCreateOrganization && !setting.Admin.DisableRegularOrgCreation + u.AllowCreateDevcontainer = setting.Service.DefaultAllowCreateDevcontainer u.EmailNotificationsPreference = setting.Admin.DefaultEmailNotification u.MaxRepoCreation = -1 u.Theme = setting.UI.DefaultTheme diff --git a/modules/docker/docker_api.go b/modules/docker/docker_api.go index fcaaeb5398..858b494d9e 100644 --- a/modules/docker/docker_api.go +++ b/modules/docker/docker_api.go @@ -89,24 +89,22 @@ func GetContainerStatus(cli *client.Client, containerID string) (string, error) if err != nil { return "", err } - state := containerInfo.State return state.Status, nil } func PushImage(dockerHost string, username string, password string, registryUrl string, imageRef string) error { script := "docker " + "-H " + dockerHost + " login -u " + username + " -p " + password + " " + registryUrl + " " cmd := exec.Command("sh", "-c", script) - _, err := cmd.CombinedOutput() + output, err := cmd.CombinedOutput() if err != nil { - return err + return fmt.Errorf("%s \n 镜像登录失败: %s", string(output), err.Error()) } // 推送到仓库 script = "docker " + "-H " + dockerHost + " push " + imageRef cmd = exec.Command("sh", "-c", script) - _, err = cmd.CombinedOutput() - + output, err = cmd.CombinedOutput() if err != nil { - return err + return fmt.Errorf("%s \n 镜像推送失败: %s", string(output), err.Error()) } return nil } diff --git a/modules/setting/service.go b/modules/setting/service.go index 5de92f1c67..e31b1ff9dd 100644 --- a/modules/setting/service.go +++ b/modules/setting/service.go @@ -71,6 +71,7 @@ var Service = struct { McaptchaURL string DefaultKeepEmailPrivate bool DefaultAllowCreateOrganization bool + DefaultAllowCreateDevcontainer bool DefaultUserIsRestricted bool EnableTimetracking bool DefaultEnableTimetracking bool @@ -205,6 +206,7 @@ func loadServiceFrom(rootCfg ConfigProvider) { Service.McaptchaSitekey = sec.Key("MCAPTCHA_SITEKEY").MustString("") Service.DefaultKeepEmailPrivate = sec.Key("DEFAULT_KEEP_EMAIL_PRIVATE").MustBool() Service.DefaultAllowCreateOrganization = sec.Key("DEFAULT_ALLOW_CREATE_ORGANIZATION").MustBool(true) + Service.DefaultAllowCreateDevcontainer = sec.Key("DEFAULT_ALLOW_CREATE_DEVCONTAINER").MustBool(true) Service.DefaultUserIsRestricted = sec.Key("DEFAULT_USER_IS_RESTRICTED").MustBool(false) Service.EnableTimetracking = sec.Key("ENABLE_TIMETRACKING").MustBool(true) if Service.EnableTimetracking { diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 91b4288d0e..43f403e8c8 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -362,7 +362,9 @@ invalid_log_root_path = The log path is invalid: %v default_keep_email_private = Hide Email Addresses by Default default_keep_email_private_popup = Hide email addresses of new user accounts by default. default_allow_create_organization = Allow Creation of Organizations by Default +default_allow_create_devcontainer = Allow Creation of DevContainers by Default default_allow_create_organization_popup = Allow new user accounts to create organizations by default. +default_allow_create_devcontainer_popup = Allow new user accounts to create devcontainers by default. default_enable_timetracking = Enable Time Tracking by Default default_enable_timetracking_popup = Enable time tracking for new repositories by default. no_reply_address = Hidden Email Domain @@ -3420,6 +3422,7 @@ config.active_code_lives = Active Code Lives config.reset_password_code_lives = Recover Account Code Expiry Time config.default_keep_email_private = Hide Email Addresses by Default config.default_allow_create_organization = Allow Creation of Organizations by Default +config.default_allow_create_devcontainer = Allow Creation of Dev Containers by Default config.enable_timetracking = Enable Time Tracking config.default_enable_timetracking = Enable Time Tracking by Default config.default_allow_only_contributors_to_track_time = Let Only Contributors Track Time diff --git a/options/locale/locale_zh-CN.ini b/options/locale/locale_zh-CN.ini index 33ac22baf7..0be352faad 100644 --- a/options/locale/locale_zh-CN.ini +++ b/options/locale/locale_zh-CN.ini @@ -357,7 +357,9 @@ invalid_log_root_path=日志路径无效: %v default_keep_email_private=默认情况下隐藏邮箱地址 default_keep_email_private_popup=默认情况下,隐藏新用户帐户的邮箱地址。 default_allow_create_organization=默认情况下允许创建组织 +default_allow_create_devcontainer=默认情况下允许创建容器 default_allow_create_organization_popup=默认情况下, 允许新用户帐户创建组织。 +default_allow_create_devcontainer_popup=默认情况下, 允许新用户帐户创建容器。 default_enable_timetracking=默认情况下启用时间跟踪 default_enable_timetracking_popup=默认情况下启用新仓库的时间跟踪。 no_reply_address=隐藏邮件域 @@ -3408,6 +3410,7 @@ config.active_code_lives=激活用户链接有效期 config.reset_password_code_lives=恢复账户验证码过期时间 config.default_keep_email_private=默认隐藏邮箱地址 config.default_allow_create_organization=默认情况下允许创建组织 +config.default_allow_create_devcontainer=默认情况下允许创建 DevContainer config.enable_timetracking=启用时间跟踪 config.default_enable_timetracking=默认情况下启用时间跟踪 config.default_allow_only_contributors_to_track_time=仅允许成员跟踪时间 diff --git a/public/assets/install.sh b/public/assets/install.sh index 9ab3e480fd..ae0fe411e9 100755 --- a/public/assets/install.sh +++ b/public/assets/install.sh @@ -86,12 +86,12 @@ function install { sudo docker pull devstar.cn/devstar/$IMAGE_NAME:$VERSION IMAGE_REGISTRY_USER=devstar.cn/devstar fi - if sudo docker pull devstar.cn/devstar/webterminal:latest; then - success "Successfully pulled devstar.cn/devstar/webterminal:latest" - else - sudo docker pull mengning997/webterminal:latest - success "Successfully pulled mengning997/webterminal:latest renamed to devstar.cn/devstar/webterminal:latest" + if sudo docker pull mengning997/webterminal:latest; then sudo docker tag mengning997/webterminal:latest devstar.cn/devstar/webterminal:latest + success "Successfully pulled mengning997/webterminal:latest renamed to devstar.cn/devstar/webterminal:latest" + else + sudo docker pull devstar.cn/devstar/webterminal:latest + success "Successfully pulled devstar.cn/devstar/webterminal:latest" fi } @@ -137,7 +137,10 @@ function stop { fi if [ $(docker ps -a --filter "name=^/devstar-studio$" -q | wc -l) -gt 0 ]; then sudo docker stop devstar-studio && sudo docker rm -f devstar-studio - fi + fi + if [ $(docker ps -a --filter "name=^/webterminal-" -q | wc -l) -gt 0 ]; then + sudo docker stop $(docker ps -a --filter "name=^/webterminal-" -q) && sudo docker rm -f $(docker ps -a --filter "name=^/webterminal-" -q) + fi } # Function to logs diff --git a/routers/install/install.go b/routers/install/install.go index f0c4c843c8..579238b1af 100644 --- a/routers/install/install.go +++ b/routers/install/install.go @@ -155,6 +155,7 @@ func Install(ctx *context.Context) { form.RequireSignInView = setting.Service.RequireSignInViewStrict form.DefaultKeepEmailPrivate = setting.Service.DefaultKeepEmailPrivate form.DefaultAllowCreateOrganization = setting.Service.DefaultAllowCreateOrganization + form.DefaultAllowCreateDevcontainer = setting.Service.DefaultAllowCreateDevcontainer form.DefaultEnableTimetracking = setting.Service.DefaultEnableTimetracking form.NoReplyAddress = setting.Service.NoReplyAddress form.PasswordAlgorithm = hash.ConfigHashAlgorithm(setting.PasswordHashAlgo) @@ -490,6 +491,7 @@ func SubmitInstall(ctx *context.Context) { cfg.Section("service").Key("REQUIRE_SIGNIN_VIEW").SetValue(strconv.FormatBool(form.RequireSignInView)) cfg.Section("service").Key("DEFAULT_KEEP_EMAIL_PRIVATE").SetValue(strconv.FormatBool(form.DefaultKeepEmailPrivate)) cfg.Section("service").Key("DEFAULT_ALLOW_CREATE_ORGANIZATION").SetValue(strconv.FormatBool(form.DefaultAllowCreateOrganization)) + cfg.Section("service").Key("DEFAULT_ALLOW_CREATE_DEVCONTAINER").SetValue(strconv.FormatBool(form.DefaultAllowCreateDevcontainer)) cfg.Section("service").Key("DEFAULT_ENABLE_TIMETRACKING").SetValue(strconv.FormatBool(form.DefaultEnableTimetracking)) cfg.Section("service").Key("NO_REPLY_ADDRESS").SetValue(form.NoReplyAddress) cfg.Section("cron.update_checker").Key("ENABLED").SetValue(strconv.FormatBool(form.EnableUpdateChecker)) diff --git a/routers/web/devcontainer/devcontainer.go b/routers/web/devcontainer/devcontainer.go index 3592744368..fd0bdc6258 100644 --- a/routers/web/devcontainer/devcontainer.go +++ b/routers/web/devcontainer/devcontainer.go @@ -55,21 +55,22 @@ func GetDevContainerDetails(ctx *context.Context) { ctx.Data["ValidateDevContainerConfiguration"] = false } - ctx.Data["HasDevContainerDockerfile"], err = devcontainer_service.HasDevContainerDockerFile(ctx, ctx.Repo) + ctx.Data["HasDevContainerDockerfile"], ctx.Data["DockerfilePath"], err = devcontainer_service.HasDevContainerDockerFile(ctx, ctx.Repo) if err != nil { log.Info(err.Error()) ctx.Flash.Error(err.Error(), true) } if ctx.Data["HasDevContainer"] == true { - configurationString, _ := devcontainer_service.GetDevcontainerConfigurationString(ctx, ctx.Repo.Repository) - configurationModel, _ := devcontainer_service.UnmarshalDevcontainerConfigContent(configurationString) - imageName := configurationModel.Image - registry, namespace, repo, tag := devcontainer_service.ParseImageName(imageName) - log.Info("%v %v", repo, tag) - ctx.Data["RepositoryAddress"] = registry - ctx.Data["RepositoryUsername"] = namespace - ctx.Data["ImageName"] = "dev-" + ctx.Repo.Repository.Name + ":latest" - + if ctx.Data["HasDevContainerConfiguration"] == true { + configurationString, _ := devcontainer_service.GetDevcontainerConfigurationString(ctx, ctx.Repo.Repository) + configurationModel, _ := devcontainer_service.UnmarshalDevcontainerConfigContent(configurationString) + imageName := configurationModel.Image + registry, namespace, repo, tag := devcontainer_service.ParseImageName(imageName) + log.Info("%v %v", repo, tag) + ctx.Data["RepositoryAddress"] = registry + ctx.Data["RepositoryUsername"] = namespace + ctx.Data["ImageName"] = "dev-" + ctx.Repo.Repository.Name + ":latest" + } if cfg.Section("k8s").Key("ENABLE").Value() == "true" { // 获取WebSSH服务端口 webTerminalURL, err := devcontainer_service.GetWebTerminalURL(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID) @@ -111,7 +112,6 @@ func GetDevContainerDetails(ctx *context.Context) { } ctx.Redirect(path.Join(ctx.Repo.RepoLink, "/devcontainer")) } else { - rootPort, err := devcontainer_service.GetPortFromURL(cfg.Section("server").Key("ROOT_URL").Value()) if err != nil { ctx.Flash.Error(err.Error(), true) @@ -136,7 +136,6 @@ func GetDevContainerDetails(ctx *context.Context) { } ctx.Data["WebSSHUrl"] = webTerminalURL + "?type=docker&" + terminalParams } - } terminalURL, err := devcontainer_service.Get_IDE_TerminalURL(ctx, ctx.Doer, ctx.Repo) if err == nil { @@ -145,7 +144,6 @@ func GetDevContainerDetails(ctx *context.Context) { ctx.Data["WindsurfUrl"] = "windsurf" + terminalURL } } - // 3. 携带数据渲染页面,返回 ctx.Data["Title"] = ctx.Locale.Tr("repo.dev_container") ctx.Data["PageIsDevContainer"] = true @@ -300,7 +298,7 @@ func UpdateDevContainer(ctx *context.Context) { ctx.JSON(http.StatusOK, map[string]string{"message": err.Error()}) return } - err = devcontainer_service.UpdateDevContainer(ctx, ctx.Doer, ctx.Repo.Repository, &updateInfo) + err = devcontainer_service.UpdateDevContainer(ctx, ctx.Doer, ctx.Repo, &updateInfo) if err != nil { ctx.JSON(http.StatusOK, map[string]string{"message": err.Error()}) return @@ -318,18 +316,43 @@ func GetTerminalCommand(ctx *context.Context) { log.Info(err.Error()) status = "error" } - - ctx.JSON(http.StatusOK, map[string]string{"command": cmd, "status": status}) + ctx.JSON(http.StatusOK, map[string]string{"command": cmd, "status": status, "workdir": "/workspace/" + ctx.Repo.Repository.Name}) } - func GetDevContainerOutput(ctx *context.Context) { // 设置 CORS 响应头 ctx.Resp.Header().Set("Access-Control-Allow-Origin", "*") ctx.Resp.Header().Set("Access-Control-Allow-Methods", "*") ctx.Resp.Header().Set("Access-Control-Allow-Headers", "*") - output, err := devcontainer_service.GetDevContainerOutput(ctx, ctx.Doer, ctx.Repo.Repository) + query := ctx.Req.URL.Query() + output, err := devcontainer_service.GetDevContainerOutput(ctx, query.Get("user"), ctx.Repo.Repository) if err != nil { log.Info(err.Error()) } - ctx.JSON(http.StatusOK, output) + ctx.JSON(http.StatusOK, map[string]string{"output": output}) +} +func SaveDevContainerOutput(ctx *context.Context) { + // 设置 CORS 响应头 + ctx.Resp.Header().Set("Access-Control-Allow-Origin", "*") + ctx.Resp.Header().Set("Access-Control-Allow-Methods", "*") + ctx.Resp.Header().Set("Access-Control-Allow-Headers", "*") + // 处理 OPTIONS 预检请求 + if ctx.Req.Method == "OPTIONS" { + ctx.JSON(http.StatusOK, "") + return + } + + query := ctx.Req.URL.Query() + + // 从请求体中读取输出内容 + body, err := io.ReadAll(ctx.Req.Body) + if err != nil { + log.Error("Failed to read request body: %v", err) + ctx.JSON(http.StatusBadRequest, map[string]string{"error": "Failed to read request body"}) + return + } + err = devcontainer_service.SaveDevContainerOutput(ctx, query.Get("user"), ctx.Repo.Repository, string(body)) + if err != nil { + log.Info(err.Error()) + } + ctx.JSON(http.StatusOK, "") } diff --git a/routers/web/repo/editor.go b/routers/web/repo/editor.go index 2a5ac10282..b63b990116 100644 --- a/routers/web/repo/editor.go +++ b/routers/web/repo/editor.go @@ -11,6 +11,7 @@ import ( "path" "strings" + "code.gitea.io/gitea/models/db" git_model "code.gitea.io/gitea/models/git" "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/unit" @@ -25,6 +26,7 @@ import ( "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/context/upload" + devcontainer_service "code.gitea.io/gitea/services/devcontainer" "code.gitea.io/gitea/services/forms" files_service "code.gitea.io/gitea/services/repository/files" ) @@ -411,6 +413,23 @@ func DeleteFilePost(ctx *context.Context) { editorHandleFileOperationError(ctx, parsed.NewBranchName, err) return } + log.Info("File deleted: %s", treePath) + if treePath == `.devcontainer/devcontainer.json` { + var userIds []int64 + err = db.GetEngine(ctx). + Table("devcontainer"). + Select("user_id"). + Where("repo_id = ?", ctx.Repo.Repository.ID). + Find(&userIds) + if err != nil { + ctx.ServerError("GetEngine", err) + return + } + for _, userId := range userIds { + devcontainer_service.DeleteDevContainer(ctx, userId, ctx.Repo.Repository.ID) + } + + } ctx.Flash.Success(ctx.Tr("repo.editor.file_delete_success", treePath)) redirectTreePath := getClosestParentWithFiles(ctx.Repo.GitRepo, parsed.NewBranchName, treePath) diff --git a/routers/web/web.go b/routers/web/web.go index 3aeea10357..bda490c935 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -1434,13 +1434,14 @@ func registerWebRoutes(m *web.Router) { m.Get("/status", devcontainer_web.GetDevContainerStatus) m.Get("/command", devcontainer_web.GetTerminalCommand) m.Get("/output", devcontainer_web.GetDevContainerOutput) + m.Methods("POST, OPTIONS", "/output", devcontainer_web.SaveDevContainerOutput) }, // 解析仓库信息 // 具有code读取权限 context.RepoAssignment, reqUnitCodeReader, ) m.Get("/devstar-home", devcontainer_web.VscodeHome) // 旧地址,保留兼容性 - m.Get("/vscode-home", devcontainer_web.VscodeHome) + m.Get("/vscode-home", devcontainer_web.VscodeHome) m.Group("/api/devcontainer", func() { // 获取 某用户在某仓库中的 DevContainer 细节(包括SSH连接信息),默认不会等待 (wait = false) // 请求方式: GET /api/devcontainer?repoId=${repoId}&wait=true // 无需传入 userId,直接从 token 中提取 diff --git a/services/devcontainer/devcontainer.go b/services/devcontainer/devcontainer.go index 09c16725e6..8f521e1789 100644 --- a/services/devcontainer/devcontainer.go +++ b/services/devcontainer/devcontainer.go @@ -6,8 +6,6 @@ import ( "context" "fmt" "math" - "net" - "net/url" "regexp" "strconv" "strings" @@ -70,21 +68,21 @@ func HasDevContainerConfiguration(ctx context.Context, repo *gitea_context.Repos return true, nil } } -func HasDevContainerDockerFile(ctx context.Context, repo *gitea_context.Repository) (bool, error) { +func HasDevContainerDockerFile(ctx context.Context, repo *gitea_context.Repository) (bool, string, error) { _, err := FileExists(".devcontainer/devcontainer.json", repo) if err != nil { if git.IsErrNotExist(err) { - return false, nil + return false, "", nil } - return false, err + return false, "", err } configurationString, err := GetDevcontainerConfigurationString(ctx, repo.Repository) if err != nil { - return false, err + return false, "", err } configurationModel, err := UnmarshalDevcontainerConfigContent(configurationString) if err != nil { - return false, err + return false, "", err } // 执行验证 if errs := configurationModel.Validate(); len(errs) > 0 { @@ -92,20 +90,34 @@ func HasDevContainerDockerFile(ctx context.Context, repo *gitea_context.Reposito for _, err := range errs { fmt.Printf(" - %s\n", err.Error()) } - return false, fmt.Errorf("配置格式错误") + return false, "", fmt.Errorf("配置格式错误") } else { log.Info("%v", configurationModel) if configurationModel.Build == nil || configurationModel.Build.Dockerfile == "" { - return false, nil + _, err := FileExists(".devcontainer/Dockerfile", repo) + if err != nil { + if git.IsErrNotExist(err) { + return false, "", nil + } + return false, "", err + } + return true, ".devcontainer/Dockerfile", nil } _, err := FileExists(".devcontainer/"+configurationModel.Build.Dockerfile, repo) if err != nil { if git.IsErrNotExist(err) { - return false, nil + _, err := FileExists(".devcontainer/Dockerfile", repo) + if err != nil { + if git.IsErrNotExist(err) { + return false, "", nil + } + return false, "", err + } + return true, ".devcontainer/Dockerfile", nil } - return false, err + return false, "", err } - return true, nil + return true, ".devcontainer/" + configurationModel.Build.Dockerfile, nil } } func CreateDevcontainerConfiguration(repo *repo.Repository, doer *user.User) error { @@ -435,7 +447,7 @@ func StopDevContainer(ctx context.Context, userID, repoID int64) error { return nil } -func UpdateDevContainer(ctx context.Context, doer *user.User, repo *repo.Repository, updateInfo *UpdateInfo) error { +func UpdateDevContainer(ctx context.Context, doer *user.User, repo *gitea_context.Repository, updateInfo *UpdateInfo) error { dbEngine := db.GetEngine(ctx) var devContainerInfo devcontainer_models.Devcontainer cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf) @@ -445,25 +457,24 @@ func UpdateDevContainer(ctx context.Context, doer *user.User, repo *repo.Reposit _, err = dbEngine. Table("devcontainer"). Select("*"). - Where("user_id = ? AND repo_id = ?", doer.ID, repo.ID). + Where("user_id = ? AND repo_id = ?", doer.ID, repo.Repository.ID). Get(&devContainerInfo) if err != nil { return err } _, err = dbEngine.Table("devcontainer"). - Where("user_id = ? AND repo_id = ? ", doer.ID, repo.ID). + Where("user_id = ? AND repo_id = ? ", doer.ID, repo.Repository.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). + Where("user_id = ? AND repo_id = ? ", doer.ID, repo.Repository.ID). Update(&devcontainer_models.Devcontainer{DevcontainerStatus: 4}) if err != nil { return err @@ -534,58 +545,72 @@ func GetTerminalCommand(ctx context.Context, userID string, repo *repo.Repositor return "", "", err } } - } break case 2: //正在创建容器,创建容器成功,则状态转移 if cfg.Section("k8s").Key("ENABLE").Value() == "true" { //k8s的逻辑 + } else { - status, err := GetDevContainerStatusFromDocker(ctx, devContainerInfo.Name) + exist, _, err := ContainerExists(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 - } + if !exist { + _, 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 } - realTimeStatus = 3 + } 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: @@ -614,6 +639,27 @@ func GetTerminalCommand(ctx context.Context, userID string, repo *repo.Repositor if err != nil { return "", "", err } + configurationString, err := GetDevcontainerConfigurationString(ctx, repo) + if err != nil { + return "", "", err + } + configurationModel, err := UnmarshalDevcontainerConfigContent(configurationString) + if err != nil { + return "", "", err + } + postAttachCommand := strings.TrimSpace(strings.Join(configurationModel.ParseCommand(configurationModel.PostAttachCommand), "\n")) + if _, ok := configurationModel.PostAttachCommand.(map[string]interface{}); ok { + // 是 map[string]interface{} 类型 + cmdObj := configurationModel.PostAttachCommand.(map[string]interface{}) + if pathValue, hasPath := cmdObj["path"]; hasPath { + fileCommand, err := GetFileContentByPath(ctx, repo, ".devcontainer/"+pathValue.(string)) + if err != nil { + return "", "", err + } + postAttachCommand += "\n" + fileCommand + } + } + cmd += postAttachCommand } break } @@ -636,67 +682,59 @@ func GetTerminalCommand(ctx context.Context, userID string, repo *repo.Repositor } 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 +func GetDevContainerOutput(ctx context.Context, user_id string, repo *repo.Repository) (string, error) { + var devContainerOutput string 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) + _, err := dbEngine.Table("devcontainer_output"). + Select("output"). + Where("user_id = ? AND repo_id = ? AND list_id = ?", user_id, repo.ID, 4). + Get(&devContainerOutput) if err != nil { - return resp, err + return "", 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, + if devContainerOutput != "" { + _, err = dbEngine.Table("devcontainer_output"). + Where("user_id = ? AND repo_id = ? AND list_id = ?", user_id, repo.ID, 4). + Update(map[string]interface{}{ + "output": "", }) - resp.CurrentJob.Steps = append(resp.CurrentJob.Steps, &ViewJobStep{ - Summary: item.Command, - Status: item.Status, - Logs: logLines, - }) - + if err != nil { + return "", err } } - return resp, nil + + return devContainerOutput, nil +} +func SaveDevContainerOutput(ctx context.Context, user_id string, repo *repo.Repository, newoutput string) error { + var devContainerOutput string + var finalOutput string + dbEngine := db.GetEngine(ctx) + + // 从数据库中获取现有的输出内容 + _, err := dbEngine.Table("devcontainer_output"). + Select("output"). + Where("user_id = ? AND repo_id = ? AND list_id = ?", user_id, repo.ID, 4). + Get(&devContainerOutput) + if err != nil { + return err + } + devContainerOutput = strings.TrimSuffix(devContainerOutput, "\r\n") + if newoutput == "\b \b" { + finalOutput = devContainerOutput[:len(devContainerOutput)-1] + } else { + finalOutput = devContainerOutput + newoutput + } + _, err = dbEngine.Table("devcontainer_output"). + Where("user_id = ? AND repo_id = ? AND list_id = ?", user_id, repo.ID, 4). + Update(map[string]interface{}{ + "output": finalOutput + "\r\n", + }) + if err != nil { + return err + } + return nil } func GetMappedPort(ctx context.Context, containerName string, port string) (uint16, error) { cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf) @@ -937,7 +975,6 @@ func GetCommandContent(ctx context.Context, userId int64, repo *repo.Repository) 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") @@ -989,6 +1026,7 @@ func AddPublicKeyToAllRunningDevContainer(ctx context.Context, userId int64, pub if err != nil { return err } + if len(devcontainerList) > 0 { // 将公钥写入这些打开的容器中 for _, repoDevContainer := range devcontainerList { diff --git a/services/devcontainer/docker_agent.go b/services/devcontainer/docker_agent.go index 77602fb64a..794d4c88f2 100644 --- a/services/devcontainer/docker_agent.go +++ b/services/devcontainer/docker_agent.go @@ -16,10 +16,13 @@ import ( "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" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/filters" "github.com/docker/docker/client" "github.com/docker/docker/errdefs" "github.com/docker/go-connections/nat" @@ -129,6 +132,7 @@ func CreateDevContainerByDockerCommand(ctx context.Context, newDevcontainer *dev if err != nil { return "", err } + var imageName = configurationModel.Image dockerSocket, err := docker_module.GetDockerSocketPath() if err != nil { @@ -213,7 +217,8 @@ func CreateDevContainerByDockerCommand(ctx context.Context, newDevcontainer *dev var envFlags string = ` -e RepoLink="` + strings.TrimSuffix(cfg.Section("server").Key("ROOT_URL").Value(), `/`) + repo.Link() + `" ` + ` -e DevstarHost="` + newDevcontainer.DevcontainerHost + `"` + ` -e WorkSpace="` + newDevcontainer.DevcontainerWorkDir + `/` + repo.Name + `" ` + - ` -e DEVCONTAINER_STATUS="start" ` + ` -e DEVCONTAINER_STATUS="start" ` + + ` -e WEB_TERMINAL_HELLO="Successfully connected to the devcontainer" ` // 遍历 ContainerEnv 映射中的每个环境变量 for name, value := range configurationModel.ContainerEnv { // 将每个环境变量转换为 "-e name=value" 格式 @@ -283,7 +288,7 @@ func CreateDevContainerByDockerCommand(ctx context.Context, newDevcontainer *dev Status: "waitting", UserId: newDevcontainer.UserId, RepoId: newDevcontainer.RepoId, - Command: `docker -H ` + dockerSocket + ` exec -it --workdir ` + newDevcontainer.DevcontainerWorkDir + "/" + repo.Name + ` ` + newDevcontainer.Name + ` sh -c "echo 'Successfully connected to the container';bash"` + "\n", + Command: `docker -H ` + dockerSocket + ` exec -it --workdir ` + newDevcontainer.DevcontainerWorkDir + "/" + repo.Name + ` ` + newDevcontainer.Name + ` sh -c 'echo "$WEB_TERMINAL_HELLO";bash'` + "\n", ListId: 4, DevcontainerId: newDevcontainer.Id, }); err != nil { @@ -391,17 +396,16 @@ func StopDevContainerByDocker(ctx context.Context, devContainerName string) erro } return nil } -func UpdateDevContainerByDocker(ctx context.Context, devContainerInfo *devcontainer_models.Devcontainer, updateInfo *UpdateInfo, repo *repo.Repository, doer *user.User) error { +func UpdateDevContainerByDocker(ctx context.Context, devContainerInfo *devcontainer_models.Devcontainer, updateInfo *UpdateInfo, repo *gitea_context.Repository, doer *user.User) error { // 创建docker client cli, err := docker_module.CreateDockerClient(ctx) if err != nil { return err } defer cli.Close() - // update容器 imageRef := updateInfo.RepositoryAddress + "/" + updateInfo.RepositoryUsername + "/" + updateInfo.ImageName - configurationString, err := GetDevcontainerConfigurationString(ctx, repo) + configurationString, err := GetDevcontainerConfigurationString(ctx, repo.Repository) if err != nil { return err } @@ -411,16 +415,45 @@ func UpdateDevContainerByDocker(ctx context.Context, devContainerInfo *devcontai } if updateInfo.SaveMethod == "on" { + // 创建构建上下文(包含Dockerfile的tar包) var buf bytes.Buffer tw := tar.NewWriter(&buf) defer tw.Close() // 添加Dockerfile到tar包 + var dockerfileContent string dockerfile := "Dockerfile" - dockerfileContent, err := GetFileContentByPath(ctx, repo, ".devcontainer/"+configurationModel.Build.Dockerfile) - if err != nil { - return err + if configurationModel.Build == nil || configurationModel.Build.Dockerfile == "" { + _, err := FileExists(".devcontainer/Dockerfile", repo) + if err != nil { + return err + } + dockerfileContent, err = GetFileContentByPath(ctx, repo.Repository, ".devcontainer/Dockerfile") + if err != nil { + return err + } + } else { + _, err := FileExists(".devcontainer/"+configurationModel.Build.Dockerfile, repo) + if err != nil { + if git.IsErrNotExist(err) { + _, err := FileExists(".devcontainer/Dockerfile", repo) + if err != nil { + return err + } + dockerfileContent, err = GetFileContentByPath(ctx, repo.Repository, ".devcontainer/Dockerfile") + if err != nil { + return err + } + } + return err + } else { + dockerfileContent, err = GetFileContentByPath(ctx, repo.Repository, ".devcontainer/"+configurationModel.Build.Dockerfile) + if err != nil { + return err + } + } } + content := []byte(dockerfileContent) header := &tar.Header{ Name: dockerfile, @@ -468,11 +501,12 @@ func UpdateDevContainerByDocker(ctx context.Context, devContainerInfo *devcontai if err != nil { return err } + // 定义正则表达式来匹配 image 字段 re := regexp.MustCompile(`"image"\s*:\s*"([^"]+)"`) // 使用正则表达式查找并替换 image 字段的值 newConfiguration := re.ReplaceAllString(configurationString, `"image": "`+imageRef+`"`) - err = UpdateDevcontainerConfiguration(newConfiguration, repo, doer) + err = UpdateDevcontainerConfiguration(newConfiguration, repo.Repository, doer) if err != nil { return err } @@ -484,7 +518,6 @@ func UpdateDevContainerByDocker(ctx context.Context, devContainerInfo *devcontai // - bool: 镜像是否存在(true=存在,false=不存在) // - error: 非空表示检查过程中发生错误 func ImageExists(ctx context.Context, imageName string) (bool, error) { - // 创建 Docker 客户端 cli, err := docker_module.CreateDockerClient(ctx) if err != nil { @@ -519,7 +552,6 @@ func CheckDirExistsFromDocker(ctx context.Context, containerName, dirPath string AttachStdout: true, AttachStderr: true, } - // 创建 exec 实例 execResp, err := cli.ContainerExecCreate(context.Background(), containerID, execConfig) if err != nil { @@ -542,6 +574,7 @@ func CheckDirExistsFromDocker(ctx context.Context, containerName, dirPath string exitCode = resp.ExitCode return exitCode == 0, nil // 退出码为 0 表示目录存在 } + func CheckFileExistsFromDocker(ctx context.Context, containerName, filePath string) (bool, error) { // 上下文 // 创建 Docker 客户端 @@ -598,7 +631,7 @@ func RegistWebTerminal(ctx context.Context) error { // 拉取镜像 err = docker_module.PullImage(ctx, cli, dockerHost, setting.DevContainerConfig.Web_Terminal_Image) if err != nil { - return fmt.Errorf("拉取web_terminal镜像失败:%v", err) + fmt.Errorf("拉取web_terminal镜像失败:%v", err) } timestamp := time.Now().Format("20060102150405") @@ -632,3 +665,36 @@ func RegistWebTerminal(ctx context.Context) error { } return nil } + +// ContainerExists 检查容器是否存在,返回存在状态和容器ID(如果存在) +func ContainerExists(ctx context.Context, containerName string) (bool, string, error) { + cli, err := docker_module.CreateDockerClient(ctx) + if err != nil { + return false, "", err + } + // 设置过滤器,根据容器名称过滤 + filter := filters.NewArgs() + filter.Add("name", containerName) + + // 获取容器列表,使用过滤器 + containers, err := cli.ContainerList(context.Background(), types.ContainerListOptions{ + All: true, // 包括所有容器(运行的和停止的) + Filters: filter, + }) + if err != nil { + return false, "", err + } + + // 遍历容器,检查名称是否完全匹配 + for _, container := range containers { + for _, name := range container.Names { + // 容器名称在Docker API中是以斜杠开头的,例如 "/my-container" + // 所以我们需要检查去掉斜杠后的名称是否匹配 + if strings.TrimPrefix(name, "/") == containerName { + return true, container.ID, nil + } + } + } + + return false, "", nil +} diff --git a/services/forms/user_form.go b/services/forms/user_form.go index e72df81144..784a1b6a40 100644 --- a/services/forms/user_form.go +++ b/services/forms/user_form.go @@ -61,6 +61,7 @@ type InstallForm struct { RequireSignInView bool DefaultKeepEmailPrivate bool DefaultAllowCreateOrganization bool + DefaultAllowCreateDevcontainer bool DefaultEnableTimetracking bool EnableUpdateChecker bool NoReplyAddress string diff --git a/templates/admin/config.tmpl b/templates/admin/config.tmpl index 806347c720..e18268f205 100644 --- a/templates/admin/config.tmpl +++ b/templates/admin/config.tmpl @@ -153,6 +153,8 @@
{{svg (Iif .Service.DefaultKeepEmailPrivate "octicon-check" "octicon-x")}}
{{ctx.Locale.Tr "admin.config.default_allow_create_organization"}}
{{svg (Iif .Service.DefaultAllowCreateOrganization "octicon-check" "octicon-x")}}
+
{{ctx.Locale.Tr "admin.config.default_allow_create_devcontainer"}}
+
{{svg (Iif .Service.DefaultAllowCreateDevcontainer "octicon-check" "octicon-x")}}
{{ctx.Locale.Tr "admin.config.enable_timetracking"}}
{{svg (Iif .Service.EnableTimetracking "octicon-check" "octicon-x")}}
{{if .Service.EnableTimetracking}} diff --git a/templates/install.tmpl b/templates/install.tmpl index 2670eb958a..76a5ffdc82 100644 --- a/templates/install.tmpl +++ b/templates/install.tmpl @@ -304,6 +304,12 @@ +
+
+ + +
+
diff --git a/templates/repo/devcontainer/default_devcontainer.json b/templates/repo/devcontainer/default_devcontainer.json index 689762930d..1108b41cfa 100644 --- a/templates/repo/devcontainer/default_devcontainer.json +++ b/templates/repo/devcontainer/default_devcontainer.json @@ -14,6 +14,10 @@ "echo \"postCreateCommand\"", "echo \"OK\"" ], + "postAttachCommand": [ + "echo \"postAttachCommand\"", + "echo \"OK\"" + ], "runArgs": [ "-p 8888" ] diff --git a/templates/repo/devcontainer/details.tmpl b/templates/repo/devcontainer/details.tmpl index f893ebd664..874d97e558 100644 --- a/templates/repo/devcontainer/details.tmpl +++ b/templates/repo/devcontainer/details.tmpl @@ -22,6 +22,7 @@ {{else}}
+
+ {{if and .ValidateDevContainerConfiguration .HasDevContainer}} + {{end}}
{{end}}
@@ -47,7 +50,7 @@ {{ctx.Locale.Tr "repo.dev_container_control"}}
- {{if .HasDevContainer}} + {{if and .ValidateDevContainerConfiguration .HasDevContainer}} {{if .isAdmin}} @@ -66,7 +69,7 @@
- +
@@ -84,6 +87,16 @@
+ +
+
+
+ 提示信息 + +
+
+
+
{{template "base/modal_actions_confirm" .}}
- +