!110 完善了devcontainer相关功能细节
All checks were successful
DevStar Studio Auto Test Pipeline / unit-frontend-test (push) Successful in 34m23s
DevStar Studio Auto Test Pipeline / unit-backend-test (push) Successful in 19m32s
DevStar Studio CI/CD Pipeline / build-and-push-x86-64-docker-image (push) Successful in 18m42s

完善了devcontainer相关功能细节 见https://gitee.com/devstar/devstar/issues/ID2H25
This commit is contained in:
2025-10-31 07:44:19 +00:00
committed by 孟宁
parent 130bb879f2
commit 28adf2541d
21 changed files with 541 additions and 185 deletions

View File

@@ -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" .

View File

@@ -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

View File

@@ -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

View File

@@ -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"]
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

View File

@@ -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

View File

@@ -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
}

View File

@@ -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 {

View File

@@ -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

View File

@@ -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=仅允许成员跟踪时间

View File

@@ -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

View File

@@ -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))

View File

@@ -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, "")
}

View File

@@ -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)

View File

@@ -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 中提取

View File

@@ -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 {

View File

@@ -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
}

View File

@@ -61,6 +61,7 @@ type InstallForm struct {
RequireSignInView bool
DefaultKeepEmailPrivate bool
DefaultAllowCreateOrganization bool
DefaultAllowCreateDevcontainer bool
DefaultEnableTimetracking bool
EnableUpdateChecker bool
NoReplyAddress string

View File

@@ -153,6 +153,8 @@
<dd>{{svg (Iif .Service.DefaultKeepEmailPrivate "octicon-check" "octicon-x")}}</dd>
<dt>{{ctx.Locale.Tr "admin.config.default_allow_create_organization"}}</dt>
<dd>{{svg (Iif .Service.DefaultAllowCreateOrganization "octicon-check" "octicon-x")}}</dd>
<dt>{{ctx.Locale.Tr "admin.config.default_allow_create_devcontainer"}}</dt>
<dd>{{svg (Iif .Service.DefaultAllowCreateDevcontainer "octicon-check" "octicon-x")}}</dd>
<dt>{{ctx.Locale.Tr "admin.config.enable_timetracking"}}</dt>
<dd>{{svg (Iif .Service.EnableTimetracking "octicon-check" "octicon-x")}}</dd>
{{if .Service.EnableTimetracking}}

View File

@@ -304,6 +304,12 @@
<input name="default_allow_create_organization" type="checkbox" {{if .default_allow_create_organization}}checked{{end}}>
</div>
</div>
<div class="inline field">
<div class="ui checkbox">
<label data-tooltip-content="{{ctx.Locale.Tr "install.default_allow_create_devcontainer_popup"}}">{{ctx.Locale.Tr "install.default_allow_create_devcontainer"}}</label>
<input name="default_allow_create_devcontainer" type="checkbox" {{if .default_allow_create_devcontainer}}checked{{end}}>
</div>
</div>
<div class="inline field">
<div class="ui checkbox">
<label data-tooltip-content="{{ctx.Locale.Tr "install.default_enable_timetracking_popup"}}">{{ctx.Locale.Tr "install.default_enable_timetracking"}}</label>

View File

@@ -14,6 +14,10 @@
"echo \"postCreateCommand\"",
"echo \"OK\""
],
"postAttachCommand": [
"echo \"postAttachCommand\"",
"echo \"OK\""
],
"runArgs": [
"-p 8888"
]

View File

@@ -22,6 +22,7 @@
{{else}}
<div class="ui container">
<form class="ui edit form">
<div class="repo-editor-header">
<div class="ui breadcrumb field">
@@ -36,7 +37,9 @@
</div>
</form>
{{if and .ValidateDevContainerConfiguration .HasDevContainer}}
<iframe id="webTerminalContainer" src="{{.WebSSHUrl}}" width="100%" style="height: 100vh; display: none;" frameborder="0">您的浏览器不支持iframe</iframe>
{{end}}
</div>
{{end}}
</div>
@@ -47,7 +50,7 @@
<strong>{{ctx.Locale.Tr "repo.dev_container_control"}}</strong>
<div class="ui relaxed list">
{{if .HasDevContainer}}
{{if and .ValidateDevContainerConfiguration .HasDevContainer}}
<div style=" display: none;" id="deleteContainer" class="item"><a class="delete-button flex-text-inline" data-modal="#delete-repo-devcontainer-of-user-modal" href="#" data-url="{{.Repository.Link}}/devcontainer/delete">{{svg "octicon-trash" 14}}{{ctx.Locale.Tr "repo.dev_container_control.delete"}}</a></div>
{{if .isAdmin}}
<div style=" display: none;" id="updateContainer" class="item"><a class="delete-button flex-text-inline" style="color:black; " data-modal-id="updatemodal" href="#">{{svg "octicon-database"}}{{ctx.Locale.Tr "repo.dev_container_control.update"}}</a></div>
@@ -66,7 +69,7 @@
<div style=" display: none;" id="createContainer" class="item">
<div>
<form method="get" action="{{.Repository.Link}}/devcontainer/create" class="ui edit form">
<button class="flex-text-inline" type="submit">{{svg "octicon-terminal" 14 "tw-mr-2"}} Create Dev Container</button>
<button class="flex-text-inline" type="submit">{{svg "octicon-terminal" 14 "tw-mr-2"}} {{ctx.Locale.Tr "repo.dev_container_control.create"}}</button>
</form>
</div>
</div>
@@ -84,6 +87,16 @@
<!-- 结束Dev Container 正文内容 -->
</div>
</div>
<!-- 自定义警告框 -->
<div id="customAlert" class="custom-alert">
<div class="alert-content">
<div class="alert-header">
<strong>提示信息</strong>
<button class="alert-close" onclick="closeCustomAlert()">&times;</button>
</div>
<div id="alertText" class="alert-body"></div>
</div>
</div>
<!-- 确认删除 Dev Container 模态对话框 -->
<div class="ui g-modal-confirm delete modal" id="delete-repo-devcontainer-of-user-modal">
@@ -96,24 +109,14 @@
</div>
{{template "base/modal_actions_confirm" .}}
</div>
<!-- 确认 Dev Container 模态对话框 -->
<!-- 保存 Dev Container 模态对话框 -->
<div class="ui g-modal-confirm delete modal" style="width: 35%" id="updatemodal">
<div class="header">
{{ctx.Locale.Tr "repo.dev_container_control.update"}}
</div>
<div class="content">
<form class="ui form tw-max-w-2xl tw-m-auto" id="updateForm" onsubmit="submitForm(event)">
<div class="inline field">
<div class="ui checkbox">
{{if not .HasDevContainerDockerfile}}
<input type="checkbox" id="SaveMethod" name="SaveMethod" disabled>
{{else}}
<input type="checkbox" id="SaveMethod" name="SaveMethod" value="on">
{{end}}
<label for="SaveMethod">Build From Dockerfile</label>
</div>
</div>
<div class="required field ">
<label for="RepositoryAddress">Registry:</label>
<input style="border: 1px solid black;" type="text" id="RepositoryAddress" name="RepositoryAddress" value="{{.RepositoryAddress}}">
@@ -124,13 +127,38 @@
</div>
<div class="required field ">
<label for="RepositoryPassword">Registry Password:</label>
<input style="border: 1px solid black;" type="text" id="RepositoryPassword" name="RepositoryPassword" required>
<div style="position: relative; display: inline-block; width: 100%;">
<input style="border: 1px solid black; width: 100%; padding-right: 80px;"
type="password"
id="RepositoryPassword"
name="RepositoryPassword"
required
autocomplete="current-password">
<button type="button"
style="position: absolute; right: 5px; top: 50%; transform: translateY(-50%);
background: none; border: none; cursor: pointer; color: #666;
font-size: 12px; padding: 5px 8px;"
onclick="togglePasswordVisibility('RepositoryPassword', this)">
显示密码
</button>
</div>
</div>
<div class="required field ">
<label for="ImageName">Image(name:tag):</label>
<input style="border: 1px solid black;" type="text" id="ImageName" name="ImageName" value="{{.ImageName}}">
</div>
<div class="inline field">
<div class="ui checkbox">
{{if not .HasDevContainerDockerfile}}
<input type="checkbox" id="SaveMethod" name="SaveMethod" disabled>
<label for="SaveMethod">There is no Dockerfile</label>
{{else}}
<input type="checkbox" id="SaveMethod" name="SaveMethod" value="on">
<label for="SaveMethod">Build From Dockerfile: {{.DockerfilePath}}</label>
{{end}}
</div>
</div>
<div class="actions">
<button class="ui primary button" type="submit" id="updateSubmitButton" >Submit</button>
<button class="ui cancel button" id="updateCloseButton">Close</button>
@@ -143,6 +171,21 @@
<script>
document.getElementById('updateSubmitButton').addEventListener('click', function() {
const form = document.getElementById('updateForm');
const formData = new FormData(form);
var RepositoryAddress = formData.get('RepositoryAddress');
var RepositoryUsername = formData.get('RepositoryUsername');
var RepositoryPassword = formData.get('RepositoryPassword');
var SaveMethod = formData.get('SaveMethod');
var ImageName = formData.get('ImageName');
if(ImageName != "" && SaveMethod != "" && RepositoryPassword != "" && RepositoryUsername != "" && RepositoryAddress != ""){
document.getElementById('updatemodal').classList.add('is-loading')
}
});
var status = '-1'
var intervalID
const createContainer = document.getElementById('createContainer');
@@ -233,13 +276,13 @@ function getStatus() {
if(status !== '9' && status !== '-1' && data.status == '9'){
window.location.reload();
}
if(status !== '-1' && data.status == '-1'){
else if(status !== '-1' && data.status == '-1'){
window.location.reload();
}
if(status !== '4' && status !== '-1' && data.status == '4'){
window.location.reload();
else if(status !== '4' && status !== '-1' && data.status == '4'){
//window.location.reload();
}
if (data.status == '-1' || data.status == '') {
else if (data.status == '-1' || data.status == '') {
if (loadingElement) {
loadingElement.style.display = 'none';
}
@@ -333,7 +376,7 @@ function getStatus() {
console.error('Error:', error);
});
}
intervalID = setInterval(getStatus, 3000);
intervalID = setInterval(getStatus, 5000);
if (restartContainer) {
restartContainer.addEventListener('click', function(event) {
// 处理点击逻辑
@@ -342,7 +385,7 @@ if (restartContainer) {
loadingElement.style.display = 'block';
}
fetch('{{.Repository.Link}}' + '/devcontainer/restart')
.then(response => {intervalID = setInterval(getStatus, 3000);})
.then(response => {intervalID = setInterval(getStatus, 5000);})
});
}
if (stopContainer) {
@@ -353,7 +396,7 @@ if (stopContainer) {
}
// 处理点击逻辑
fetch('{{.Repository.Link}}' + '/devcontainer/stop')
.then(response => {intervalID = setInterval(getStatus, 3000);})
.then(response => {intervalID = setInterval(getStatus, 5000);})
});
}
@@ -363,10 +406,46 @@ if (deleteContainer) {
});
}
function togglePasswordVisibility(passwordFieldId, button) {
const passwordInput = document.getElementById(passwordFieldId);
if (passwordInput.type === 'password') {
passwordInput.type = 'text';
button.textContent = '隐藏密码';
button.style.color = '#2185d0'; // 主色调,表示激活状态
} else {
passwordInput.type = 'password';
button.textContent = '显示密码';
button.style.color = '#666'; // 恢复默认颜色
}
}
function showCustomAlert(message, title = "提示信息") {
const alertBox = document.getElementById('customAlert');
const alertText = document.getElementById('alertText');
const alertHeader = alertBox.querySelector('.alert-header strong');
alertHeader.textContent = title;
alertText.textContent = message;
alertBox.style.display = 'block';
}
function closeCustomAlert() {
document.getElementById('customAlert').style.display = 'none';
}
// 点击背景关闭
document.getElementById('customAlert').addEventListener('click', function(e) {
if (e.target === this) {
closeCustomAlert();
}
});
function submitForm(event) {
event.preventDefault(); // 阻止默认的表单提交行为
const {csrfToken} = window.config;
const {appSubUrl} = window.config;
const formModal = document.getElementById('updatemodal');
const form = document.getElementById('updateForm');
const submitButton = document.getElementById('updateSubmitButton');
const closeButton = document.getElementById('updateCloseButton');
@@ -390,9 +469,10 @@ function submitForm(event) {
.then(response => response.json())
.then(data => {
submitButton.disabled = false;
alert(data.message);
formModal.classList.remove('is-loading')
showCustomAlert(data.message);
if(data.redirect){
closeButton.click()
closeCustomAlert()
}
intervalID = setInterval(getStatus, 3000);
})
@@ -422,6 +502,69 @@ function submitForm(event) {
0%{-webkit-transform:rotate(0deg)}
100%{-webkit-transform:rotate(360deg)}
}
.custom-alert {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.5);
z-index: 10000;
}
.alert-content {
color: black;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: white;
padding: 0; /* 移除内边距,在内部元素中设置 */
border-radius: 8px;
width: 80%;
max-width: 600px;
max-height: 80%;
display: flex;
flex-direction: column;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
.alert-header {
padding: 15px 20px;
border-bottom: 1px solid #eee;
background: #f8f9fa;
border-radius: 8px 8px 0 0;
position: sticky;
top: 0;
z-index: 10;
display: flex;
justify-content: space-between;
align-items: center;
}
.alert-close {
cursor: pointer;
font-size: 24px;
font-weight: bold;
color: #666;
background: none;
border: none;
padding: 0;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
}
.alert-close:hover {
background: #e9ecef;
color: #000;
}
.alert-body {
padding: 20px;
overflow-y: auto;
max-height: calc(80vh - 100px); /* 减去头部高度 */
white-space: pre-wrap;
word-wrap: break-word;
}
</style>
{{template "base/footer" .}}