Compare commits
1 Commits
docs/kuber
...
devstar-do
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
28adf2541d |
23
Makefile
23
Makefile
@@ -917,12 +917,31 @@ generate-manpage: ## generate manpage
|
|||||||
|
|
||||||
.PHONY: devstar
|
.PHONY: devstar
|
||||||
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 .
|
docker build -t devstar-studio:latest -f docker/Dockerfile.devstar .
|
||||||
|
|
||||||
.PHONY: docker
|
.PHONY: docker
|
||||||
docker:
|
docker:
|
||||||
docker build -t devstar.cn/devstar/webterminal:latest -f docker/Dockerfile.webTerminal .
|
|
||||||
|
|
||||||
docker build --disable-content-trust=false -t $(DOCKER_REF) .
|
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" .
|
# support also build args docker build --build-arg GITEA_VERSION=v1.2.3 --build-arg TAGS="bindata sqlite sqlite_unlock_notify" .
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,12 @@ RUN apk --no-cache add \
|
|||||||
&& rm -rf /var/cache/apk/*
|
&& rm -rf /var/cache/apk/*
|
||||||
|
|
||||||
# To acquire Gitea dev container:
|
# 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 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
|
# $ docker push devstar.cn/devstar/devstar-dev-container:latest
|
||||||
|
|
||||||
|
|
||||||
|
# Release Notes:
|
||||||
|
# v1.0 - Initial release
|
||||||
|
|||||||
@@ -19,6 +19,12 @@ RUN apk --no-cache add \
|
|||||||
&& rm -rf /var/cache/apk/*
|
&& rm -rf /var/cache/apk/*
|
||||||
|
|
||||||
# To acquire Gitea base runtime container:
|
# 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 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
|
# $ docker push devstar.cn/devstar/devstar-runtime-container:latest
|
||||||
|
|
||||||
|
|
||||||
|
# Release Notes:
|
||||||
|
# v1.0 - Initial release
|
||||||
|
|||||||
@@ -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/*
|
apt remove --purge curl -y && apt autoremove -y && apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||||
|
|
||||||
ENTRYPOINT ["/usr/bin/tini", "--"]
|
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
|
||||||
@@ -667,6 +667,7 @@ func createUser(ctx context.Context, u *User, meta *Meta, createdByAdmin bool, o
|
|||||||
u.KeepEmailPrivate = setting.Service.DefaultKeepEmailPrivate
|
u.KeepEmailPrivate = setting.Service.DefaultKeepEmailPrivate
|
||||||
u.Visibility = setting.Service.DefaultUserVisibilityMode
|
u.Visibility = setting.Service.DefaultUserVisibilityMode
|
||||||
u.AllowCreateOrganization = setting.Service.DefaultAllowCreateOrganization && !setting.Admin.DisableRegularOrgCreation
|
u.AllowCreateOrganization = setting.Service.DefaultAllowCreateOrganization && !setting.Admin.DisableRegularOrgCreation
|
||||||
|
u.AllowCreateDevcontainer = setting.Service.DefaultAllowCreateDevcontainer
|
||||||
u.EmailNotificationsPreference = setting.Admin.DefaultEmailNotification
|
u.EmailNotificationsPreference = setting.Admin.DefaultEmailNotification
|
||||||
u.MaxRepoCreation = -1
|
u.MaxRepoCreation = -1
|
||||||
u.Theme = setting.UI.DefaultTheme
|
u.Theme = setting.UI.DefaultTheme
|
||||||
|
|||||||
@@ -89,24 +89,22 @@ func GetContainerStatus(cli *client.Client, containerID string) (string, error)
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
state := containerInfo.State
|
state := containerInfo.State
|
||||||
return state.Status, nil
|
return state.Status, nil
|
||||||
}
|
}
|
||||||
func PushImage(dockerHost string, username string, password string, registryUrl string, imageRef string) error {
|
func PushImage(dockerHost string, username string, password string, registryUrl string, imageRef string) error {
|
||||||
script := "docker " + "-H " + dockerHost + " login -u " + username + " -p " + password + " " + registryUrl + " "
|
script := "docker " + "-H " + dockerHost + " login -u " + username + " -p " + password + " " + registryUrl + " "
|
||||||
cmd := exec.Command("sh", "-c", script)
|
cmd := exec.Command("sh", "-c", script)
|
||||||
_, err := cmd.CombinedOutput()
|
output, err := cmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("%s \n 镜像登录失败: %s", string(output), err.Error())
|
||||||
}
|
}
|
||||||
// 推送到仓库
|
// 推送到仓库
|
||||||
script = "docker " + "-H " + dockerHost + " push " + imageRef
|
script = "docker " + "-H " + dockerHost + " push " + imageRef
|
||||||
cmd = exec.Command("sh", "-c", script)
|
cmd = exec.Command("sh", "-c", script)
|
||||||
_, err = cmd.CombinedOutput()
|
output, err = cmd.CombinedOutput()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("%s \n 镜像推送失败: %s", string(output), err.Error())
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ var Service = struct {
|
|||||||
McaptchaURL string
|
McaptchaURL string
|
||||||
DefaultKeepEmailPrivate bool
|
DefaultKeepEmailPrivate bool
|
||||||
DefaultAllowCreateOrganization bool
|
DefaultAllowCreateOrganization bool
|
||||||
|
DefaultAllowCreateDevcontainer bool
|
||||||
DefaultUserIsRestricted bool
|
DefaultUserIsRestricted bool
|
||||||
EnableTimetracking bool
|
EnableTimetracking bool
|
||||||
DefaultEnableTimetracking bool
|
DefaultEnableTimetracking bool
|
||||||
@@ -205,6 +206,7 @@ func loadServiceFrom(rootCfg ConfigProvider) {
|
|||||||
Service.McaptchaSitekey = sec.Key("MCAPTCHA_SITEKEY").MustString("")
|
Service.McaptchaSitekey = sec.Key("MCAPTCHA_SITEKEY").MustString("")
|
||||||
Service.DefaultKeepEmailPrivate = sec.Key("DEFAULT_KEEP_EMAIL_PRIVATE").MustBool()
|
Service.DefaultKeepEmailPrivate = sec.Key("DEFAULT_KEEP_EMAIL_PRIVATE").MustBool()
|
||||||
Service.DefaultAllowCreateOrganization = sec.Key("DEFAULT_ALLOW_CREATE_ORGANIZATION").MustBool(true)
|
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.DefaultUserIsRestricted = sec.Key("DEFAULT_USER_IS_RESTRICTED").MustBool(false)
|
||||||
Service.EnableTimetracking = sec.Key("ENABLE_TIMETRACKING").MustBool(true)
|
Service.EnableTimetracking = sec.Key("ENABLE_TIMETRACKING").MustBool(true)
|
||||||
if Service.EnableTimetracking {
|
if Service.EnableTimetracking {
|
||||||
|
|||||||
@@ -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 = Hide Email Addresses by Default
|
||||||
default_keep_email_private_popup = Hide email addresses of new user accounts 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_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_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 = Enable Time Tracking by Default
|
||||||
default_enable_timetracking_popup = Enable time tracking for new repositories by default.
|
default_enable_timetracking_popup = Enable time tracking for new repositories by default.
|
||||||
no_reply_address = Hidden Email Domain
|
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.reset_password_code_lives = Recover Account Code Expiry Time
|
||||||
config.default_keep_email_private = Hide Email Addresses by Default
|
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_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.enable_timetracking = Enable Time Tracking
|
||||||
config.default_enable_timetracking = Enable Time Tracking by Default
|
config.default_enable_timetracking = Enable Time Tracking by Default
|
||||||
config.default_allow_only_contributors_to_track_time = Let Only Contributors Track Time
|
config.default_allow_only_contributors_to_track_time = Let Only Contributors Track Time
|
||||||
|
|||||||
@@ -357,7 +357,9 @@ invalid_log_root_path=日志路径无效: %v
|
|||||||
default_keep_email_private=默认情况下隐藏邮箱地址
|
default_keep_email_private=默认情况下隐藏邮箱地址
|
||||||
default_keep_email_private_popup=默认情况下,隐藏新用户帐户的邮箱地址。
|
default_keep_email_private_popup=默认情况下,隐藏新用户帐户的邮箱地址。
|
||||||
default_allow_create_organization=默认情况下允许创建组织
|
default_allow_create_organization=默认情况下允许创建组织
|
||||||
|
default_allow_create_devcontainer=默认情况下允许创建容器
|
||||||
default_allow_create_organization_popup=默认情况下, 允许新用户帐户创建组织。
|
default_allow_create_organization_popup=默认情况下, 允许新用户帐户创建组织。
|
||||||
|
default_allow_create_devcontainer_popup=默认情况下, 允许新用户帐户创建容器。
|
||||||
default_enable_timetracking=默认情况下启用时间跟踪
|
default_enable_timetracking=默认情况下启用时间跟踪
|
||||||
default_enable_timetracking_popup=默认情况下启用新仓库的时间跟踪。
|
default_enable_timetracking_popup=默认情况下启用新仓库的时间跟踪。
|
||||||
no_reply_address=隐藏邮件域
|
no_reply_address=隐藏邮件域
|
||||||
@@ -3408,6 +3410,7 @@ config.active_code_lives=激活用户链接有效期
|
|||||||
config.reset_password_code_lives=恢复账户验证码过期时间
|
config.reset_password_code_lives=恢复账户验证码过期时间
|
||||||
config.default_keep_email_private=默认隐藏邮箱地址
|
config.default_keep_email_private=默认隐藏邮箱地址
|
||||||
config.default_allow_create_organization=默认情况下允许创建组织
|
config.default_allow_create_organization=默认情况下允许创建组织
|
||||||
|
config.default_allow_create_devcontainer=默认情况下允许创建 DevContainer
|
||||||
config.enable_timetracking=启用时间跟踪
|
config.enable_timetracking=启用时间跟踪
|
||||||
config.default_enable_timetracking=默认情况下启用时间跟踪
|
config.default_enable_timetracking=默认情况下启用时间跟踪
|
||||||
config.default_allow_only_contributors_to_track_time=仅允许成员跟踪时间
|
config.default_allow_only_contributors_to_track_time=仅允许成员跟踪时间
|
||||||
|
|||||||
@@ -86,12 +86,12 @@ function install {
|
|||||||
sudo docker pull devstar.cn/devstar/$IMAGE_NAME:$VERSION
|
sudo docker pull devstar.cn/devstar/$IMAGE_NAME:$VERSION
|
||||||
IMAGE_REGISTRY_USER=devstar.cn/devstar
|
IMAGE_REGISTRY_USER=devstar.cn/devstar
|
||||||
fi
|
fi
|
||||||
if sudo docker pull devstar.cn/devstar/webterminal:latest; then
|
if sudo docker pull mengning997/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"
|
|
||||||
sudo docker tag mengning997/webterminal:latest devstar.cn/devstar/webterminal:latest
|
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
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,7 +137,10 @@ function stop {
|
|||||||
fi
|
fi
|
||||||
if [ $(docker ps -a --filter "name=^/devstar-studio$" -q | wc -l) -gt 0 ]; then
|
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
|
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
|
# Function to logs
|
||||||
|
|||||||
@@ -155,6 +155,7 @@ func Install(ctx *context.Context) {
|
|||||||
form.RequireSignInView = setting.Service.RequireSignInViewStrict
|
form.RequireSignInView = setting.Service.RequireSignInViewStrict
|
||||||
form.DefaultKeepEmailPrivate = setting.Service.DefaultKeepEmailPrivate
|
form.DefaultKeepEmailPrivate = setting.Service.DefaultKeepEmailPrivate
|
||||||
form.DefaultAllowCreateOrganization = setting.Service.DefaultAllowCreateOrganization
|
form.DefaultAllowCreateOrganization = setting.Service.DefaultAllowCreateOrganization
|
||||||
|
form.DefaultAllowCreateDevcontainer = setting.Service.DefaultAllowCreateDevcontainer
|
||||||
form.DefaultEnableTimetracking = setting.Service.DefaultEnableTimetracking
|
form.DefaultEnableTimetracking = setting.Service.DefaultEnableTimetracking
|
||||||
form.NoReplyAddress = setting.Service.NoReplyAddress
|
form.NoReplyAddress = setting.Service.NoReplyAddress
|
||||||
form.PasswordAlgorithm = hash.ConfigHashAlgorithm(setting.PasswordHashAlgo)
|
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("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_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_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("DEFAULT_ENABLE_TIMETRACKING").SetValue(strconv.FormatBool(form.DefaultEnableTimetracking))
|
||||||
cfg.Section("service").Key("NO_REPLY_ADDRESS").SetValue(form.NoReplyAddress)
|
cfg.Section("service").Key("NO_REPLY_ADDRESS").SetValue(form.NoReplyAddress)
|
||||||
cfg.Section("cron.update_checker").Key("ENABLED").SetValue(strconv.FormatBool(form.EnableUpdateChecker))
|
cfg.Section("cron.update_checker").Key("ENABLED").SetValue(strconv.FormatBool(form.EnableUpdateChecker))
|
||||||
|
|||||||
@@ -55,21 +55,22 @@ func GetDevContainerDetails(ctx *context.Context) {
|
|||||||
ctx.Data["ValidateDevContainerConfiguration"] = false
|
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 {
|
if err != nil {
|
||||||
log.Info(err.Error())
|
log.Info(err.Error())
|
||||||
ctx.Flash.Error(err.Error(), true)
|
ctx.Flash.Error(err.Error(), true)
|
||||||
}
|
}
|
||||||
if ctx.Data["HasDevContainer"] == true {
|
if ctx.Data["HasDevContainer"] == true {
|
||||||
configurationString, _ := devcontainer_service.GetDevcontainerConfigurationString(ctx, ctx.Repo.Repository)
|
if ctx.Data["HasDevContainerConfiguration"] == true {
|
||||||
configurationModel, _ := devcontainer_service.UnmarshalDevcontainerConfigContent(configurationString)
|
configurationString, _ := devcontainer_service.GetDevcontainerConfigurationString(ctx, ctx.Repo.Repository)
|
||||||
imageName := configurationModel.Image
|
configurationModel, _ := devcontainer_service.UnmarshalDevcontainerConfigContent(configurationString)
|
||||||
registry, namespace, repo, tag := devcontainer_service.ParseImageName(imageName)
|
imageName := configurationModel.Image
|
||||||
log.Info("%v %v", repo, tag)
|
registry, namespace, repo, tag := devcontainer_service.ParseImageName(imageName)
|
||||||
ctx.Data["RepositoryAddress"] = registry
|
log.Info("%v %v", repo, tag)
|
||||||
ctx.Data["RepositoryUsername"] = namespace
|
ctx.Data["RepositoryAddress"] = registry
|
||||||
ctx.Data["ImageName"] = "dev-" + ctx.Repo.Repository.Name + ":latest"
|
ctx.Data["RepositoryUsername"] = namespace
|
||||||
|
ctx.Data["ImageName"] = "dev-" + ctx.Repo.Repository.Name + ":latest"
|
||||||
|
}
|
||||||
if cfg.Section("k8s").Key("ENABLE").Value() == "true" {
|
if cfg.Section("k8s").Key("ENABLE").Value() == "true" {
|
||||||
// 获取WebSSH服务端口
|
// 获取WebSSH服务端口
|
||||||
webTerminalURL, err := devcontainer_service.GetWebTerminalURL(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID)
|
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"))
|
ctx.Redirect(path.Join(ctx.Repo.RepoLink, "/devcontainer"))
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
rootPort, err := devcontainer_service.GetPortFromURL(cfg.Section("server").Key("ROOT_URL").Value())
|
rootPort, err := devcontainer_service.GetPortFromURL(cfg.Section("server").Key("ROOT_URL").Value())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Flash.Error(err.Error(), true)
|
ctx.Flash.Error(err.Error(), true)
|
||||||
@@ -136,7 +136,6 @@ func GetDevContainerDetails(ctx *context.Context) {
|
|||||||
}
|
}
|
||||||
ctx.Data["WebSSHUrl"] = webTerminalURL + "?type=docker&" + terminalParams
|
ctx.Data["WebSSHUrl"] = webTerminalURL + "?type=docker&" + terminalParams
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
terminalURL, err := devcontainer_service.Get_IDE_TerminalURL(ctx, ctx.Doer, ctx.Repo)
|
terminalURL, err := devcontainer_service.Get_IDE_TerminalURL(ctx, ctx.Doer, ctx.Repo)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@@ -145,7 +144,6 @@ func GetDevContainerDetails(ctx *context.Context) {
|
|||||||
ctx.Data["WindsurfUrl"] = "windsurf" + terminalURL
|
ctx.Data["WindsurfUrl"] = "windsurf" + terminalURL
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 携带数据渲染页面,返回
|
// 3. 携带数据渲染页面,返回
|
||||||
ctx.Data["Title"] = ctx.Locale.Tr("repo.dev_container")
|
ctx.Data["Title"] = ctx.Locale.Tr("repo.dev_container")
|
||||||
ctx.Data["PageIsDevContainer"] = true
|
ctx.Data["PageIsDevContainer"] = true
|
||||||
@@ -300,7 +298,7 @@ func UpdateDevContainer(ctx *context.Context) {
|
|||||||
ctx.JSON(http.StatusOK, map[string]string{"message": err.Error()})
|
ctx.JSON(http.StatusOK, map[string]string{"message": err.Error()})
|
||||||
return
|
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 {
|
if err != nil {
|
||||||
ctx.JSON(http.StatusOK, map[string]string{"message": err.Error()})
|
ctx.JSON(http.StatusOK, map[string]string{"message": err.Error()})
|
||||||
return
|
return
|
||||||
@@ -318,18 +316,43 @@ func GetTerminalCommand(ctx *context.Context) {
|
|||||||
log.Info(err.Error())
|
log.Info(err.Error())
|
||||||
status = "error"
|
status = "error"
|
||||||
}
|
}
|
||||||
|
ctx.JSON(http.StatusOK, map[string]string{"command": cmd, "status": status, "workdir": "/workspace/" + ctx.Repo.Repository.Name})
|
||||||
ctx.JSON(http.StatusOK, map[string]string{"command": cmd, "status": status})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetDevContainerOutput(ctx *context.Context) {
|
func GetDevContainerOutput(ctx *context.Context) {
|
||||||
// 设置 CORS 响应头
|
// 设置 CORS 响应头
|
||||||
ctx.Resp.Header().Set("Access-Control-Allow-Origin", "*")
|
ctx.Resp.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
ctx.Resp.Header().Set("Access-Control-Allow-Methods", "*")
|
ctx.Resp.Header().Set("Access-Control-Allow-Methods", "*")
|
||||||
ctx.Resp.Header().Set("Access-Control-Allow-Headers", "*")
|
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 {
|
if err != nil {
|
||||||
log.Info(err.Error())
|
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, "")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"path"
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/models/db"
|
||||||
git_model "code.gitea.io/gitea/models/git"
|
git_model "code.gitea.io/gitea/models/git"
|
||||||
"code.gitea.io/gitea/models/issues"
|
"code.gitea.io/gitea/models/issues"
|
||||||
"code.gitea.io/gitea/models/unit"
|
"code.gitea.io/gitea/models/unit"
|
||||||
@@ -25,6 +26,7 @@ import (
|
|||||||
"code.gitea.io/gitea/modules/web"
|
"code.gitea.io/gitea/modules/web"
|
||||||
"code.gitea.io/gitea/services/context"
|
"code.gitea.io/gitea/services/context"
|
||||||
"code.gitea.io/gitea/services/context/upload"
|
"code.gitea.io/gitea/services/context/upload"
|
||||||
|
devcontainer_service "code.gitea.io/gitea/services/devcontainer"
|
||||||
"code.gitea.io/gitea/services/forms"
|
"code.gitea.io/gitea/services/forms"
|
||||||
files_service "code.gitea.io/gitea/services/repository/files"
|
files_service "code.gitea.io/gitea/services/repository/files"
|
||||||
)
|
)
|
||||||
@@ -411,6 +413,23 @@ func DeleteFilePost(ctx *context.Context) {
|
|||||||
editorHandleFileOperationError(ctx, parsed.NewBranchName, err)
|
editorHandleFileOperationError(ctx, parsed.NewBranchName, err)
|
||||||
return
|
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))
|
ctx.Flash.Success(ctx.Tr("repo.editor.file_delete_success", treePath))
|
||||||
redirectTreePath := getClosestParentWithFiles(ctx.Repo.GitRepo, parsed.NewBranchName, treePath)
|
redirectTreePath := getClosestParentWithFiles(ctx.Repo.GitRepo, parsed.NewBranchName, treePath)
|
||||||
|
|||||||
@@ -1434,13 +1434,14 @@ func registerWebRoutes(m *web.Router) {
|
|||||||
m.Get("/status", devcontainer_web.GetDevContainerStatus)
|
m.Get("/status", devcontainer_web.GetDevContainerStatus)
|
||||||
m.Get("/command", devcontainer_web.GetTerminalCommand)
|
m.Get("/command", devcontainer_web.GetTerminalCommand)
|
||||||
m.Get("/output", devcontainer_web.GetDevContainerOutput)
|
m.Get("/output", devcontainer_web.GetDevContainerOutput)
|
||||||
|
m.Methods("POST, OPTIONS", "/output", devcontainer_web.SaveDevContainerOutput)
|
||||||
},
|
},
|
||||||
// 解析仓库信息
|
// 解析仓库信息
|
||||||
// 具有code读取权限
|
// 具有code读取权限
|
||||||
context.RepoAssignment, reqUnitCodeReader,
|
context.RepoAssignment, reqUnitCodeReader,
|
||||||
)
|
)
|
||||||
m.Get("/devstar-home", devcontainer_web.VscodeHome) // 旧地址,保留兼容性
|
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() {
|
m.Group("/api/devcontainer", func() {
|
||||||
// 获取 某用户在某仓库中的 DevContainer 细节(包括SSH连接信息),默认不会等待 (wait = false)
|
// 获取 某用户在某仓库中的 DevContainer 细节(包括SSH连接信息),默认不会等待 (wait = false)
|
||||||
// 请求方式: GET /api/devcontainer?repoId=${repoId}&wait=true // 无需传入 userId,直接从 token 中提取
|
// 请求方式: GET /api/devcontainer?repoId=${repoId}&wait=true // 无需传入 userId,直接从 token 中提取
|
||||||
|
|||||||
@@ -6,8 +6,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"net"
|
|
||||||
"net/url"
|
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -70,21 +68,21 @@ func HasDevContainerConfiguration(ctx context.Context, repo *gitea_context.Repos
|
|||||||
return true, nil
|
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)
|
_, err := FileExists(".devcontainer/devcontainer.json", repo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if git.IsErrNotExist(err) {
|
if git.IsErrNotExist(err) {
|
||||||
return false, nil
|
return false, "", nil
|
||||||
}
|
}
|
||||||
return false, err
|
return false, "", err
|
||||||
}
|
}
|
||||||
configurationString, err := GetDevcontainerConfigurationString(ctx, repo.Repository)
|
configurationString, err := GetDevcontainerConfigurationString(ctx, repo.Repository)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, "", err
|
||||||
}
|
}
|
||||||
configurationModel, err := UnmarshalDevcontainerConfigContent(configurationString)
|
configurationModel, err := UnmarshalDevcontainerConfigContent(configurationString)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, "", err
|
||||||
}
|
}
|
||||||
// 执行验证
|
// 执行验证
|
||||||
if errs := configurationModel.Validate(); len(errs) > 0 {
|
if errs := configurationModel.Validate(); len(errs) > 0 {
|
||||||
@@ -92,20 +90,34 @@ func HasDevContainerDockerFile(ctx context.Context, repo *gitea_context.Reposito
|
|||||||
for _, err := range errs {
|
for _, err := range errs {
|
||||||
fmt.Printf(" - %s\n", err.Error())
|
fmt.Printf(" - %s\n", err.Error())
|
||||||
}
|
}
|
||||||
return false, fmt.Errorf("配置格式错误")
|
return false, "", fmt.Errorf("配置格式错误")
|
||||||
} else {
|
} else {
|
||||||
log.Info("%v", configurationModel)
|
log.Info("%v", configurationModel)
|
||||||
if configurationModel.Build == nil || configurationModel.Build.Dockerfile == "" {
|
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)
|
_, err := FileExists(".devcontainer/"+configurationModel.Build.Dockerfile, repo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if git.IsErrNotExist(err) {
|
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 {
|
func CreateDevcontainerConfiguration(repo *repo.Repository, doer *user.User) error {
|
||||||
@@ -435,7 +447,7 @@ func StopDevContainer(ctx context.Context, userID, repoID int64) error {
|
|||||||
return nil
|
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)
|
dbEngine := db.GetEngine(ctx)
|
||||||
var devContainerInfo devcontainer_models.Devcontainer
|
var devContainerInfo devcontainer_models.Devcontainer
|
||||||
cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf)
|
cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf)
|
||||||
@@ -445,25 +457,24 @@ func UpdateDevContainer(ctx context.Context, doer *user.User, repo *repo.Reposit
|
|||||||
_, err = dbEngine.
|
_, err = dbEngine.
|
||||||
Table("devcontainer").
|
Table("devcontainer").
|
||||||
Select("*").
|
Select("*").
|
||||||
Where("user_id = ? AND repo_id = ?", doer.ID, repo.ID).
|
Where("user_id = ? AND repo_id = ?", doer.ID, repo.Repository.ID).
|
||||||
Get(&devContainerInfo)
|
Get(&devContainerInfo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
_, err = dbEngine.Table("devcontainer").
|
_, 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})
|
Update(&devcontainer_models.Devcontainer{DevcontainerStatus: 5})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
otherCtx := context.Background()
|
otherCtx := context.Background()
|
||||||
if cfg.Section("k8s").Key("ENABLE").Value() == "true" {
|
if cfg.Section("k8s").Key("ENABLE").Value() == "true" {
|
||||||
//k8s的逻辑
|
//k8s的逻辑
|
||||||
} else {
|
} else {
|
||||||
updateErr := UpdateDevContainerByDocker(otherCtx, &devContainerInfo, updateInfo, repo, doer)
|
updateErr := UpdateDevContainerByDocker(otherCtx, &devContainerInfo, updateInfo, repo, doer)
|
||||||
_, err = dbEngine.Table("devcontainer").
|
_, 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})
|
Update(&devcontainer_models.Devcontainer{DevcontainerStatus: 4})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -534,58 +545,72 @@ func GetTerminalCommand(ctx context.Context, userID string, repo *repo.Repositor
|
|||||||
return "", "", err
|
return "", "", err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
case 2:
|
case 2:
|
||||||
//正在创建容器,创建容器成功,则状态转移
|
//正在创建容器,创建容器成功,则状态转移
|
||||||
if cfg.Section("k8s").Key("ENABLE").Value() == "true" {
|
if cfg.Section("k8s").Key("ENABLE").Value() == "true" {
|
||||||
//k8s的逻辑
|
//k8s的逻辑
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
status, err := GetDevContainerStatusFromDocker(ctx, devContainerInfo.Name)
|
exist, _, err := ContainerExists(ctx, devContainerInfo.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", err
|
return "", "", err
|
||||||
}
|
}
|
||||||
if status == "created" {
|
if !exist {
|
||||||
//添加脚本文件
|
_, err = dbEngine.Table("devcontainer_output").
|
||||||
if cfg.Section("k8s").Key("ENABLE").Value() == "true" {
|
Select("command").
|
||||||
} else {
|
Where("user_id = ? AND repo_id = ? AND list_id = ?", userID, repo.ID, realTimeStatus).
|
||||||
userNum, err := strconv.ParseInt(userID, 10, 64)
|
Get(&cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", err
|
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
|
} 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
|
break
|
||||||
case 3:
|
case 3:
|
||||||
@@ -614,6 +639,27 @@ func GetTerminalCommand(ctx context.Context, userID string, repo *repo.Repositor
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", err
|
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
|
break
|
||||||
}
|
}
|
||||||
@@ -636,67 +682,59 @@ func GetTerminalCommand(ctx context.Context, userID string, repo *repo.Repositor
|
|||||||
}
|
}
|
||||||
return cmd, fmt.Sprintf("%d", realTimeStatus), nil
|
return cmd, fmt.Sprintf("%d", realTimeStatus), nil
|
||||||
}
|
}
|
||||||
func GetDevContainerOutput(ctx context.Context, doer *user.User, repo *repo.Repository) (OutputResponse, error) {
|
func GetDevContainerOutput(ctx context.Context, user_id string, repo *repo.Repository) (string, error) {
|
||||||
var devContainerOutput []devcontainer_models.DevcontainerOutput
|
var devContainerOutput string
|
||||||
dbEngine := db.GetEngine(ctx)
|
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").
|
_, err := dbEngine.Table("devcontainer_output").
|
||||||
Where("user_id = ? AND repo_id = ?", doer.ID, repo.ID).
|
Select("output").
|
||||||
Find(&devContainerOutput)
|
Where("user_id = ? AND repo_id = ? AND list_id = ?", user_id, repo.ID, 4).
|
||||||
|
Get(&devContainerOutput)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return resp, err
|
return "", err
|
||||||
}
|
}
|
||||||
|
if devContainerOutput != "" {
|
||||||
if len(devContainerOutput) > 0 {
|
_, err = dbEngine.Table("devcontainer_output").
|
||||||
|
Where("user_id = ? AND repo_id = ? AND list_id = ?", user_id, repo.ID, 4).
|
||||||
resp.CurrentJob.Title = repo.Name + " Devcontainer Info"
|
Update(map[string]interface{}{
|
||||||
resp.CurrentJob.Detail = status
|
"output": "",
|
||||||
if status == "4" {
|
|
||||||
// 获取WebSSH服务端口
|
|
||||||
webTerminalURL, err := GetWebTerminalURL(ctx, doer.ID, repo.ID)
|
|
||||||
if err == nil {
|
|
||||||
return resp, err
|
|
||||||
}
|
|
||||||
// 解析URL
|
|
||||||
u, err := url.Parse(webTerminalURL)
|
|
||||||
if err != nil {
|
|
||||||
return resp, err
|
|
||||||
}
|
|
||||||
// 分离主机和端口
|
|
||||||
terminalHost, terminalPort, err := net.SplitHostPort(u.Host)
|
|
||||||
resp.CurrentJob.IP = terminalHost
|
|
||||||
resp.CurrentJob.Port = terminalPort
|
|
||||||
if err != nil {
|
|
||||||
return resp, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, item := range devContainerOutput {
|
|
||||||
logLines := []ViewStepLogLine{}
|
|
||||||
logLines = append(logLines, ViewStepLogLine{
|
|
||||||
Index: 1,
|
|
||||||
Message: item.Output,
|
|
||||||
})
|
})
|
||||||
resp.CurrentJob.Steps = append(resp.CurrentJob.Steps, &ViewJobStep{
|
if err != nil {
|
||||||
Summary: item.Command,
|
return "", err
|
||||||
Status: item.Status,
|
|
||||||
Logs: logLines,
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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) {
|
func GetMappedPort(ctx context.Context, containerName string, port string) (uint16, error) {
|
||||||
cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf)
|
cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf)
|
||||||
@@ -937,7 +975,6 @@ func GetCommandContent(ctx context.Context, userId int64, repo *repo.Repository)
|
|||||||
script = append(script, v)
|
script = append(script, v)
|
||||||
}
|
}
|
||||||
scriptCommand := strings.TrimSpace(strings.Join(script, "\n"))
|
scriptCommand := strings.TrimSpace(strings.Join(script, "\n"))
|
||||||
|
|
||||||
userCommand := scriptCommand + "\n" + onCreateCommand + "\n" + updateCommand + "\n" + postCreateCommand + "\n" + postStartCommand + "\n"
|
userCommand := scriptCommand + "\n" + onCreateCommand + "\n" + updateCommand + "\n" + postCreateCommand + "\n" + postStartCommand + "\n"
|
||||||
assetFS := templates.AssetFS()
|
assetFS := templates.AssetFS()
|
||||||
Content_tmpl, err := assetFS.ReadFile("repo/devcontainer/devcontainer_tmpl.sh")
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(devcontainerList) > 0 {
|
if len(devcontainerList) > 0 {
|
||||||
// 将公钥写入这些打开的容器中
|
// 将公钥写入这些打开的容器中
|
||||||
for _, repoDevContainer := range devcontainerList {
|
for _, repoDevContainer := range devcontainerList {
|
||||||
|
|||||||
@@ -16,10 +16,13 @@ import (
|
|||||||
"code.gitea.io/gitea/models/repo"
|
"code.gitea.io/gitea/models/repo"
|
||||||
"code.gitea.io/gitea/models/user"
|
"code.gitea.io/gitea/models/user"
|
||||||
docker_module "code.gitea.io/gitea/modules/docker"
|
docker_module "code.gitea.io/gitea/modules/docker"
|
||||||
|
"code.gitea.io/gitea/modules/git"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"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"
|
||||||
"github.com/docker/docker/api/types/container"
|
"github.com/docker/docker/api/types/container"
|
||||||
|
"github.com/docker/docker/api/types/filters"
|
||||||
"github.com/docker/docker/client"
|
"github.com/docker/docker/client"
|
||||||
"github.com/docker/docker/errdefs"
|
"github.com/docker/docker/errdefs"
|
||||||
"github.com/docker/go-connections/nat"
|
"github.com/docker/go-connections/nat"
|
||||||
@@ -129,6 +132,7 @@ func CreateDevContainerByDockerCommand(ctx context.Context, newDevcontainer *dev
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
var imageName = configurationModel.Image
|
var imageName = configurationModel.Image
|
||||||
dockerSocket, err := docker_module.GetDockerSocketPath()
|
dockerSocket, err := docker_module.GetDockerSocketPath()
|
||||||
if err != nil {
|
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() + `" ` +
|
var envFlags string = ` -e RepoLink="` + strings.TrimSuffix(cfg.Section("server").Key("ROOT_URL").Value(), `/`) + repo.Link() + `" ` +
|
||||||
` -e DevstarHost="` + newDevcontainer.DevcontainerHost + `"` +
|
` -e DevstarHost="` + newDevcontainer.DevcontainerHost + `"` +
|
||||||
` -e WorkSpace="` + newDevcontainer.DevcontainerWorkDir + `/` + repo.Name + `" ` +
|
` -e WorkSpace="` + newDevcontainer.DevcontainerWorkDir + `/` + repo.Name + `" ` +
|
||||||
` -e DEVCONTAINER_STATUS="start" `
|
` -e DEVCONTAINER_STATUS="start" ` +
|
||||||
|
` -e WEB_TERMINAL_HELLO="Successfully connected to the devcontainer" `
|
||||||
// 遍历 ContainerEnv 映射中的每个环境变量
|
// 遍历 ContainerEnv 映射中的每个环境变量
|
||||||
for name, value := range configurationModel.ContainerEnv {
|
for name, value := range configurationModel.ContainerEnv {
|
||||||
// 将每个环境变量转换为 "-e name=value" 格式
|
// 将每个环境变量转换为 "-e name=value" 格式
|
||||||
@@ -283,7 +288,7 @@ func CreateDevContainerByDockerCommand(ctx context.Context, newDevcontainer *dev
|
|||||||
Status: "waitting",
|
Status: "waitting",
|
||||||
UserId: newDevcontainer.UserId,
|
UserId: newDevcontainer.UserId,
|
||||||
RepoId: newDevcontainer.RepoId,
|
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,
|
ListId: 4,
|
||||||
DevcontainerId: newDevcontainer.Id,
|
DevcontainerId: newDevcontainer.Id,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
@@ -391,17 +396,16 @@ func StopDevContainerByDocker(ctx context.Context, devContainerName string) erro
|
|||||||
}
|
}
|
||||||
return nil
|
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
|
// 创建docker client
|
||||||
cli, err := docker_module.CreateDockerClient(ctx)
|
cli, err := docker_module.CreateDockerClient(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer cli.Close()
|
defer cli.Close()
|
||||||
|
|
||||||
// update容器
|
// update容器
|
||||||
imageRef := updateInfo.RepositoryAddress + "/" + updateInfo.RepositoryUsername + "/" + updateInfo.ImageName
|
imageRef := updateInfo.RepositoryAddress + "/" + updateInfo.RepositoryUsername + "/" + updateInfo.ImageName
|
||||||
configurationString, err := GetDevcontainerConfigurationString(ctx, repo)
|
configurationString, err := GetDevcontainerConfigurationString(ctx, repo.Repository)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -411,16 +415,45 @@ func UpdateDevContainerByDocker(ctx context.Context, devContainerInfo *devcontai
|
|||||||
}
|
}
|
||||||
|
|
||||||
if updateInfo.SaveMethod == "on" {
|
if updateInfo.SaveMethod == "on" {
|
||||||
|
|
||||||
// 创建构建上下文(包含Dockerfile的tar包)
|
// 创建构建上下文(包含Dockerfile的tar包)
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
tw := tar.NewWriter(&buf)
|
tw := tar.NewWriter(&buf)
|
||||||
defer tw.Close()
|
defer tw.Close()
|
||||||
// 添加Dockerfile到tar包
|
// 添加Dockerfile到tar包
|
||||||
|
var dockerfileContent string
|
||||||
dockerfile := "Dockerfile"
|
dockerfile := "Dockerfile"
|
||||||
dockerfileContent, err := GetFileContentByPath(ctx, repo, ".devcontainer/"+configurationModel.Build.Dockerfile)
|
if configurationModel.Build == nil || configurationModel.Build.Dockerfile == "" {
|
||||||
if err != nil {
|
_, err := FileExists(".devcontainer/Dockerfile", repo)
|
||||||
return err
|
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)
|
content := []byte(dockerfileContent)
|
||||||
header := &tar.Header{
|
header := &tar.Header{
|
||||||
Name: dockerfile,
|
Name: dockerfile,
|
||||||
@@ -468,11 +501,12 @@ func UpdateDevContainerByDocker(ctx context.Context, devContainerInfo *devcontai
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 定义正则表达式来匹配 image 字段
|
// 定义正则表达式来匹配 image 字段
|
||||||
re := regexp.MustCompile(`"image"\s*:\s*"([^"]+)"`)
|
re := regexp.MustCompile(`"image"\s*:\s*"([^"]+)"`)
|
||||||
// 使用正则表达式查找并替换 image 字段的值
|
// 使用正则表达式查找并替换 image 字段的值
|
||||||
newConfiguration := re.ReplaceAllString(configurationString, `"image": "`+imageRef+`"`)
|
newConfiguration := re.ReplaceAllString(configurationString, `"image": "`+imageRef+`"`)
|
||||||
err = UpdateDevcontainerConfiguration(newConfiguration, repo, doer)
|
err = UpdateDevcontainerConfiguration(newConfiguration, repo.Repository, doer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -484,7 +518,6 @@ func UpdateDevContainerByDocker(ctx context.Context, devContainerInfo *devcontai
|
|||||||
// - bool: 镜像是否存在(true=存在,false=不存在)
|
// - bool: 镜像是否存在(true=存在,false=不存在)
|
||||||
// - error: 非空表示检查过程中发生错误
|
// - error: 非空表示检查过程中发生错误
|
||||||
func ImageExists(ctx context.Context, imageName string) (bool, error) {
|
func ImageExists(ctx context.Context, imageName string) (bool, error) {
|
||||||
|
|
||||||
// 创建 Docker 客户端
|
// 创建 Docker 客户端
|
||||||
cli, err := docker_module.CreateDockerClient(ctx)
|
cli, err := docker_module.CreateDockerClient(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -519,7 +552,6 @@ func CheckDirExistsFromDocker(ctx context.Context, containerName, dirPath string
|
|||||||
AttachStdout: true,
|
AttachStdout: true,
|
||||||
AttachStderr: true,
|
AttachStderr: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建 exec 实例
|
// 创建 exec 实例
|
||||||
execResp, err := cli.ContainerExecCreate(context.Background(), containerID, execConfig)
|
execResp, err := cli.ContainerExecCreate(context.Background(), containerID, execConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -542,6 +574,7 @@ func CheckDirExistsFromDocker(ctx context.Context, containerName, dirPath string
|
|||||||
exitCode = resp.ExitCode
|
exitCode = resp.ExitCode
|
||||||
return exitCode == 0, nil // 退出码为 0 表示目录存在
|
return exitCode == 0, nil // 退出码为 0 表示目录存在
|
||||||
}
|
}
|
||||||
|
|
||||||
func CheckFileExistsFromDocker(ctx context.Context, containerName, filePath string) (bool, error) {
|
func CheckFileExistsFromDocker(ctx context.Context, containerName, filePath string) (bool, error) {
|
||||||
// 上下文
|
// 上下文
|
||||||
// 创建 Docker 客户端
|
// 创建 Docker 客户端
|
||||||
@@ -598,7 +631,7 @@ func RegistWebTerminal(ctx context.Context) error {
|
|||||||
// 拉取镜像
|
// 拉取镜像
|
||||||
err = docker_module.PullImage(ctx, cli, dockerHost, setting.DevContainerConfig.Web_Terminal_Image)
|
err = docker_module.PullImage(ctx, cli, dockerHost, setting.DevContainerConfig.Web_Terminal_Image)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("拉取web_terminal镜像失败:%v", err)
|
fmt.Errorf("拉取web_terminal镜像失败:%v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
timestamp := time.Now().Format("20060102150405")
|
timestamp := time.Now().Format("20060102150405")
|
||||||
@@ -632,3 +665,36 @@ func RegistWebTerminal(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
return nil
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ type InstallForm struct {
|
|||||||
RequireSignInView bool
|
RequireSignInView bool
|
||||||
DefaultKeepEmailPrivate bool
|
DefaultKeepEmailPrivate bool
|
||||||
DefaultAllowCreateOrganization bool
|
DefaultAllowCreateOrganization bool
|
||||||
|
DefaultAllowCreateDevcontainer bool
|
||||||
DefaultEnableTimetracking bool
|
DefaultEnableTimetracking bool
|
||||||
EnableUpdateChecker bool
|
EnableUpdateChecker bool
|
||||||
NoReplyAddress string
|
NoReplyAddress string
|
||||||
|
|||||||
@@ -153,6 +153,8 @@
|
|||||||
<dd>{{svg (Iif .Service.DefaultKeepEmailPrivate "octicon-check" "octicon-x")}}</dd>
|
<dd>{{svg (Iif .Service.DefaultKeepEmailPrivate "octicon-check" "octicon-x")}}</dd>
|
||||||
<dt>{{ctx.Locale.Tr "admin.config.default_allow_create_organization"}}</dt>
|
<dt>{{ctx.Locale.Tr "admin.config.default_allow_create_organization"}}</dt>
|
||||||
<dd>{{svg (Iif .Service.DefaultAllowCreateOrganization "octicon-check" "octicon-x")}}</dd>
|
<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>
|
<dt>{{ctx.Locale.Tr "admin.config.enable_timetracking"}}</dt>
|
||||||
<dd>{{svg (Iif .Service.EnableTimetracking "octicon-check" "octicon-x")}}</dd>
|
<dd>{{svg (Iif .Service.EnableTimetracking "octicon-check" "octicon-x")}}</dd>
|
||||||
{{if .Service.EnableTimetracking}}
|
{{if .Service.EnableTimetracking}}
|
||||||
|
|||||||
@@ -304,6 +304,12 @@
|
|||||||
<input name="default_allow_create_organization" type="checkbox" {{if .default_allow_create_organization}}checked{{end}}>
|
<input name="default_allow_create_organization" type="checkbox" {{if .default_allow_create_organization}}checked{{end}}>
|
||||||
</div>
|
</div>
|
||||||
</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="inline field">
|
||||||
<div class="ui checkbox">
|
<div class="ui checkbox">
|
||||||
<label data-tooltip-content="{{ctx.Locale.Tr "install.default_enable_timetracking_popup"}}">{{ctx.Locale.Tr "install.default_enable_timetracking"}}</label>
|
<label data-tooltip-content="{{ctx.Locale.Tr "install.default_enable_timetracking_popup"}}">{{ctx.Locale.Tr "install.default_enable_timetracking"}}</label>
|
||||||
|
|||||||
@@ -14,6 +14,10 @@
|
|||||||
"echo \"postCreateCommand\"",
|
"echo \"postCreateCommand\"",
|
||||||
"echo \"OK\""
|
"echo \"OK\""
|
||||||
],
|
],
|
||||||
|
"postAttachCommand": [
|
||||||
|
"echo \"postAttachCommand\"",
|
||||||
|
"echo \"OK\""
|
||||||
|
],
|
||||||
"runArgs": [
|
"runArgs": [
|
||||||
"-p 8888"
|
"-p 8888"
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
{{else}}
|
{{else}}
|
||||||
|
|
||||||
<div class="ui container">
|
<div class="ui container">
|
||||||
|
|
||||||
<form class="ui edit form">
|
<form class="ui edit form">
|
||||||
<div class="repo-editor-header">
|
<div class="repo-editor-header">
|
||||||
<div class="ui breadcrumb field">
|
<div class="ui breadcrumb field">
|
||||||
@@ -36,7 +37,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
|
{{if and .ValidateDevContainerConfiguration .HasDevContainer}}
|
||||||
<iframe id="webTerminalContainer" src="{{.WebSSHUrl}}" width="100%" style="height: 100vh; display: none;" frameborder="0">您的浏览器不支持iframe</iframe>
|
<iframe id="webTerminalContainer" src="{{.WebSSHUrl}}" width="100%" style="height: 100vh; display: none;" frameborder="0">您的浏览器不支持iframe</iframe>
|
||||||
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
@@ -47,7 +50,7 @@
|
|||||||
<strong>{{ctx.Locale.Tr "repo.dev_container_control"}}</strong>
|
<strong>{{ctx.Locale.Tr "repo.dev_container_control"}}</strong>
|
||||||
<div class="ui relaxed list">
|
<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>
|
<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}}
|
{{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>
|
<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 style=" display: none;" id="createContainer" class="item">
|
||||||
<div>
|
<div>
|
||||||
<form method="get" action="{{.Repository.Link}}/devcontainer/create" class="ui edit form">
|
<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>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -84,6 +87,16 @@
|
|||||||
<!-- 结束Dev Container 正文内容 -->
|
<!-- 结束Dev Container 正文内容 -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- 自定义警告框 -->
|
||||||
|
<div id="customAlert" class="custom-alert">
|
||||||
|
<div class="alert-content">
|
||||||
|
<div class="alert-header">
|
||||||
|
<strong>提示信息</strong>
|
||||||
|
<button class="alert-close" onclick="closeCustomAlert()">×</button>
|
||||||
|
</div>
|
||||||
|
<div id="alertText" class="alert-body"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 确认删除 Dev Container 模态对话框 -->
|
<!-- 确认删除 Dev Container 模态对话框 -->
|
||||||
<div class="ui g-modal-confirm delete modal" id="delete-repo-devcontainer-of-user-modal">
|
<div class="ui g-modal-confirm delete modal" id="delete-repo-devcontainer-of-user-modal">
|
||||||
@@ -96,24 +109,14 @@
|
|||||||
</div>
|
</div>
|
||||||
{{template "base/modal_actions_confirm" .}}
|
{{template "base/modal_actions_confirm" .}}
|
||||||
</div>
|
</div>
|
||||||
<!-- 确认 Dev Container 模态对话框 -->
|
<!-- 保存 Dev Container 模态对话框 -->
|
||||||
<div class="ui g-modal-confirm delete modal" style="width: 35%" id="updatemodal">
|
<div class="ui g-modal-confirm delete modal" style="width: 35%" id="updatemodal">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
{{ctx.Locale.Tr "repo.dev_container_control.update"}}
|
{{ctx.Locale.Tr "repo.dev_container_control.update"}}
|
||||||
</div>
|
</div>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<form class="ui form tw-max-w-2xl tw-m-auto" id="updateForm" onsubmit="submitForm(event)">
|
<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 ">
|
<div class="required field ">
|
||||||
<label for="RepositoryAddress">Registry:</label>
|
<label for="RepositoryAddress">Registry:</label>
|
||||||
<input style="border: 1px solid black;" type="text" id="RepositoryAddress" name="RepositoryAddress" value="{{.RepositoryAddress}}">
|
<input style="border: 1px solid black;" type="text" id="RepositoryAddress" name="RepositoryAddress" value="{{.RepositoryAddress}}">
|
||||||
@@ -124,13 +127,38 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="required field ">
|
<div class="required field ">
|
||||||
<label for="RepositoryPassword">Registry Password:</label>
|
<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>
|
||||||
<div class="required field ">
|
<div class="required field ">
|
||||||
<label for="ImageName">Image(name:tag):</label>
|
<label for="ImageName">Image(name:tag):</label>
|
||||||
<input style="border: 1px solid black;" type="text" id="ImageName" name="ImageName" value="{{.ImageName}}">
|
<input style="border: 1px solid black;" type="text" id="ImageName" name="ImageName" value="{{.ImageName}}">
|
||||||
</div>
|
</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">
|
<div class="actions">
|
||||||
<button class="ui primary button" type="submit" id="updateSubmitButton" >Submit</button>
|
<button class="ui primary button" type="submit" id="updateSubmitButton" >Submit</button>
|
||||||
<button class="ui cancel button" id="updateCloseButton">Close</button>
|
<button class="ui cancel button" id="updateCloseButton">Close</button>
|
||||||
@@ -143,6 +171,21 @@
|
|||||||
|
|
||||||
|
|
||||||
<script>
|
<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 status = '-1'
|
||||||
var intervalID
|
var intervalID
|
||||||
const createContainer = document.getElementById('createContainer');
|
const createContainer = document.getElementById('createContainer');
|
||||||
@@ -233,13 +276,13 @@ function getStatus() {
|
|||||||
if(status !== '9' && status !== '-1' && data.status == '9'){
|
if(status !== '9' && status !== '-1' && data.status == '9'){
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
}
|
}
|
||||||
if(status !== '-1' && data.status == '-1'){
|
else if(status !== '-1' && data.status == '-1'){
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
}
|
}
|
||||||
if(status !== '4' && status !== '-1' && data.status == '4'){
|
else if(status !== '4' && status !== '-1' && data.status == '4'){
|
||||||
window.location.reload();
|
//window.location.reload();
|
||||||
}
|
}
|
||||||
if (data.status == '-1' || data.status == '') {
|
else if (data.status == '-1' || data.status == '') {
|
||||||
if (loadingElement) {
|
if (loadingElement) {
|
||||||
loadingElement.style.display = 'none';
|
loadingElement.style.display = 'none';
|
||||||
}
|
}
|
||||||
@@ -333,7 +376,7 @@ function getStatus() {
|
|||||||
console.error('Error:', error);
|
console.error('Error:', error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
intervalID = setInterval(getStatus, 3000);
|
intervalID = setInterval(getStatus, 5000);
|
||||||
if (restartContainer) {
|
if (restartContainer) {
|
||||||
restartContainer.addEventListener('click', function(event) {
|
restartContainer.addEventListener('click', function(event) {
|
||||||
// 处理点击逻辑
|
// 处理点击逻辑
|
||||||
@@ -342,7 +385,7 @@ if (restartContainer) {
|
|||||||
loadingElement.style.display = 'block';
|
loadingElement.style.display = 'block';
|
||||||
}
|
}
|
||||||
fetch('{{.Repository.Link}}' + '/devcontainer/restart')
|
fetch('{{.Repository.Link}}' + '/devcontainer/restart')
|
||||||
.then(response => {intervalID = setInterval(getStatus, 3000);})
|
.then(response => {intervalID = setInterval(getStatus, 5000);})
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (stopContainer) {
|
if (stopContainer) {
|
||||||
@@ -353,7 +396,7 @@ if (stopContainer) {
|
|||||||
}
|
}
|
||||||
// 处理点击逻辑
|
// 处理点击逻辑
|
||||||
fetch('{{.Repository.Link}}' + '/devcontainer/stop')
|
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) {
|
function submitForm(event) {
|
||||||
event.preventDefault(); // 阻止默认的表单提交行为
|
event.preventDefault(); // 阻止默认的表单提交行为
|
||||||
const {csrfToken} = window.config;
|
const {csrfToken} = window.config;
|
||||||
const {appSubUrl} = window.config;
|
const {appSubUrl} = window.config;
|
||||||
|
const formModal = document.getElementById('updatemodal');
|
||||||
const form = document.getElementById('updateForm');
|
const form = document.getElementById('updateForm');
|
||||||
const submitButton = document.getElementById('updateSubmitButton');
|
const submitButton = document.getElementById('updateSubmitButton');
|
||||||
const closeButton = document.getElementById('updateCloseButton');
|
const closeButton = document.getElementById('updateCloseButton');
|
||||||
@@ -390,9 +469,10 @@ function submitForm(event) {
|
|||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
submitButton.disabled = false;
|
submitButton.disabled = false;
|
||||||
alert(data.message);
|
formModal.classList.remove('is-loading')
|
||||||
|
showCustomAlert(data.message);
|
||||||
if(data.redirect){
|
if(data.redirect){
|
||||||
closeButton.click()
|
closeCustomAlert()
|
||||||
}
|
}
|
||||||
intervalID = setInterval(getStatus, 3000);
|
intervalID = setInterval(getStatus, 3000);
|
||||||
})
|
})
|
||||||
@@ -422,6 +502,69 @@ function submitForm(event) {
|
|||||||
0%{-webkit-transform:rotate(0deg)}
|
0%{-webkit-transform:rotate(0deg)}
|
||||||
100%{-webkit-transform:rotate(360deg)}
|
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>
|
</style>
|
||||||
{{template "base/footer" .}}
|
{{template "base/footer" .}}
|
||||||
|
|||||||
Reference in New Issue
Block a user