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

368 lines
10 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 (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"regexp"
"strconv"
"code.gitea.io/gitea/models/db"
devcontainer_model "code.gitea.io/gitea/models/devcontainer"
"code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/charset"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/typesniffer"
"code.gitea.io/gitea/modules/util"
gitea_context "code.gitea.io/gitea/services/context"
files_service "code.gitea.io/gitea/services/repository/files"
"github.com/docker/go-connections/nat"
)
type DevStarJSON struct {
ForwardPorts nat.PortSet
ContainerEnv []string
Image string
InitializeCommand []string
PostCreateCommand []string
RunArgs []string
}
func CreateDevcontainerJSON(ctx *gitea_context.Context) {
jsonString := `{
"image":"mcr.microsoft.com/devcontainers/base:dev-ubuntu-20.04",
"forwardPorts": [
{
"containerPort": 8080,
"protocol": "tcp"
}
],
"containerEnv": {
"NODE_ENV": "development"
},
"initializeCommand": "echo \"init\";",
"postCreateCommand": [
"echo \"created\";",
"echo \"test\";"
],
"runArgs": [
"-p",
"8888:8888"
]
}`
resp, err := files_service.ChangeRepoFiles(db.DefaultContext, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{
Files: []*files_service.ChangeRepoFile{
{
Operation: "create",
TreePath: ".devcontainer/devcontainer.json",
ContentReader: bytes.NewReader([]byte(jsonString)),
},
},
OldBranch: "main",
NewBranch: "main",
Message: "add container configuration",
})
log.Info(resp.Commit.URL)
if err != nil {
log.Info("error ChangeRepoFiles:", err)
ctx.JSON(500, map[string]string{
"message": "error ChangeRepoFiles"})
}
}
func UpdateDevcontainerJSON(ctx *gitea_context.Context, jsonString string) error {
// 更新devcontainer.json配置文件
_, err := files_service.ChangeRepoFiles(db.DefaultContext, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{
Files: []*files_service.ChangeRepoFile{
{
Operation: "update",
TreePath: ".devcontainer/devcontainer.json",
ContentReader: bytes.NewReader([]byte(jsonString)),
},
},
OldBranch: ctx.Repo.Repository.DefaultBranch,
Message: "Update container",
})
if err != nil {
return fmt.Errorf("更新devcontainer.json配置文件失败 %v", err)
}
return nil
}
// GetImageFromDevcontainerJSON 从 .devcontainer/devcontainer.json 中获取 devContainer image
func GetDevcontainerJsonModel(ctx context.Context, repo *repo.Repository) (*DevStarJSON, error) {
devcontainerJSONContent, err := GetDevcontainerJsonString(ctx, repo)
var devContainerJson *devcontainer_model.DevContainerJSON
if err != nil {
return nil, err
}
// 1. 移除注释
cleanedContent, err := removeComments(devcontainerJSONContent)
if err != nil {
log.Error("Failed to remove comments from .devcontainer/devcontainer.json: %v", err)
return nil, err
}
// 2. 解析 JSON
devContainerJson, err = devcontainer_model.Unmarshal(cleanedContent)
if err != nil {
log.Error("Failed to unmarshal .devcontainer/devcontainer.json: %v", err)
return nil, err
}
log.Info("%v", devContainerJson)
devStarJson := &DevStarJSON{
Image: devContainerJson.Image,
ContainerEnv: ConvertContainerEnv(devContainerJson.ContainerEnv),
}
devStarJson.ForwardPorts, _ = ConvertForwardPorts(devContainerJson.ForwardPorts)
devStarJson.InitializeCommand, _ = parseCommand(devContainerJson.InitializeCommand)
c1, _ := parseCommand(devContainerJson.OnCreateCommand)
c2, _ := parseCommand(devContainerJson.UpdateContentCommand)
c3, _ := parseCommand(devContainerJson.PostCreateCommand)
c4, _ := parseCommand(devContainerJson.PostStartCommand)
c5, _ := parseCommand(devContainerJson.PostAttachCommand)
devStarJson.PostCreateCommand = append(devStarJson.PostCreateCommand, c1...)
devStarJson.PostCreateCommand = append(devStarJson.PostCreateCommand, c2...)
devStarJson.PostCreateCommand = append(devStarJson.PostCreateCommand, c3...)
devStarJson.PostCreateCommand = append(devStarJson.PostCreateCommand, c4...)
devStarJson.PostCreateCommand = append(devStarJson.PostCreateCommand, c5...)
devStarJson.RunArgs = devContainerJson.RunArgs
log.Info("%v", devStarJson)
return devStarJson, nil
}
func GetDevcontainerJsonString(ctx context.Context, repo *repo.Repository) (string, error) {
// 1. 获取默认分支名
branchName := repo.DefaultBranch
if len(branchName) == 0 {
branchName = setting.Devcontainer.DefaultGitBranchName
}
// 2. 打开默认分支
gitRepoEntity, err := git.OpenRepository(ctx, repo.RepoPath())
if err != nil {
log.Error("Failed to open repository %s: %v", repo.RepoPath(), err)
return "", err
}
defer func(gitRepoEntity *git.Repository) {
_ = gitRepoEntity.Close()
}(gitRepoEntity)
// 3. 获取分支名称
commit, err := gitRepoEntity.GetBranchCommit(branchName)
if err != nil {
return "", err
}
entry, err := commit.GetTreeEntryByPath(".devcontainer/devcontainer.json")
if err != nil {
log.Info("Repo.Commit.GetTreeEntryByPath %v", err.Error())
return "", err
}
// No way to edit a directory online.
if entry.IsDir() {
return "", fmt.Errorf(".devcontainer/devcontainer.json entry.IsDir")
}
blob := entry.Blob()
if blob.Size() >= setting.UI.MaxDisplayFileSize {
return "", fmt.Errorf(".devcontainer/devcontainer.json blob.Size overflow")
}
dataRc, err := blob.DataAsync()
if err != nil {
return "", err
}
defer dataRc.Close()
buf := make([]byte, 1024)
n, _ := util.ReadAtMost(dataRc, buf)
buf = buf[:n]
// Only some file types are editable online as text.
if !typesniffer.DetectContentType(buf).IsRepresentableAsText() {
return "", fmt.Errorf("typesniffer.IsRepresentableAsText")
}
d, _ := io.ReadAll(dataRc)
buf = append(buf, d...)
if content, err := charset.ToUTF8(buf, charset.ConvertOpts{KeepBOM: true}); err != nil {
log.Error("ToUTF8: %v", err)
return string(buf), nil
} else {
return content, nil
}
}
// 移除 JSON 文件中的注释
func removeComments(data string) (string, error) {
// 移除单行注释 // ...
re := regexp.MustCompile(`//.*`)
data = re.ReplaceAllString(data, "")
// 移除多行注释 /* ... */
re = regexp.MustCompile(`/\*.*?\*/`)
data = re.ReplaceAllString(data, "")
return data, nil
}
func ConvertContainerEnv(envMap map[string]string) []string {
var envSlice []string
for key, value := range envMap {
envSlice = append(envSlice, fmt.Sprintf("%s=%s", key, value))
}
return envSlice
}
func parseCommand(cmd interface{}) ([]string, error) {
var result []string
switch v := cmd.(type) {
case string: // 直接处理字符串类型
result = []string{v}
case []interface{}: // 处理数组类型(如 ["echo","hello"]
var parts []string
for _, item := range v {
// 处理数组元素可能是多种类型的情况
switch elem := item.(type) {
case string:
parts = append(parts, elem)
case int, float64: // 处理数字自动转换的情况
parts = append(parts, fmt.Sprintf("%v", elem))
default:
return []string{}, fmt.Errorf("unsupported array element type: %T", elem)
}
}
result = parts
case map[string]interface{}: // 处理对象类型(如 {"command":"echo","args":["hello"]}
// 方式1序列化成JSON字符串
jsonBytes, err := json.Marshal(v)
if err != nil {
return []string{}, fmt.Errorf("failed to marshal object: %v", err)
}
result = []string{string(jsonBytes)}
// 方式2或拼接成 key=value 格式(根据需求选择)
// var pairs []string
// for key, val := range v {
// pairs = append(pairs, fmt.Sprintf("%s=%v", key, val))
// }
// result = strings.Join(pairs, " ")
default:
return []string{}, fmt.Errorf("unsupported command type: %T", v)
}
return result, nil
}
// 转换入口函数
func ConvertForwardPorts(input interface{}) (nat.PortSet, error) {
portSet := make(nat.PortSet)
// 类型检查确保是切片
rawPorts, ok := input.([]interface{})
if !ok {
return nil, fmt.Errorf("forwardPorts must be an array")
}
// 遍历每个端口定义
for _, rawPort := range rawPorts {
port, proto, err := parsePortDefinition(rawPort)
if err != nil {
return nil, err
}
// 构建 nat.Port 并加入集合
natPort := nat.Port(fmt.Sprintf("%s/%s", port, proto))
portSet[natPort] = struct{}{}
}
return portSet, nil
}
// 处理三种可能的端口定义形式:
// 1. 数字: 3000
// 2. 字符串: "3000" 或 "3000/udp"
// 3. 对象: {"containerPort": 3000, "protocol": "tcp"}
func parsePortDefinition(raw interface{}) (string, string, error) {
switch v := raw.(type) {
case float64: // JSON 数字默认解析为 float64
return validatePort(strconv.Itoa(int(v))), "tcp", nil
case string:
return parsePortString(v)
case map[string]interface{}:
return parsePortObject(v)
default:
return "", "", fmt.Errorf("invalid port type: %T", raw)
}
}
// 处理字符串形式(支持带协议)
func parsePortString(s string) (string, string, error) {
proto, port := nat.SplitProtoPort(s)
if port == "" {
return "", "", fmt.Errorf("invalid port string: %s", s)
}
return validatePort(port), proto, nil
}
// 处理对象形式
func parsePortObject(obj map[string]interface{}) (string, string, error) {
// 提取 containerPort
rawPort, exists := obj["containerPort"]
if !exists {
return "", "", fmt.Errorf("missing containerPort field")
}
// 转换端口值
var portStr string
switch p := rawPort.(type) {
case float64:
portStr = strconv.Itoa(int(p))
case string:
portStr = p
default:
return "", "", fmt.Errorf("invalid containerPort type: %T", rawPort)
}
// 验证端口格式
portStr = validatePort(portStr)
// 获取协议(默认为 tcp
proto := "tcp"
if rawProto, exists := obj["protocol"]; exists {
if protoStr, ok := rawProto.(string); ok {
proto = protoStr
}
}
return portStr, proto, nil
}
// 验证端口格式有效性
func validatePort(port string) string {
// 这里可以添加更严格的验证逻辑
// 示例仅处理基本数字格式
if _, err := strconv.Atoi(port); err == nil {
return port
}
panic(fmt.Sprintf("invalid port format: %s", port))
}