Files
devstar/routers/web/devcontainer/devcontainer.go
xinitx cd856c72bc !62 json管理和日志输出
* 合并输出按阶段显示
* ttyd初始目录
* 访问数据库放在services层
* 端口指定映射
* vscode链接
* 去掉devstar字符串
* Devcontainer前端页面显示进行了整理优化
* 修复 数据库 bug
* 增加容器output
2025-03-18 15:52:08 +00:00

295 lines
9.6 KiB
Go

package devcontainer
import (
"encoding/json"
"io"
"net/http"
"path"
"strings"
"code.gitea.io/gitea/models/db"
devcontainer_models "code.gitea.io/gitea/models/devcontainer"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/services/context"
devcontainer_service "code.gitea.io/gitea/services/devcontainer"
)
const (
tplGetRepoDevcontainerDetail base.TplName = "repo/devcontainer/details"
)
// 获取仓库 Dev Container 详细信息
// GET /{username}/{reponame}/dev-container
func GetRepoDevContainerDetails(ctx *context.Context) {
// 1. 查询当前 Repo 已有的 Dev Container 信息
opts := &devcontainer_service.RepoDevcontainerOptions{
Actor: ctx.Doer,
Repository: ctx.Repo.Repository,
}
var devContainerOutput []devcontainer_models.DevcontainerOutput
dbEngine := db.GetEngine(*ctx)
err := dbEngine.Table("devcontainer_output").
Where("user_id = ? AND repo_id = ?", ctx.Doer.ID, ctx.Repo.Repository.ID).
Find(&devContainerOutput)
ctx.Data["isCreatingDevcontainer"] = false
if err == nil && len(devContainerOutput) > 0 {
ctx.Data["isCreatingDevcontainer"] = true
}
var created bool = false
for _, item := range devContainerOutput {
if item.ListId > 0 && item.Status == "success" {
created = true
}
}
//ctx.Repo.RepoLink == ctx.Repo.Repository.Link()
devContainerMetadata, err := devcontainer_service.GetRepoDevcontainerDetails(ctx, opts)
hasDevContainer := err == nil && devContainerMetadata.DevContainerId > 0 && created
ctx.Data["HasDevContainer"] = hasDevContainer
ctx.Data["HasDevContainerSetting"] = false
if hasDevContainer {
ctx.Data["DevContainer"] = devContainerMetadata
}
// 2. 检查当前仓库的当前分支是否存在有效的 /.devcontainer/devcontainer.json
isValidRepoDevcontainerJson := isValidRepoDevcontainerJsonFile(ctx)
if !hasDevContainer && !isValidRepoDevcontainerJson {
ctx.Flash.Error(ctx.Tr("repo.dev_container_invalid_config_prompt"), true)
}
// 从devcontainer.json文件提取image字段解析成仓库地址、命名空间、镜像名
devcontainerJson, err := devcontainer_service.GetDevcontainerJsonModel(ctx, ctx.Repo.Repository)
if err == nil {
imageName := devcontainerJson.Image
registry, namespace, repo, tag := ParseImageName(imageName)
ctx.Data["RepositoryAddress"] = registry
ctx.Data["RepositoryUsername"] = namespace
ctx.Data["ImageName"] = ctx.Repo.Repository.Name + "-" + repo + ":" + tag
}
// 获取WebSSH服务端口
webTerminalURL, err := devcontainer_service.GetWebTerminalURL(ctx, devContainerMetadata.DevContainerName)
if err == nil {
ctx.Data["WebSSHUrl"] = webTerminalURL
}
vsCodeTerminalURL, err := devcontainer_service.GetVSCodeTerminalURL(ctx, &devContainerMetadata)
if err == nil {
ctx.Data["VSCodeUrl"] = vsCodeTerminalURL
log.Info(vsCodeTerminalURL)
}
//存在devcontainer.json读取信息展示
if isValidRepoDevcontainerJson {
fileContent, err := devcontainer_service.GetDevcontainerJsonString(ctx, ctx.Repo.Repository)
if err == nil {
ctx.Data["FileContent"] = fileContent
}
}
// 3. 携带数据渲染页面,返回
ctx.Data["Title"] = ctx.Locale.Tr("repo.dev_container")
ctx.Data["PageIsRepoDevcontainerDetails"] = true
ctx.Data["HasValidDevContainerJSON"] = isValidRepoDevcontainerJson
ctx.Data["Repository"] = ctx.Repo.Repository
ctx.Data["ContextUser"] = ctx.Doer
ctx.Data["CreateDevcontainerSettingUrl"] = "/" + ctx.Doer.Name + "/" + ctx.Repo.Repository.Name + "/dev-container/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.Repo.BranchNameSubURL()
ctx.HTML(http.StatusOK, tplGetRepoDevcontainerDetail)
}
func CreateRepoDevContainerConfiguration(ctx *context.Context) {
devcontainer_service.CreateDevcontainerJSON(ctx)
ctx.Redirect(path.Join(ctx.Repo.RepoLink, "/dev-container"))
}
func ParseImageName(imageName string) (registry, namespace, repo, tag string) {
// 分离仓库地址和命名空间
parts := strings.Split(imageName, "/")
if len(parts) == 3 {
registry = parts[0]
namespace = parts[1]
repo = parts[2]
} else if len(parts) == 2 {
registry = parts[0]
repo = parts[1]
} else {
repo = imageName
}
// 分离标签
parts = strings.Split(repo, ":")
if len(parts) > 1 {
tag = parts[1]
repo = parts[0]
} else {
tag = "latest"
}
if registry == "" {
registry = "docker.io"
}
return registry, namespace, repo, tag
}
// 创建仓库 Dev Container
func CreateRepoDevContainer(ctx *context.Context) {
if !isUserDevcontainerAlreadyInRepository(ctx) {
opts := &devcontainer_service.CreateRepoDevcontainerOptions{
Actor: ctx.Doer,
Repository: ctx.Repo.Repository,
}
devcontainer_service.CreateRepoDevcontainer(ctx, opts)
}
ctx.Redirect(ctx.Repo.RepoLink + "/dev-container")
}
// 辅助判断当前仓库的当前分支是否存在有效的 /.devcontainer/devcontainer.json
func isValidRepoDevcontainerJsonFile(ctx *context.Context) bool {
// 1. 仓库非空,且非 Archived 状态
if ctx.Repo.Repository.IsEmpty || ctx.Repo.Repository.IsArchived {
return false
}
// 2. 当前分支的目录 .devcontainer 下存在 devcontainer.json 文件
fileDevcontainerJsonExists, err := ctx.Repo.FileExists(".devcontainer/devcontainer.json", ctx.Repo.BranchName)
if err != nil || !fileDevcontainerJsonExists {
return false
}
// 3. TODO: DevContainer 格式正确
return true
}
// 辅助判断当前用户在当前仓库是否已有 Dev Container
func isUserDevcontainerAlreadyInRepository(ctx *context.Context) bool {
opts := &devcontainer_service.RepoDevcontainerOptions{
Actor: ctx.Doer,
Repository: ctx.Repo.Repository,
}
devcontainerDetails, _ := devcontainer_service.GetRepoDevcontainerDetails(ctx, opts)
return devcontainerDetails.DevContainerId > 0
}
func UpdateRepoDevContainerForCurrentActor(ctx *context.Context) {
opt := &devcontainer_service.RepoDevcontainerOptions{
Actor: ctx.Doer,
Repository: ctx.Repo.Repository,
}
devContainerMetadata, _ := devcontainer_service.GetRepoDevcontainerDetails(ctx, opt)
// 取得参数
body, _ := io.ReadAll(ctx.Req.Body)
log.Info(string(body))
var updateInfo devcontainer_service.UpdateInfo
err := json.Unmarshal(body, &updateInfo)
// 保存容器功能使用弹窗显示错误信息
if err != nil {
log.Info("保存容器参数反序列化失败:", err)
ctx.JSON(400, map[string]string{"message": "输入错误"})
return
}
opts := &devcontainer_service.UpdateDevcontainerOptions{
ImageName: updateInfo.ImageName,
PassWord: updateInfo.PassWord,
RepositoryAddress: updateInfo.RepositoryAddress,
RepositoryUsername: updateInfo.RepositoryUsername,
DevContainerName: devContainerMetadata.DevContainerName,
Actor: ctx.Doer,
Repository: ctx.Repo.Repository,
}
err = devcontainer_service.UpdateDevcontainerAPIService(ctx, opts)
if err != nil {
ctx.JSON(500, map[string]string{"message": err.Error()})
return
}
ctx.JSON(http.StatusOK, map[string]string{"redirect": ctx.Repo.RepoLink + "/dev-container", "message": "成功"})
}
// 删除仓库 当前用户 Dev Container
func DeleteRepoDevContainerForCurrentActor(ctx *context.Context) {
if isUserDevcontainerAlreadyInRepository(ctx) {
opts := &devcontainer_service.RepoDevcontainerOptions{
Actor: ctx.Doer,
Repository: ctx.Repo.Repository,
}
err := devcontainer_service.DeleteRepoDevcontainer(ctx, opts)
if err != nil {
log.Warn("failed to delete devContainer with option{%v}: %v", opts, err.Error())
ctx.Flash.Error(ctx.Tr("repo.dev_container_control.deletion_failed_for_user", ctx.Doer.Name))
} else {
ctx.Flash.Success(ctx.Tr("repo.dev_container_control.deletion_success_for_user", ctx.Doer.Name))
}
}
ctx.JSONRedirect(ctx.Repo.RepoLink + "/dev-container")
}
type OutputResponse struct {
CurrentJob struct {
Title string `json:"title"`
Detail string `json:"detail"`
Steps []*ViewJobStep `json:"steps"`
} `json:"currentDevcontainer"`
}
type ViewJobStep struct {
Summary string `json:"summary"`
Duration string `json:"duration"`
Status string `json:"status"`
Logs []ViewStepLogLine `json:"logs"`
}
type ViewStepLogLine struct {
Index int64 `json:"index"`
Message string `json:"message"`
Timestamp float64 `json:"timestamp"`
}
func GetContainerOutput(ctx *context.Context) {
var devContainerOutput []devcontainer_models.DevcontainerOutput
dbEngine := db.GetEngine(*ctx)
err := dbEngine.Table("devcontainer_output").
Where("user_id = ? AND repo_id = ?", ctx.Doer.ID, ctx.Repo.Repository.ID).
Find(&devContainerOutput)
if err != nil {
ctx.Error(http.StatusInternalServerError, err.Error())
return
}
if len(devContainerOutput) > 0 {
resp := &OutputResponse{}
resp.CurrentJob.Title = ctx.Repo.Repository.Name + " Devcontainer Info"
resp.CurrentJob.Detail = "success"
for _, item := range devContainerOutput {
if item.Status != "success" {
resp.CurrentJob.Detail = "running"
if devContainerOutput[0].Status == "success" && devContainerOutput[1].Status == "success" {
resp.CurrentJob.Detail = "created"
}
}
logLines := []ViewStepLogLine{}
logLines = append(logLines, ViewStepLogLine{
Index: 1,
Message: item.Output,
})
resp.CurrentJob.Steps = append(resp.CurrentJob.Steps, &ViewJobStep{
Summary: item.Command,
Status: item.Status,
Logs: logLines,
})
}
ctx.JSON(http.StatusOK, resp)
return
}
ctx.Done()
}