Files
devstar/routers/web/devcontainer/devcontainer.go
xinitx 9071a754f4 !108 给devcontainer增加变量和脚本功能
给devcontainer增加变量和脚本功能

- 能从devstar.cn上获取预定义的DEVSTAR_开头的变量或脚本
- 添加到脚本管理中的变量名,在devcontainer启动时会自动执行,然后才执行devcontainer.json中用户自定义脚本,其中可以调用设置的变量或脚本
- 变量或脚本在用户设置、项目设置和后台管理中都可以添加,如有重名优先级为:用户设置 > 项目设置 > 后台管理
2025-10-18 08:53:50 +00:00

336 lines
12 KiB
Go

package devcontainer
import (
"encoding/json"
"fmt"
"io"
"net/http"
"path"
"strconv"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/services/context"
devcontainer_service "code.gitea.io/gitea/services/devcontainer"
)
const (
tplGetDevContainerDetails templates.TplName = "repo/devcontainer/details"
)
// 获取仓库 Dev Container 详细信息
// GET /{username}/{reponame}/devcontainer
func GetDevContainerDetails(ctx *context.Context) {
if ctx.Doer == nil {
ctx.HTML(http.StatusForbidden, "")
return
}
var err error
cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf)
log.Info("setting.CustomConf %s", setting.CustomConf)
log.Info("cfg.Section().Key().Value() %s", cfg.Section("server").Key("ROOT_URL").Value())
if err != nil {
ctx.Flash.Error(err.Error(), true)
}
ctx.Data["isAdmin"], err = devcontainer_service.IsAdmin(ctx, ctx.Doer, ctx.Repo.Repository.ID)
if err != nil {
log.Info(err.Error())
ctx.Flash.Error(err.Error(), true)
}
ctx.Data["HasDevContainer"], err = devcontainer_service.HasDevContainer(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID)
if err != nil {
log.Info(err.Error())
ctx.Flash.Error(err.Error(), true)
}
ctx.Data["ValidateDevContainerConfiguration"] = true
ctx.Data["HasDevContainerConfiguration"], err = devcontainer_service.HasDevContainerConfiguration(ctx, ctx.Repo)
if err != nil {
log.Info(err.Error())
ctx.Data["ValidateDevContainerConfiguration"] = false
ctx.Flash.Error(err.Error(), true)
}
if ctx.Data["HasDevContainerConfiguration"] == false {
ctx.Data["ValidateDevContainerConfiguration"] = false
}
ctx.Data["HasDevContainerDockerfile"], 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 cfg.Section("k8s").Key("ENABLE").Value() == "true" {
// 获取WebSSH服务端口
webTerminalURL, err := devcontainer_service.GetWebTerminalURL(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID)
if err != nil {
log.Info(err.Error())
ctx.Flash.Error(err.Error(), true)
} else {
ctx.Data["WebSSHUrl"] = webTerminalURL
}
} else {
webTerminalContainerName := cfg.Section("devcontainer").Key("WEB_TERMINAL_CONTAINER").Value()
isWebTerminalNotFound, err := devcontainer_service.IsContainerNotFound(ctx, webTerminalContainerName)
if err != nil {
ctx.Flash.Error(err.Error(), true)
}
var webTerminalStatus string
if !isWebTerminalNotFound {
webTerminalStatus, err = devcontainer_service.GetDevContainerStatusFromDocker(ctx, webTerminalContainerName)
if err != nil {
ctx.Flash.Error(err.Error(), true)
}
}
if webTerminalContainerName == "" || isWebTerminalNotFound {
ctx.Flash.Error("webTerminal do not exist. creating ....", true)
err = devcontainer_service.RegistWebTerminal(ctx)
if err != nil {
ctx.Flash.Error(err.Error(), true)
}
ctx.Redirect(path.Join(ctx.Repo.RepoLink, "/devcontainer"))
} else if webTerminalStatus != "running" && webTerminalStatus != "restarting" {
err = devcontainer_service.DeleteDevContainerByDocker(ctx, webTerminalContainerName)
if err != nil {
ctx.Flash.Error(err.Error(), true)
}
err = devcontainer_service.RegistWebTerminal(ctx)
if err != nil {
ctx.Flash.Error(err.Error(), true)
}
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)
}
terminalParams := "user=" +
ctx.Doer.Name +
"&repo=" +
ctx.Repo.Repository.Name +
"&repoid=" +
strconv.FormatInt(ctx.Repo.Repository.ID, 10) +
"&userid=" +
strconv.FormatInt(ctx.Doer.ID, 10) +
"&domain=" +
cfg.Section("server").Key("DOMAIN").Value() +
"&port=" +
rootPort
port, err := devcontainer_service.GetMappedPort(ctx, webTerminalContainerName, "7681")
webTerminalURL, err := devcontainer_service.ReplacePortOfUrl(cfg.Section("server").Key("ROOT_URL").Value(), fmt.Sprintf("%d", port))
if err != nil {
log.Info(err.Error())
ctx.Flash.Error(err.Error(), true)
}
ctx.Data["WebSSHUrl"] = webTerminalURL + "?type=docker&" + terminalParams
}
}
terminalURL, err := devcontainer_service.Get_IDE_TerminalURL(ctx, ctx.Doer, ctx.Repo)
if err == nil {
ctx.Data["VSCodeUrl"] = "vscode" + terminalURL
ctx.Data["CursorUrl"] = "cursor" + terminalURL
ctx.Data["WindsurfUrl"] = "windsurf" + terminalURL
}
}
// 3. 携带数据渲染页面,返回
ctx.Data["Title"] = ctx.Locale.Tr("repo.dev_container")
ctx.Data["PageIsDevContainer"] = true
ctx.Data["Repository"] = ctx.Repo.Repository
ctx.Data["CreateDevcontainerSettingUrl"] = "/" + ctx.ContextUser.Name + "/" + ctx.Repo.Repository.Name + "/devcontainer/createConfiguration"
ctx.Data["EditDevcontainerConfigurationUrl"] = ctx.Repo.RepoLink + "/_edit/" + ctx.Repo.Repository.DefaultBranch + "/.devcontainer/devcontainer.json"
ctx.Data["TreeNames"] = []string{".devcontainer", "devcontainer.json"}
ctx.Data["TreePaths"] = []string{".devcontainer", ".devcontainer/devcontainer.json"}
ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src"
ctx.Data["SaveMethods"] = []string{"Container", "DockerFile"}
ctx.Data["SaveMethod"] = "Container"
ctx.HTML(http.StatusOK, tplGetDevContainerDetails)
}
func GetDevContainerStatus(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", "*")
var userID string
if ctx.Doer != nil {
userID = fmt.Sprintf("%d", ctx.Doer.ID)
} else {
query := ctx.Req.URL.Query()
userID = query.Get("user")
}
realTimeStatus, err := devcontainer_service.GetDevContainerStatus(ctx, userID, fmt.Sprintf("%d", ctx.Repo.Repository.ID))
if err != nil {
log.Info("%v\n", err)
}
ctx.JSON(http.StatusOK, map[string]string{"status": realTimeStatus})
}
func CreateDevContainerConfiguration(ctx *context.Context) {
hasDevContainerConfiguration, err := devcontainer_service.HasDevContainerConfiguration(ctx, ctx.Repo)
if err != nil {
log.Info(err.Error())
ctx.Flash.Error(err.Error(), true)
ctx.Redirect(path.Join(ctx.Repo.RepoLink, "/devcontainer"))
return
}
if hasDevContainerConfiguration {
ctx.Flash.Error("Already exist", true)
ctx.Redirect(path.Join(ctx.Repo.RepoLink, "/devcontainer"))
return
}
isAdmin, err := devcontainer_service.IsAdmin(ctx, ctx.Doer, ctx.Repo.Repository.ID)
if err != nil {
log.Info(err.Error())
ctx.Flash.Error(err.Error(), true)
ctx.Redirect(path.Join(ctx.Repo.RepoLink, "/devcontainer"))
return
}
if !isAdmin {
ctx.Flash.Error("permisson denied", true)
ctx.Redirect(path.Join(ctx.Repo.RepoLink, "/devcontainer"))
return
}
err = devcontainer_service.CreateDevcontainerConfiguration(ctx.Repo.Repository, ctx.Doer)
if err != nil {
log.Info(err.Error())
ctx.Flash.Error(err.Error(), true)
ctx.Redirect(path.Join(ctx.Repo.RepoLink, "/devcontainer"))
return
}
ctx.Redirect(path.Join(ctx.Repo.RepoLink, "/devcontainer"))
}
func CreateDevContainer(ctx *context.Context) {
hasDevContainer, err := devcontainer_service.HasDevContainer(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID)
if err != nil {
log.Info(err.Error())
ctx.Flash.Error(err.Error(), true)
return
}
if hasDevContainer {
ctx.Flash.Error("Already exist", true)
return
}
err = devcontainer_service.CreateDevcontainerAPIService(ctx, ctx.Repo.Repository, ctx.Doer, []string{}, true)
if err != nil {
ctx.Flash.Error(err.Error(), true)
}
ctx.Redirect(path.Join(ctx.Repo.RepoLink, "/devcontainer"))
}
func DeleteDevContainer(ctx *context.Context) {
hasDevContainer, err := devcontainer_service.HasDevContainer(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID)
if err != nil {
log.Info(err.Error())
ctx.Flash.Error(err.Error(), true)
return
}
if !hasDevContainer {
ctx.Flash.Error("Already Deleted.", true)
return
}
err = devcontainer_service.DeleteDevContainer(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID)
if err != nil {
ctx.Flash.Error(err.Error(), true)
}
ctx.Redirect(path.Join(ctx.Repo.RepoLink, "/devcontainer"))
}
func RestartDevContainer(ctx *context.Context) {
hasDevContainer, err := devcontainer_service.HasDevContainer(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID)
if err != nil {
log.Info(err.Error())
ctx.Flash.Error(err.Error(), true)
return
}
if !hasDevContainer {
log.Info(err.Error())
ctx.Flash.Error("Already delete", true)
return
}
err = devcontainer_service.RestartDevContainer(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID)
if err != nil {
ctx.Flash.Error(err.Error(), true)
}
ctx.JSON(http.StatusOK, map[string]string{"status": "6"})
}
func StopDevContainer(ctx *context.Context) {
hasDevContainer, err := devcontainer_service.HasDevContainer(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID)
if err != nil {
log.Info(err.Error())
ctx.Flash.Error(err.Error(), true)
return
}
if !hasDevContainer {
ctx.Flash.Error("Already delete", true)
return
}
err = devcontainer_service.StopDevContainer(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID)
if err != nil {
ctx.Flash.Error(err.Error(), true)
}
ctx.JSON(http.StatusOK, map[string]string{"status": "7"})
}
func UpdateDevContainer(ctx *context.Context) {
hasDevContainer, err := devcontainer_service.HasDevContainer(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID)
if err != nil {
log.Info(err.Error())
ctx.JSON(http.StatusOK, map[string]string{"message": err.Error()})
return
}
if !hasDevContainer {
ctx.JSON(http.StatusOK, map[string]string{"message": "Already delete"})
return
}
// 取得参数
body, _ := io.ReadAll(ctx.Req.Body)
var updateInfo devcontainer_service.UpdateInfo
err = json.Unmarshal(body, &updateInfo)
if err != nil {
ctx.JSON(http.StatusOK, map[string]string{"message": err.Error()})
return
}
err = devcontainer_service.UpdateDevContainer(ctx, ctx.Doer, ctx.Repo.Repository, &updateInfo)
if err != nil {
ctx.JSON(http.StatusOK, map[string]string{"message": err.Error()})
return
}
ctx.JSON(http.StatusOK, map[string]string{"redirect": ctx.Repo.RepoLink + "/devcontainer", "message": "成功"})
}
func GetTerminalCommand(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", "*")
query := ctx.Req.URL.Query()
cmd, status, err := devcontainer_service.GetTerminalCommand(ctx, query.Get("user"), ctx.Repo.Repository)
if err != nil {
log.Info(err.Error())
status = "error"
}
ctx.JSON(http.StatusOK, map[string]string{"command": cmd, "status": status})
}
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)
if err != nil {
log.Info(err.Error())
}
ctx.JSON(http.StatusOK, output)
}