* 合并输出按阶段显示 * ttyd初始目录 * 访问数据库放在services层 * 端口指定映射 * vscode链接 * 去掉devstar字符串 * Devcontainer前端页面显示进行了整理优化 * 修复 数据库 bug * 增加容器output
295 lines
9.6 KiB
Go
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()
|
|
}
|