Files
devstar/services/devcontainer/devcontainer.go
2025-10-30 19:38:23 +08:00

1204 lines
37 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 (
"archive/tar"
"bytes"
"context"
"fmt"
"math"
"net"
"net/url"
"regexp"
"strconv"
"strings"
"time"
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
devcontainer_models "code.gitea.io/gitea/models/devcontainer"
"code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/docker"
docker_module "code.gitea.io/gitea/modules/docker"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/templates"
gitea_context "code.gitea.io/gitea/services/context"
files_service "code.gitea.io/gitea/services/repository/files"
"github.com/docker/docker/api/types"
"xorm.io/builder"
)
func HasDevContainer(ctx context.Context, userID, repoID int64) (bool, error) {
var hasDevContainer bool
dbEngine := db.GetEngine(ctx)
hasDevContainer, err := dbEngine.
Table("devcontainer").
Select("*").
Where("user_id = ? AND repo_id = ?", userID, repoID).
Exist()
if err != nil {
return hasDevContainer, err
}
return hasDevContainer, nil
}
func HasDevContainerConfiguration(ctx context.Context, repo *gitea_context.Repository) (bool, error) {
_, err := FileExists(".devcontainer/devcontainer.json", repo)
if err != nil {
if git.IsErrNotExist(err) {
return false, nil
}
return false, err
}
configurationString, err := GetDevcontainerConfigurationString(ctx, repo.Repository)
if err != nil {
return true, err
}
configurationModel, err := UnmarshalDevcontainerConfigContent(configurationString)
if err != nil {
return true, err
}
// 执行验证
if errs := configurationModel.Validate(); len(errs) > 0 {
log.Info("配置验证失败:")
for _, err := range errs {
fmt.Printf(" - %s\n", err.Error())
}
return true, fmt.Errorf("配置格式错误")
} else {
return true, nil
}
}
func HasDevContainerDockerFile(ctx context.Context, repo *gitea_context.Repository) (bool, error) {
_, err := FileExists(".devcontainer/devcontainer.json", repo)
if err != nil {
if git.IsErrNotExist(err) {
return false, nil
}
return false, err
}
configurationString, err := GetDevcontainerConfigurationString(ctx, repo.Repository)
if err != nil {
return false, err
}
configurationModel, err := UnmarshalDevcontainerConfigContent(configurationString)
if err != nil {
return false, err
}
// 执行验证
if errs := configurationModel.Validate(); len(errs) > 0 {
log.Info("配置验证失败:")
for _, err := range errs {
fmt.Printf(" - %s\n", err.Error())
}
return false, fmt.Errorf("配置格式错误")
} else {
log.Info("%v", configurationModel)
if configurationModel.Build == nil || configurationModel.Build.Dockerfile == "" {
return false, nil
}
_, err := FileExists(".devcontainer/"+configurationModel.Build.Dockerfile, repo)
if err != nil {
if git.IsErrNotExist(err) {
return false, nil
}
return false, err
}
return true, nil
}
}
func CreateDevcontainerConfiguration(repo *repo.Repository, doer *user.User) error {
jsonContent, err := templates.AssetFS().ReadFile("repo/devcontainer/default_devcontainer.json")
if err != nil {
return err
}
_, 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(jsonContent)),
},
},
OldBranch: "main",
NewBranch: "main",
Message: "add container configuration",
})
if err != nil {
return err
}
return nil
}
func GetWebTerminalURL(ctx context.Context, userID, repoID int64) (string, error) {
var devcontainerName string
cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf)
if err != nil {
return "", err
}
dbEngine := db.GetEngine(ctx)
_, err = dbEngine.
Table("devcontainer").
Select("name").
Where("user_id = ? AND repo_id = ?", userID, repoID).
Get(&devcontainerName)
if err != nil {
return "", err
}
if cfg.Section("k8s").Key("ENABLE").Value() == "true" {
// K8s 模式:使用 Istio Gateway + VirtualService
log.Info("GetWebTerminalURL: 使用 Istio 模式获取 WebTerminal URL for DevContainer: %s", devcontainerName)
// 从配置中读取域名
domain := cfg.Section("server").Key("DOMAIN").Value()
// 从容器名称中提取用户名和仓库名
parts := strings.Split(devcontainerName, "-")
var username, repoName string
if len(parts) >= 2 {
username = parts[0]
repoName = parts[1]
} else {
username = "unknown"
repoName = "unknown"
}
// 构建基于 Istio Gateway 的 URL
path := fmt.Sprintf("/%s/%s/dev-container-webterminal", username, repoName)
webTerminalURL := fmt.Sprintf("http://%s%s", domain, path)
log.Info("GetWebTerminalURL: 生成 Istio WebTerminal URL: %s", webTerminalURL)
return webTerminalURL, nil
}
return "", nil
}
/*
-1不存在
0 已创建数据库记录
1 正在拉取镜像
2 正在创建和启动容器
3 容器安装必要工具
4 容器正在运行
5 正在提交容器更新
6 正在重启
7 正在停止
8 容器已停止
9 正在删除
10已删除
*/
func GetDevContainerStatus(ctx context.Context, userID, repoID string) (string, error) {
log.Info("GetDevContainerStatus: Starting - userID: %s, repoID: %s", userID, repoID)
var id int
var containerName string
var status uint16
var realTimeStatus uint16
cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf)
if err != nil {
log.Error("GetDevContainerStatus: Failed to load config: %v", err)
return "", err
}
dbEngine := db.GetEngine(ctx)
_, err = dbEngine.
Table("devcontainer").
Select("devcontainer_status, id, name").
Where("user_id = ? AND repo_id = ?", userID, repoID).
Get(&status, &id, &containerName)
if err != nil {
log.Error("GetDevContainerStatus: Failed to query database: %v", err)
return "", err
}
log.Info("GetDevContainerStatus: Database query result - id: %d, containerName: %s, status: %d", id, containerName, status)
if id == 0 {
log.Info("GetDevContainerStatus: No devcontainer found, returning -1")
return fmt.Sprintf("%d", -1), nil
}
realTimeStatus = status
log.Info("GetDevContainerStatus: Initial realTimeStatus: %d", realTimeStatus)
switch status {
//正在重启
case 6:
if cfg.Section("k8s").Key("ENABLE").Value() == "true" {
// k8s 逻辑:检查 Pod 是否已恢复运行
log.Info("GetDevContainerStatus: K8s branch for case 6 (restarting), container: %s", containerName)
opts := &OpenDevcontainerAppDispatcherOptions{
Name: containerName,
Wait: false,
}
log.Info("GetDevContainerStatus: Calling AssignDevcontainerGetting2K8sOperator with opts: %+v", opts)
devcontainerApp, err := AssignDevcontainerGetting2K8sOperator(&ctx, opts)
if err != nil {
log.Error("GetDevContainerStatus: AssignDevcontainerGetting2K8sOperator failed: %v", err)
} else if devcontainerApp != nil {
log.Info("GetDevContainerStatus: DevcontainerApp retrieved - Name: %s, Ready: %v", devcontainerApp.Name, devcontainerApp.Status.Ready)
if devcontainerApp.Status.Ready {
realTimeStatus = 4 // 已恢复运行
log.Info("GetDevContainerStatus: Container %s is ready, updating status to 4", containerName)
}
} else {
log.Warn("GetDevContainerStatus: DevcontainerApp is nil for container: %s", containerName)
}
} else {
containerRealTimeStatus, err := GetDevContainerStatusFromDocker(ctx, containerName)
if err != nil {
return "", err
} else if containerRealTimeStatus == "running" {
realTimeStatus = 4
}
}
break
//正在关闭
case 7:
if cfg.Section("k8s").Key("ENABLE").Value() == "true" {
// k8s 逻辑:检查 Pod 是否已停止
log.Info("GetDevContainerStatus: K8s branch for case 7 (stopping), container: %s", containerName)
opts := &OpenDevcontainerAppDispatcherOptions{
Name: containerName,
Wait: false,
}
log.Info("GetDevContainerStatus: Calling AssignDevcontainerGetting2K8sOperator for stop check with opts: %+v", opts)
devcontainerApp, err := AssignDevcontainerGetting2K8sOperator(&ctx, opts)
if err != nil {
log.Info("GetDevContainerStatus: DevcontainerApp not found or error, considering stopped: %v", err)
realTimeStatus = 8 // 已停止
} else if devcontainerApp == nil || !devcontainerApp.Status.Ready {
log.Info("GetDevContainerStatus: DevcontainerApp is nil or not ready, considering stopped")
realTimeStatus = 8 // 已停止
} else {
log.Info("GetDevContainerStatus: DevcontainerApp still running - Name: %s, Ready: %v", devcontainerApp.Name, devcontainerApp.Status.Ready)
}
// 已在外部通过 StopDevContainer 触发,此处仅检查状态
} else {
containerRealTimeStatus, err := GetDevContainerStatusFromDocker(ctx, containerName)
if err != nil {
return "", err
} else if containerRealTimeStatus == "exited" {
realTimeStatus = 8
} else {
err = StopDevContainerByDocker(ctx, containerName)
if err != nil {
log.Info(err.Error())
}
}
}
break
case 9:
if cfg.Section("k8s").Key("ENABLE").Value() == "true" {
// k8s 逻辑:检查 Pod 是否已删除
log.Info("GetDevContainerStatus: K8s branch for case 9 (deleting), container: %s", containerName)
opts := &OpenDevcontainerAppDispatcherOptions{
Name: containerName,
Wait: false,
}
log.Info("GetDevContainerStatus: Calling AssignDevcontainerGetting2K8sOperator for delete check with opts: %+v", opts)
_, err := AssignDevcontainerGetting2K8sOperator(&ctx, opts)
if err != nil {
log.Info("GetDevContainerStatus: DevcontainerApp not found, considering deleted: %v", err)
realTimeStatus = 10 // 已删除
} else {
log.Info("GetDevContainerStatus: DevcontainerApp still exists, not deleted yet")
}
// 已在外部通过 DeleteDevContainer 触发,此处仅检查状态
} else {
isContainerNotFound, err := IsContainerNotFound(ctx, containerName)
if err != nil {
return "", err
} else if isContainerNotFound {
realTimeStatus = 10
} else {
err = DeleteDevContainerByDocker(ctx, containerName)
if err != nil {
log.Info(err.Error())
}
}
}
break
default:
log.Info("other status")
}
// K8s: 仅在 Ready 后才返回 4否则维持/降为 3
if cfg.Section("k8s").Key("ENABLE").Value() == "true" && (status == 3 || status == 4) {
opts := &OpenDevcontainerAppDispatcherOptions{
Name: containerName,
Wait: false,
}
app, err := AssignDevcontainerGetting2K8sOperator(&ctx, opts)
if err != nil || app == nil {
// 获取不到 CR 或出错时,保守认为未就绪
realTimeStatus = 3
} else if app.Status.Ready {
realTimeStatus = 4
} else {
realTimeStatus = 3
}
}
//状态更新
if realTimeStatus != status {
if realTimeStatus == 10 {
_, err = dbEngine.Table("devcontainer").
Where("user_id = ? AND repo_id = ? ", userID, repoID).
Delete()
if err != nil {
return "", err
}
_, err = dbEngine.Table("devcontainer_output").
Where("user_id = ? AND repo_id = ? ", userID, repoID).
Delete()
if err != nil {
return "", err
}
return "-1", nil
}
_, err = dbEngine.Table("devcontainer").
Where("user_id = ? AND repo_id = ? ", userID, repoID).
Update(&devcontainer_models.Devcontainer{DevcontainerStatus: realTimeStatus})
if err != nil {
return "", err
}
_, err = dbEngine.Table("devcontainer_output").
Where("user_id = ? AND repo_id = ? AND list_id = ?", userID, repoID, status).
Update(&devcontainer_models.DevcontainerOutput{Status: "finished"})
if err != nil {
return "", err
}
}
log.Info("GetDevContainerStatus: Final realTimeStatus: %d, returning status string", realTimeStatus)
return fmt.Sprintf("%d", realTimeStatus), nil
}
func CreateDevContainer(ctx context.Context, repo *repo.Repository, doer *user.User, publicKeyList []string, isWebTerminal bool) error {
containerName := getSanitizedDevcontainerName(doer.Name, repo.Name)
cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf)
if err != nil {
return err
}
unixTimestamp := time.Now().Unix()
newDevcontainer := devcontainer_models.Devcontainer{
Name: containerName,
DevcontainerHost: cfg.Section("server").Key("DOMAIN").Value(),
DevcontainerUsername: "root",
DevcontainerWorkDir: "/workspace",
DevcontainerStatus: 0,
RepoId: repo.ID,
UserId: doer.ID,
CreatedUnix: unixTimestamp,
UpdatedUnix: unixTimestamp,
}
dbEngine := db.GetEngine(ctx)
_, err = dbEngine.
Table("devcontainer").
Insert(newDevcontainer)
if err != nil {
return err
}
_, err = dbEngine.
Table("devcontainer").
Select("*").
Where("user_id = ? AND repo_id = ?", doer.ID, repo.ID).
Get(&newDevcontainer)
if err != nil {
return err
}
go func() {
otherCtx := context.Background()
if cfg.Section("k8s").Key("ENABLE").Value() == "true" {
// K8s 模式:直接调用 K8s Operator 创建 DevContainer
configurationString, err := GetDevcontainerConfigurationString(otherCtx, repo)
if err != nil {
log.Info("CreateDevContainer: 读取 devcontainer 配置失败: %v", err)
return
}
configurationModel, err := UnmarshalDevcontainerConfigContent(configurationString)
if err != nil {
log.Info("CreateDevContainer: 解析 devcontainer 配置失败: %v", err)
return
}
newDTO := &CreateDevcontainerDTO{
Devcontainer: newDevcontainer,
SSHPublicKeyList: publicKeyList,
GitRepositoryURL: strings.TrimSuffix(setting.AppURL, "/") + repo.Link(),
Image: configurationModel.Image,
}
if err := AssignDevcontainerCreation2K8sOperator(&otherCtx, newDTO); err != nil {
log.Error("CreateDevContainer: K8s 创建失败: %v", err)
return
}
} else {
imageName, err := CreateDevContainerByDockerCommand(otherCtx, &newDevcontainer, repo, publicKeyList)
if err != nil {
return
}
if !isWebTerminal {
CreateDevContainerByDockerAPI(otherCtx, &newDevcontainer, imageName, repo, publicKeyList)
}
}
}()
return nil
}
func DeleteDevContainer(ctx context.Context, userID, repoID int64) error {
dbEngine := db.GetEngine(ctx)
var devContainerInfo devcontainer_models.Devcontainer
cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf)
if err != nil {
return err
}
_, err = dbEngine.
Table("devcontainer").
Select("*").
Where("user_id = ? AND repo_id = ?", userID, repoID).
Get(&devContainerInfo)
if err != nil {
return err
}
_, err = dbEngine.Table("devcontainer").
Where("user_id = ? AND repo_id = ? ", userID, repoID).
Update(&devcontainer_models.Devcontainer{DevcontainerStatus: 9})
if err != nil {
return err
}
go func() {
otherCtx := context.Background()
if cfg.Section("k8s").Key("ENABLE").Value() == "true" {
// k8s 模式:调用 K8s Operator 删除 DevContainer 资源
devList := []devcontainer_models.Devcontainer{devContainerInfo}
_ = AssignDevcontainerDeletion2K8sOperator(&otherCtx, &devList)
} else {
err = DeleteDevContainerByDocker(otherCtx, devContainerInfo.Name)
if err != nil {
log.Info(err.Error())
}
}
}()
return nil
}
func RestartDevContainer(ctx context.Context, userID, repoID int64) error {
dbEngine := db.GetEngine(ctx)
var devContainerInfo devcontainer_models.Devcontainer
cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf)
if err != nil {
return err
}
_, err = dbEngine.
Table("devcontainer").
Select("*").
Where("user_id = ? AND repo_id = ?", userID, repoID).
Get(&devContainerInfo)
if err != nil {
return err
}
_, err = dbEngine.Table("devcontainer").
Where("user_id = ? AND repo_id = ? ", userID, repoID).
Update(&devcontainer_models.Devcontainer{DevcontainerStatus: 6})
if err != nil {
return err
}
go func() {
otherCtx := context.Background()
if cfg.Section("k8s").Key("ENABLE").Value() == "true" {
// k8s 模式:调用 K8s Operator 重启 DevContainer
vo := &DevcontainerVO{
DevContainerName: devContainerInfo.Name,
UserId: userID,
RepoId: repoID,
}
if err := AssignDevcontainerRestart2K8sOperator(&otherCtx, vo); err != nil {
log.Error("RestartDevContainer: K8s 重启失败: %v", err)
}
} else {
err = RestartDevContainerByDocker(otherCtx, devContainerInfo.Name)
if err != nil {
log.Info(err.Error())
}
}
}()
return nil
}
func StopDevContainer(ctx context.Context, userID, repoID int64) error {
dbEngine := db.GetEngine(ctx)
var devContainerInfo devcontainer_models.Devcontainer
cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf)
if err != nil {
return err
}
_, err = dbEngine.
Table("devcontainer").
Select("*").
Where("user_id = ? AND repo_id = ?", userID, repoID).
Get(&devContainerInfo)
if err != nil {
return err
}
_, err = dbEngine.Table("devcontainer").
Where("user_id = ? AND repo_id = ? ", userID, repoID).
Update(&devcontainer_models.Devcontainer{DevcontainerStatus: 7})
if err != nil {
return err
}
go func() {
otherCtx := context.Background()
if cfg.Section("k8s").Key("ENABLE").Value() == "true" {
// k8s 模式:调用 K8s Operator 停止 DevContainer
vo := &DevcontainerVO{
DevContainerName: devContainerInfo.Name,
UserId: userID,
RepoId: repoID,
}
if err := AssignDevcontainerStop2K8sOperator(&otherCtx, vo); err != nil {
log.Error("StopDevContainer: K8s 停止失败: %v", err)
}
} else {
err = StopDevContainerByDocker(otherCtx, devContainerInfo.Name)
if err != nil {
log.Info(err.Error())
}
}
}()
return nil
}
func UpdateDevContainer(ctx context.Context, doer *user.User, repo *repo.Repository, updateInfo *UpdateInfo) error {
dbEngine := db.GetEngine(ctx)
var devContainerInfo devcontainer_models.Devcontainer
cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf)
if err != nil {
return err
}
_, err = dbEngine.
Table("devcontainer").
Select("*").
Where("user_id = ? AND repo_id = ?", doer.ID, repo.ID).
Get(&devContainerInfo)
if err != nil {
return err
}
_, err = dbEngine.Table("devcontainer").
Where("user_id = ? AND repo_id = ? ", doer.ID, repo.ID).
Update(&devcontainer_models.Devcontainer{DevcontainerStatus: 5})
if err != nil {
return err
}
otherCtx := context.Background()
if cfg.Section("k8s").Key("ENABLE").Value() == "true" {
//k8s的逻辑
} else {
updateErr := UpdateDevContainerByDocker(otherCtx, &devContainerInfo, updateInfo, repo, doer)
_, err = dbEngine.Table("devcontainer").
Where("user_id = ? AND repo_id = ? ", doer.ID, repo.ID).
Update(&devcontainer_models.Devcontainer{DevcontainerStatus: 4})
if err != nil {
return err
}
if updateErr != nil {
return updateErr
}
}
return nil
}
func GetTerminalCommand(ctx context.Context, userID string, repo *repo.Repository) (string, string, error) {
dbEngine := db.GetEngine(ctx)
var devContainerInfo devcontainer_models.Devcontainer
cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf)
if err != nil {
return "", "", err
}
_, err = dbEngine.
Table("devcontainer").
Select("*").
Where("user_id = ? AND repo_id = ?", userID, repo.ID).
Get(&devContainerInfo)
if err != nil {
return "", "", err
}
realTimeStatus := devContainerInfo.DevcontainerStatus
var cmd string
switch devContainerInfo.DevcontainerStatus {
case 0:
if devContainerInfo.Id > 0 {
realTimeStatus = 1
}
break
case 1:
//正在拉取镜像,当镜像拉取成功,则状态转移
if cfg.Section("k8s").Key("ENABLE").Value() == "true" {
//k8s的逻辑
} else {
configurationString, err := GetDevcontainerConfigurationString(ctx, repo)
if err != nil {
return "", "", err
}
configurationModel, err := UnmarshalDevcontainerConfigContent(configurationString)
if err != nil {
return "", "", err
}
var imageName string
if configurationModel.Build == nil || configurationModel.Build.Dockerfile == "" {
imageName = configurationModel.Image
} else {
imageName = userID + "-" + fmt.Sprintf("%d", repo.ID) + "-dockerfile"
}
isExist, err := ImageExists(ctx, imageName)
if err != nil {
return "", "", err
}
if isExist {
realTimeStatus = 2
} else {
_, err = dbEngine.Table("devcontainer_output").
Select("command").
Where("user_id = ? AND repo_id = ? AND list_id = ?", userID, repo.ID, realTimeStatus).
Get(&cmd)
if err != nil {
return "", "", err
}
}
}
break
case 2:
//正在创建容器,创建容器成功,则状态转移
if cfg.Section("k8s").Key("ENABLE").Value() == "true" {
//k8s的逻辑
} else {
status, err := GetDevContainerStatusFromDocker(ctx, devContainerInfo.Name)
if err != nil {
return "", "", err
}
if status == "created" {
//添加脚本文件
if cfg.Section("k8s").Key("ENABLE").Value() == "true" {
} else {
userNum, err := strconv.ParseInt(userID, 10, 64)
if err != nil {
return "", "", err
}
var scriptContent string
scriptContent, err = GetCommandContent(ctx, userNum, repo)
log.Info("command: %s", scriptContent)
if err != nil {
return "", "", err
}
// 创建 tar 归档文件
var buf bytes.Buffer
tw := tar.NewWriter(&buf)
defer tw.Close()
// 添加文件到 tar 归档
AddFileToTar(tw, "webTerminal.sh", string(scriptContent), 0777)
// 创建 Docker 客户端
cli, err := docker_module.CreateDockerClient(ctx)
if err != nil {
return "", "", err
}
// 获取容器 ID
containerID, err := docker_module.GetContainerID(cli, devContainerInfo.Name)
if err != nil {
return "", "", err
}
err = cli.CopyToContainer(ctx, containerID, "/home", bytes.NewReader(buf.Bytes()), types.CopyToContainerOptions{})
if err != nil {
log.Info("%v", err)
return "", "", err
}
}
realTimeStatus = 3
}
}
break
case 3:
//正在初始化容器,初始化容器成功,则状态转移
if cfg.Section("k8s").Key("ENABLE").Value() == "true" {
//k8s的逻辑
} else {
status, err := CheckDirExistsFromDocker(ctx, devContainerInfo.Name, devContainerInfo.DevcontainerWorkDir)
if err != nil {
return "", "", err
}
if status {
realTimeStatus = 4
}
}
break
case 4:
//正在连接容器
if cfg.Section("k8s").Key("ENABLE").Value() == "true" {
//k8s的逻辑
} else {
_, err = dbEngine.Table("devcontainer_output").
Select("command").
Where("user_id = ? AND repo_id = ? AND list_id = ?", userID, repo.ID, realTimeStatus).
Get(&cmd)
if err != nil {
return "", "", err
}
}
break
}
if realTimeStatus != devContainerInfo.DevcontainerStatus {
//下一条指令
_, err = dbEngine.Table("devcontainer_output").
Select("command").
Where("user_id = ? AND repo_id = ? AND list_id = ?", userID, repo.ID, realTimeStatus).
Get(&cmd)
if err != nil {
return "", "", err
}
_, err = dbEngine.Table("devcontainer").
Where("user_id = ? AND repo_id = ? ", userID, repo.ID).
Update(&devcontainer_models.Devcontainer{DevcontainerStatus: realTimeStatus})
if err != nil {
return "", "", err
}
}
return cmd, fmt.Sprintf("%d", realTimeStatus), nil
}
func GetDevContainerOutput(ctx context.Context, doer *user.User, repo *repo.Repository) (OutputResponse, error) {
var devContainerOutput []devcontainer_models.DevcontainerOutput
dbEngine := db.GetEngine(ctx)
resp := OutputResponse{}
var status string
var containerName string
_, err := dbEngine.
Table("devcontainer").
Select("devcontainer_status, name").
Where("user_id = ? AND repo_id = ?", doer.ID, repo.ID).
Get(&status, &containerName)
if err != nil {
return resp, err
}
err = dbEngine.Table("devcontainer_output").
Where("user_id = ? AND repo_id = ?", doer.ID, repo.ID).
Find(&devContainerOutput)
if err != nil {
return resp, err
}
if len(devContainerOutput) > 0 {
resp.CurrentJob.Title = repo.Name + " Devcontainer Info"
resp.CurrentJob.Detail = status
if status == "4" {
// 获取WebSSH服务端口
webTerminalURL, err := GetWebTerminalURL(ctx, doer.ID, repo.ID)
if err == nil {
return resp, err
}
// 解析URL
u, err := url.Parse(webTerminalURL)
if err != nil {
return resp, err
}
// 分离主机和端口
terminalHost, terminalPort, err := net.SplitHostPort(u.Host)
resp.CurrentJob.IP = terminalHost
resp.CurrentJob.Port = terminalPort
if err != nil {
return resp, err
}
}
for _, item := range devContainerOutput {
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,
})
}
}
return resp, nil
}
func GetMappedPort(ctx context.Context, containerName string, port string) (uint16, error) {
cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf)
if err != nil {
return 0, err
}
if cfg.Section("k8s").Key("ENABLE").Value() == "true" {
//k8s的逻辑
return 0, nil
} else {
port, err := docker_module.GetMappedPort(ctx, containerName, port)
if err != nil {
return 0, err
}
return port, nil
}
}
func GetDevcontainersList(ctx context.Context, doer *user.User, pageNum, pageSize int) (DevcontainerList, error) {
// 0. 构造异常返回时的空数据
var resultDevContainerListVO = DevcontainerList{
Page: 0,
PageSize: 50,
PageTotalNum: 0,
ItemTotalNum: 0,
DevContainers: []devcontainer_models.Devcontainer{},
}
resultDevContainerListVO.UserID = doer.ID
resultDevContainerListVO.Username = doer.Name
paginationOption := db.ListOptions{
Page: pageNum,
PageSize: pageSize,
}
paginationOption.ListAll = false // 强制使用分页查询,禁止一次性列举所有 devContainers
if paginationOption.Page <= 0 { // 未指定页码/无效页码:查询第 1 页
paginationOption.Page = 1
}
if paginationOption.PageSize <= 0 || paginationOption.PageSize > 50 {
paginationOption.PageSize = 50 // /无效页面大小/超过每页最大限制:自动调整到系统最大开发容器页面大小
}
resultDevContainerListVO.Page = paginationOption.Page
resultDevContainerListVO.PageSize = paginationOption.PageSize
// 2. SQL 条件构建
sqlCondition := builder.Eq{"user_id": doer.ID}
// 执行数据库事务
err := db.WithTx(ctx, func(ctx context.Context) error {
// 查询总数
count, err := db.GetEngine(ctx).
Table("devcontainer").
Where(sqlCondition).
Count()
if err != nil {
return err
}
resultDevContainerListVO.ItemTotalNum = count
// 无记录直接返回
if count == 0 {
return nil
}
// 计算分页参数
pageSize := int64(resultDevContainerListVO.PageSize)
resultDevContainerListVO.PageTotalNum = int(math.Ceil(float64(count) / float64(pageSize)))
// 查询分页数据
sess := db.GetEngine(ctx).
Table("devcontainer").
Join("INNER", "repository", "devcontainer.repo_id = repository.id").
Where(sqlCondition).
OrderBy("devcontainer_id DESC").
Select(`devcontainer.id AS devcontainer_id,
devcontainer.name AS devcontainer_name,
devcontainer.devcontainer_host AS devcontainer_host,
devcontainer.devcontainer_username AS devcontainer_username,
devcontainer.devcontainer_work_dir AS devcontainer_work_dir,
devcontainer.repo_id AS repo_id,
repository.name AS repo_name,
repository.owner_name AS repo_owner_name,
repository.description AS repo_description,
CONCAT('/', repository.owner_name, '/', repository.name) AS repo_link`)
resultDevContainerListVO.DevContainers = make([]devcontainer_models.Devcontainer, 0, pageSize)
err = db.SetSessionPagination(sess, &paginationOption).
Find(&resultDevContainerListVO.DevContainers)
if err != nil {
return err
}
return nil
})
if err != nil {
return resultDevContainerListVO, err
}
return resultDevContainerListVO, nil
}
func Get_IDE_TerminalURL(ctx *gitea_context.Context, doer *user.User, repo *gitea_context.Repository) (string, error) {
dbEngine := db.GetEngine(ctx)
var devContainerInfo devcontainer_models.Devcontainer
_, err := dbEngine.
Table("devcontainer").
Select("*").
Where("user_id = ? AND repo_id = ?", doer.ID, repo.Repository.ID).
Get(&devContainerInfo)
if err != nil {
return "", err
}
// 加载配置文件
cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf)
if err != nil {
log.Error("Get_IDE_TerminalURL: 加载配置文件失败: %v", err)
return "", err
}
log.Info("Get_IDE_TerminalURL: 配置文件加载成功, ROOT_URL=%s", cfg.Section("server").Key("ROOT_URL").Value())
var access_token string
// 检查 session 中是否已存在 token
if ctx.Session.Get("access_token") != nil {
access_token = ctx.Session.Get("access_token").(string)
} else {
// 生成 token
token := &auth_model.AccessToken{
UID: devContainerInfo.UserId,
Name: "terminal_login_token",
}
exist, err := auth_model.AccessTokenByNameExists(ctx, token)
if err != nil {
return "", err
}
if exist {
db.GetEngine(ctx).Table("access_token").Where("uid = ? AND name = ?", doer.ID, "terminal_login_token").Delete()
}
scope, err := auth_model.AccessTokenScope(strings.Join([]string{"write:user", "write:repository"}, ",")).Normalize()
if err != nil {
return "", err
}
token.Scope = scope
err = auth_model.NewAccessToken(db.DefaultContext, token)
if err != nil {
return "", err
}
ctx.Session.Set("terminal_login_token", token.Token)
access_token = token.Token
}
// 根据不同的代理类型获取 SSH 端口
var port string
if cfg.Section("k8s").Key("ENABLE").Value() == "true" {
// K8s 环境:通过 DevcontainerApp 的 NodePort 作为 SSH 端口
apiRequestCtx := ctx.Req.Context()
opts := &OpenDevcontainerAppDispatcherOptions{
Name: devContainerInfo.Name,
Wait: false,
}
devcontainerApp, err := AssignDevcontainerGetting2K8sOperator(&apiRequestCtx, opts)
if err != nil {
return "", err
}
if devcontainerApp == nil || devcontainerApp.Status.NodePortAssigned == 0 {
return "", fmt.Errorf("k8s DevcontainerApp 未就绪或未分配 NodePort: %s", devContainerInfo.Name)
}
port = fmt.Sprintf("%d", devcontainerApp.Status.NodePortAssigned)
} else {
mappedPort, err := docker_module.GetMappedPort(ctx, devContainerInfo.Name, "22")
if err != nil {
return "", err
}
port = fmt.Sprintf("%d", mappedPort)
}
// 构建并返回 URL
return "://mengning.devstar/" +
"openProject?host=" + repo.Repository.Name +
"&hostname=" + devContainerInfo.DevcontainerHost +
"&port=" + port +
"&username=" + doer.Name +
"&path=" + devContainerInfo.DevcontainerWorkDir +
"&access_token=" + access_token +
"&devstar_username=" + repo.Repository.OwnerName +
"&devstar_domain=" + cfg.Section("server").Key("ROOT_URL").Value(), nil
}
func GetCommandContent(ctx context.Context, userId int64, repo *repo.Repository) (string, error) {
configurationString, err := GetDevcontainerConfigurationString(ctx, repo)
if err != nil {
return "", err
}
configurationModel, err := UnmarshalDevcontainerConfigContent(configurationString)
if err != nil {
return "", err
}
onCreateCommand := strings.TrimSpace(strings.Join(configurationModel.ParseCommand(configurationModel.OnCreateCommand), "\n"))
if _, ok := configurationModel.OnCreateCommand.(map[string]interface{}); ok {
// 是 map[string]interface{} 类型
cmdObj := configurationModel.OnCreateCommand.(map[string]interface{})
if pathValue, hasPath := cmdObj["path"]; hasPath {
fileCommand, err := GetFileContentByPath(ctx, repo, ".devcontainer/"+pathValue.(string))
if err != nil {
return "", err
}
onCreateCommand += "\n" + fileCommand
}
}
updateCommand := strings.TrimSpace(strings.Join(configurationModel.ParseCommand(configurationModel.UpdateContentCommand), "\n"))
if _, ok := configurationModel.UpdateContentCommand.(map[string]interface{}); ok {
// 是 map[string]interface{} 类型
cmdObj := configurationModel.UpdateContentCommand.(map[string]interface{})
if pathValue, hasPath := cmdObj["path"]; hasPath {
fileCommand, err := GetFileContentByPath(ctx, repo, ".devcontainer/"+pathValue.(string))
if err != nil {
return "", err
}
updateCommand += "\n" + fileCommand
}
}
postCreateCommand := strings.TrimSpace(strings.Join(configurationModel.ParseCommand(configurationModel.PostCreateCommand), "\n"))
if _, ok := configurationModel.PostCreateCommand.(map[string]interface{}); ok {
// 是 map[string]interface{} 类型
cmdObj := configurationModel.PostCreateCommand.(map[string]interface{})
if pathValue, hasPath := cmdObj["path"]; hasPath {
fileCommand, err := GetFileContentByPath(ctx, repo, ".devcontainer/"+pathValue.(string))
if err != nil {
return "", err
}
postCreateCommand += "\n" + fileCommand
}
}
postStartCommand := strings.TrimSpace(strings.Join(configurationModel.ParseCommand(configurationModel.PostStartCommand), "\n"))
if _, ok := configurationModel.PostStartCommand.(map[string]interface{}); ok {
// 是 map[string]interface{} 类型
cmdObj := configurationModel.PostStartCommand.(map[string]interface{})
if pathValue, hasPath := cmdObj["path"]; hasPath {
fileCommand, err := GetFileContentByPath(ctx, repo, ".devcontainer/"+pathValue.(string))
if err != nil {
return "", err
}
postStartCommand += "\n" + fileCommand
}
}
var script []string
scripts, err := devcontainer_models.GetScript(ctx, userId, repo.ID)
for _, v := range scripts {
script = append(script, v)
}
scriptCommand := strings.TrimSpace(strings.Join(script, "\n"))
userCommand := scriptCommand + "\n" + onCreateCommand + "\n" + updateCommand + "\n" + postCreateCommand + "\n" + postStartCommand + "\n"
assetFS := templates.AssetFS()
Content_tmpl, err := assetFS.ReadFile("repo/devcontainer/devcontainer_tmpl.sh")
if err != nil {
return "", err
}
Content_start, err := assetFS.ReadFile("repo/devcontainer/devcontainer_start.sh")
if err != nil {
return "", err
}
Content_restart, err := assetFS.ReadFile("repo/devcontainer/devcontainer_restart.sh")
if err != nil {
return "", err
}
final_command := string(Content_tmpl)
re1 := regexp.MustCompile(`\$\{` + regexp.QuoteMeta("START") + `\}|` + `\$` + regexp.QuoteMeta("START") + `\b`)
escapedContentStart := strings.ReplaceAll(string(Content_start), `$`, `$$`)
escapedUserCommand := strings.ReplaceAll(userCommand, `$`, `$$`)
final_command = re1.ReplaceAllString(final_command, escapedContentStart+"\n"+escapedUserCommand)
re1 = regexp.MustCompile(`\$RESTART\b`)
escapedContentRestart := strings.ReplaceAll(string(Content_restart), `$`, `$$`)
escapedPostStartCommand := strings.ReplaceAll(postStartCommand, `$`, `$$`)
final_command = re1.ReplaceAllString(final_command, escapedContentRestart+"\n"+escapedPostStartCommand)
return parseCommand(ctx, final_command, userId, repo)
}
func AddPublicKeyToAllRunningDevContainer(ctx context.Context, userId int64, publicKey string) error {
// 加载配置文件
cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf)
if err != nil {
log.Error("Get_IDE_TerminalURL: 加载配置文件失败: %v", err)
return err
}
if cfg.Section("k8s").Key("ENABLE").Value() == "true" {
} else {
cli, err := docker.CreateDockerClient(ctx)
if err != nil {
return err
}
defer cli.Close()
var devcontainerList []devcontainer_models.Devcontainer
// 查询所有打开的容器
err = db.GetEngine(ctx).
Table("devcontainer").
Where("user_id = ? AND devcontainer_status = ?", userId, 4).
Find(&devcontainerList)
if err != nil {
return err
}
if len(devcontainerList) > 0 {
// 将公钥写入这些打开的容器中
for _, repoDevContainer := range devcontainerList {
containerID, err := docker.GetContainerID(cli, repoDevContainer.Name)
if err != nil {
return err
}
log.Info("container id: %s, name: %s", containerID, repoDevContainer.Name)
// 检查容器状态
containerStatus, err := docker.GetContainerStatus(cli, containerID)
if err != nil {
continue
}
if containerStatus == "running" {
// 只为处于运行状态的容器添加公钥
_, err = docker.ExecCommandInContainer(ctx, cli, repoDevContainer.Name, fmt.Sprintf("echo '%s' >> ~/.ssh/authorized_keys", publicKey))
if err != nil {
return err
}
}
}
}
return nil
}
return fmt.Errorf("unknown agent")
}
func parseCommand(ctx context.Context, command string, userId int64, repo *repo.Repository) (string, error) {
variables, err := devcontainer_models.GetVariables(ctx, userId, repo.ID)
var variablesName []string
variablesCircle := checkEachVariable(variables)
for key := range variables {
if !variablesCircle[key] {
variablesName = append(variablesName, key)
}
}
for ContainsAnySubstring(command, variablesName) {
for key, value := range variables {
if variablesCircle[key] == true {
continue
}
log.Info("key: %s, value: %s", key, value)
re1 := regexp.MustCompile(`\$\{` + regexp.QuoteMeta(key) + `\}|` + `\$` + regexp.QuoteMeta(key) + `\b`)
escapedValue := strings.ReplaceAll(value, `$`, `$$`)
command = re1.ReplaceAllString(command, escapedValue)
variablesName = append(variablesName, key)
}
}
var userSSHPublicKeyList []string
err = db.GetEngine(ctx).
Table("public_key").
Select("content").
Where("owner_id = ?", userId).
Find(&userSSHPublicKeyList)
if err != nil {
return "", err
}
re1 := regexp.MustCompile(`\$\{` + regexp.QuoteMeta("PUBLIC_KEY_LIST") + `\}|` + `\$` + regexp.QuoteMeta("PUBLIC_KEY_LIST") + `\b`)
command = re1.ReplaceAllString(command, strings.Join(userSSHPublicKeyList, "\n"))
return command, nil
}