Files
devstar/services/devcontainer/devcontainer_json.go
xinitx 02baa3b7af !67 增加了重启停止容器、dockerfile方式创建保存容器功能
* change initializeScript path
* Merge branch 'add-dockerfile-method-and-start-stop-container' of https…
* 更新了容器镜像方式的构建、安装和使用方法,但是devcontainer功能还有问题
* fix run postCreateCommand bug
* sh文件方式管理启动脚本
* add restart command and fix bug
* add dockerfile method to create container and save container .restart …
2025-05-07 11:10:30 +00:00

398 lines
11 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
DockerfilePath 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"
]
}`
_, 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",
})
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) {
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.Dockerfile == "" {
return "", nil
}
return GetFileContentByPath(ctx, repo, ".devcontainer/"+devContainerJson.Build.Dockerfile)
}
// 移除 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))
}