408 lines
11 KiB
Go
408 lines
11 KiB
Go
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/models/user"
|
||
"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
|
||
DockerfilePath string
|
||
InitializeCommand []string
|
||
PostCreateCommand []string
|
||
RunArgs []string
|
||
}
|
||
|
||
func CreateDevcontainerJSON(ctx *gitea_context.Context, repo *repo.Repository, doer *user.User) {
|
||
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"
|
||
]
|
||
}`
|
||
_, err := files_service.ChangeRepoFiles(db.DefaultContext, repo, 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",
|
||
})
|
||
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
|
||
}
|
||
|
||
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
|
||
if devContainerJson.Build != nil {
|
||
devStarJson.DockerfilePath = devContainerJson.Build.Dockerfile
|
||
}
|
||
|
||
return devStarJson, nil
|
||
}
|
||
func GetFileContentByPath(ctx context.Context, repo *repo.Repository, path string) (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(path)
|
||
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(path + " entry.IsDir")
|
||
}
|
||
|
||
blob := entry.Blob()
|
||
if blob.Size() >= setting.UI.MaxDisplayFileSize {
|
||
|
||
return "", fmt.Errorf(path + " 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
|
||
}
|
||
|
||
}
|
||
func GetDevcontainerJsonString(ctx context.Context, repo *repo.Repository) (string, error) {
|
||
return GetFileContentByPath(ctx, repo, ".devcontainer/devcontainer.json")
|
||
}
|
||
func GetDockerfileContent(ctx context.Context, repo *repo.Repository) (string, error) {
|
||
dockerfilePath, err := GetDockerfilePath(ctx, repo)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
|
||
return GetFileContentByPath(ctx, repo, ".devcontainer/"+dockerfilePath)
|
||
}
|
||
|
||
func GetDockerfilePath(ctx context.Context, repo *repo.Repository) (string, error) {
|
||
devcontainerJSONContent, err := GetDevcontainerJsonString(ctx, repo)
|
||
var devContainerJson *devcontainer_model.DevContainerJSON
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
// 1. 移除注释
|
||
cleanedContent, err := removeComments(devcontainerJSONContent)
|
||
if err != nil {
|
||
log.Error("Failed to remove comments from .devcontainer/devcontainer.json: %v", err)
|
||
return "", err
|
||
}
|
||
|
||
// 2. 解析 JSON
|
||
devContainerJson, err = devcontainer_model.Unmarshal(cleanedContent)
|
||
if err != nil {
|
||
log.Error("Failed to unmarshal .devcontainer/devcontainer.json: %v", err)
|
||
return "", err
|
||
}
|
||
if devContainerJson.Build == nil || devContainerJson.Build.Dockerfile == "" {
|
||
return "", fmt.Errorf("devcontainer.json error")
|
||
}
|
||
log.Info("%vsdadasdsa", devContainerJson.Build.Dockerfile)
|
||
return devContainerJson.Build.Dockerfile, 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))
|
||
}
|