Files
devstar/routers/web/devcontainer/devcontainer.go

471 lines
16 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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, "")
}