471 lines
16 KiB
Go
471 lines
16 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/modules/web/middleware"
|
||
"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 {
|
||
// 保存当前 URL 到 cookie,以便登录后跳转回来
|
||
if ctx.Req.URL.Path != "/user/events" {
|
||
middleware.SetRedirectToCookie(ctx.Resp, setting.AppSubURL+ctx.Req.URL.RequestURI())
|
||
}
|
||
// 跳转到登录页面
|
||
ctx.Redirect(setting.AppSubURL + string(setting.LandingPageLogin))
|
||
return
|
||
}
|
||
var err error
|
||
log.Info("setting.CustomConf %s", setting.CustomConf)
|
||
log.Info("cfg.Section().Key().Value() %s", setting.AppURL)
|
||
|
||
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"], ctx.Data["DockerfilePath"], err = devcontainer_service.HasDevContainerDockerFile(ctx, ctx.Repo)
|
||
if err != nil {
|
||
log.Info(err.Error())
|
||
ctx.Flash.Error(err.Error(), true)
|
||
}
|
||
|
||
// 列出所有可用的Dockerfile文件,用于下拉菜单
|
||
availableDockerfiles, err := devcontainer_service.ListDockerfilesInDevcontainer(ctx, ctx.Repo)
|
||
if err != nil {
|
||
log.Info("Failed to list dockerfiles: %v", err)
|
||
// 不阻断流程,只是记录日志
|
||
availableDockerfiles = []string{}
|
||
}
|
||
ctx.Data["AvailableDockerfiles"] = availableDockerfiles
|
||
|
||
// 获取当前使用的Dockerfile文件名(不含路径)
|
||
currentDockerfileName := ""
|
||
if ctx.Data["HasDevContainerDockerfile"] == true {
|
||
currentDockerfileName, err = devcontainer_service.GetCurrentDockerfileName(ctx, ctx.Repo)
|
||
if err != nil {
|
||
log.Info("Failed to get current dockerfile name: %v", err)
|
||
}
|
||
}
|
||
ctx.Data["CurrentDockerfileName"] = currentDockerfileName
|
||
|
||
// 从 ROOT_URL 解析平台地址,用于生成 Docker 镜像名称
|
||
rootURL := setting.AppURL
|
||
platformAddress := devcontainer_service.GetPlatformAddressFromRootURL(rootURL)
|
||
ctx.Data["PlatformAddress"] = platformAddress
|
||
|
||
if ctx.Data["HasDevContainer"] == true {
|
||
if ctx.Data["HasDevContainerConfiguration"] == true {
|
||
// ImageName 由前端 JavaScript 自动生成,匹配对应dockerfile的tag
|
||
}
|
||
if setting.K8sConfig.Enable {
|
||
// 获取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 := setting.DevContainerConfig.Web_Terminal_Container
|
||
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(setting.AppURL)
|
||
if err != nil {
|
||
ctx.Flash.Error(err.Error(), true)
|
||
}
|
||
terminalParams := "user=" +
|
||
ctx.Repo.Owner.Name +
|
||
"&repo=" +
|
||
ctx.Repo.Repository.Name +
|
||
"&repoid=" +
|
||
strconv.FormatInt(ctx.Repo.Repository.ID, 10) +
|
||
"&userid=" +
|
||
strconv.FormatInt(ctx.Doer.ID, 10) +
|
||
"&domain=" +
|
||
setting.Domain +
|
||
"&port=" +
|
||
rootPort
|
||
port, err := devcontainer_service.GetMappedPort(ctx, webTerminalContainerName, "7681")
|
||
webTerminalURL, err := devcontainer_service.ReplacePortOfUrl(setting.AppURL, 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 {
|
||
// Get configured IDE apps dynamically
|
||
devContainerApps := setting.Config().Repository.DevContainerEditorApps.Value(ctx)
|
||
ideURLs := make(map[string]string)
|
||
for _, app := range devContainerApps {
|
||
url := app.Protocol + terminalURL
|
||
ideURLs[app.DisplayName] = url
|
||
}
|
||
ctx.Data["DevContainerIDEs"] = ideURLs
|
||
}
|
||
}
|
||
// 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"
|
||
// 获取 devcontainer.json 的实际路径(.devstar 优先)
|
||
configPath, exists, err := devcontainer_service.GetDevcontainerPath(ctx, ctx.Repo.Repository)
|
||
var baseDir string
|
||
if err == nil && exists {
|
||
ctx.Data["EditDevcontainerConfigurationUrl"] = ctx.Repo.RepoLink + "/_edit/" + ctx.Repo.Repository.DefaultBranch + "/" + configPath
|
||
baseDir = devcontainer_service.ExtractDevcontainerDirFromPath(configPath)
|
||
if baseDir == "" {
|
||
// 使用 .devstar 作为默认值
|
||
baseDir = devcontainer_service.DevstarDevcontainerDir
|
||
}
|
||
ctx.Data["TreeNames"] = []string{baseDir, "devcontainer.json"}
|
||
ctx.Data["TreePaths"] = []string{baseDir, configPath}
|
||
} else {
|
||
// 文件不存在:新建时使用 .devstar(优先级)
|
||
baseDir = devcontainer_service.DevstarDevcontainerDir
|
||
defaultConfigPath := devcontainer_service.DevstarDevcontainerPath
|
||
ctx.Data["EditDevcontainerConfigurationUrl"] = ctx.Repo.RepoLink + "/_edit/" + ctx.Repo.Repository.DefaultBranch + "/" + defaultConfigPath
|
||
ctx.Data["TreeNames"] = []string{baseDir, "devcontainer.json"}
|
||
ctx.Data["TreePaths"] = []string{baseDir, defaultConfigPath}
|
||
}
|
||
// 传递 devcontainer 目录路径给模板,用于前端构建 Dockerfile 链接
|
||
ctx.Data["DevcontainerDir"] = baseDir
|
||
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)
|
||
}
|
||
|
||
ideURLs := make(map[string]string)
|
||
// 增加对 ctx.Doer、ctx.Repo、ctx.Repo.Repository 的检查,避免出现空指针异常
|
||
// 只在第一次状态变为 "5" 时生成 URL,避免频繁调用导致 token 被删除,随 Session 过期(默认 24 小时)
|
||
// 通过检查 session 中是否已有 terminal_url 来判断是否是第一次
|
||
if realTimeStatus == "5" && ctx.Doer != nil && ctx.Repo != nil && ctx.Repo.Repository != nil {
|
||
// 检查 session 中是否已有缓存的 URL
|
||
cachedUrls := ctx.Session.Get("terminal_urls")
|
||
if cachedUrls == nil {
|
||
// 第一次状态为 "5",生成 URL 并缓存
|
||
terminalURL, err := devcontainer_service.Get_IDE_TerminalURL(ctx, ctx.Doer, ctx.Repo)
|
||
if err == nil {
|
||
// Get configured IDE apps dynamically
|
||
devContainerApps := setting.Config().Repository.DevContainerEditorApps.Value(ctx)
|
||
for _, app := range devContainerApps {
|
||
url := app.Protocol + terminalURL
|
||
ideURLs[app.DisplayName] = url
|
||
}
|
||
// 缓存 URL 到 session
|
||
ctx.Session.Set("terminal_urls", ideURLs)
|
||
}
|
||
} else {
|
||
// 使用缓存的 URL
|
||
urls := cachedUrls.(map[string]string)
|
||
ideURLs = urls
|
||
}
|
||
} else {
|
||
// 状态不是 "5" 或 ctx.Doer 为 nil,清除缓存的 URL
|
||
ctx.Session.Delete("terminal_urls")
|
||
}
|
||
|
||
ctx.JSON(http.StatusOK, map[string]any{
|
||
"status": realTimeStatus,
|
||
"ideURLs": ideURLs,
|
||
})
|
||
}
|
||
|
||
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, 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, &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, "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", "*")
|
||
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, nil)
|
||
return
|
||
}
|
||
ctx.JSON(http.StatusOK, output)
|
||
}
|
||
|
||
// GetDevContainerLogs 内存中实时获取容器创建日志
|
||
func GetDevContainerLogs(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()
|
||
logs, err := devcontainer_service.GetDevContainerLogs(ctx, query.Get("user"), ctx.Repo.Repository)
|
||
if err != nil {
|
||
log.Info("GetDevContainerLogs error: %v", err)
|
||
ctx.JSON(http.StatusOK, &devcontainer_service.OutputResponse{
|
||
CurrentJob: struct {
|
||
State string `json:"state"`
|
||
Steps []*devcontainer_service.ViewJobStep `json:"steps"`
|
||
}{
|
||
State: "-1",
|
||
Steps: []*devcontainer_service.ViewJobStep{},
|
||
},
|
||
})
|
||
return
|
||
}
|
||
ctx.JSON(http.StatusOK, logs)
|
||
}
|
||
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, "")
|
||
}
|