Some checks failed
DevStar Studio Auto Test Pipeline / unit-frontend-test (pull_request) Failing after 6m25s
DevStar Studio Auto Test Pipeline / unit-backend-test (pull_request) Failing after 4m54s
DevStar Studio CI/CD Pipeline / build-and-push-x86-64-docker-image (pull_request) Failing after 1m24s
DevStar E2E Test / e2e-test (pull_request) Failing after 3m0s
1536 lines
49 KiB
Go
1536 lines
49 KiB
Go
package devcontainer
|
||
|
||
import (
|
||
"archive/tar"
|
||
"bytes"
|
||
"context"
|
||
"fmt"
|
||
"math"
|
||
"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"
|
||
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, string, 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 == "" {
|
||
_, err := FileExists(".devcontainer/Dockerfile", repo)
|
||
if err != nil {
|
||
if git.IsErrNotExist(err) {
|
||
return false, "", nil
|
||
}
|
||
return false, "", err
|
||
}
|
||
return true, ".devcontainer/Dockerfile", nil
|
||
}
|
||
_, err := FileExists(".devcontainer/"+configurationModel.Build.Dockerfile, repo)
|
||
if err != nil {
|
||
if git.IsErrNotExist(err) {
|
||
_, err := FileExists(".devcontainer/Dockerfile", repo)
|
||
if err != nil {
|
||
if git.IsErrNotExist(err) {
|
||
return false, "", nil
|
||
}
|
||
return false, "", err
|
||
}
|
||
return true, ".devcontainer/Dockerfile", nil
|
||
}
|
||
return false, "", err
|
||
}
|
||
return true, ".devcontainer/" + configurationModel.Build.Dockerfile, 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已删除
|
||
*/
|
||
|
||
// checkContainerExistence 检测容器是否存在
|
||
// 返回 true 表示容器存在,false 表示容器不存在
|
||
func checkContainerExistence(ctx context.Context, containerName string) (bool, error) {
|
||
cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf)
|
||
if err != nil {
|
||
return false, err
|
||
}
|
||
|
||
if cfg.Section("k8s").Key("ENABLE").Value() == "true" {
|
||
// K8s 模式:检查 Pod 是否存在
|
||
opts := &OpenDevcontainerAppDispatcherOptions{
|
||
Name: containerName,
|
||
Wait: false,
|
||
}
|
||
devcontainerApp, err := AssignDevcontainerGetting2K8sOperator(&ctx, opts)
|
||
return (err == nil && devcontainerApp != nil), err
|
||
} else {
|
||
// Docker 模式:检查容器是否存在
|
||
isContainerNotFound, err := IsContainerNotFound(ctx, containerName)
|
||
if err != nil {
|
||
return false, err
|
||
}
|
||
return !isContainerNotFound, nil
|
||
}
|
||
}
|
||
|
||
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)
|
||
|
||
// 统一检查容器是否已被删除
|
||
// 如果容器不存在,直接设置为已删除状态,跳过后续的状态检测
|
||
// 注意:创建过程中的状态(0,1,2,3)和正在删除的状态(9)不应该检测容器是否存在
|
||
if status != 0 && status != 1 && status != 2 && status != 3 && status != 9 {
|
||
containerExists, err := checkContainerExistence(ctx, containerName)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
|
||
if !containerExists {
|
||
// 容器不存在,已被删除
|
||
log.Info("GetDevContainerStatus: Container %s not found, considering deleted", containerName)
|
||
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
|
||
}
|
||
}
|
||
|
||
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.Error("RestartDevContainer: Docker restart call failed: %v", err)
|
||
// Try to check container current status, if already running then sync database
|
||
statusStr, stErr := GetDevContainerStatusFromDocker(otherCtx, devContainerInfo.Name)
|
||
if stErr == nil && statusStr == "running" {
|
||
_, updateErr := db.GetEngine(otherCtx).Table("devcontainer").
|
||
Where("user_id = ? AND repo_id = ?", userID, repoID).
|
||
Update(&devcontainer_models.Devcontainer{DevcontainerStatus: 4})
|
||
if updateErr != nil {
|
||
log.Error("RestartDevContainer: Failed to update status to 4: %v", updateErr)
|
||
} else {
|
||
_, upOutErr := db.GetEngine(otherCtx).Table("devcontainer_output").
|
||
Where("user_id = ? AND repo_id = ? AND list_id = ?", userID, repoID, 6).
|
||
Update(&devcontainer_models.DevcontainerOutput{Status: "finished"})
|
||
if upOutErr != nil {
|
||
log.Error("RestartDevContainer: Failed to update output status: %v", upOutErr)
|
||
}
|
||
}
|
||
} else {
|
||
log.Error("RestartDevContainer: Container is not running after restart call failed: %v", stErr)
|
||
}
|
||
} else {
|
||
// Restart call succeeded, poll until container status is running or timeout
|
||
for i := 0; i < 10; i++ {
|
||
statusStr, stErr := GetDevContainerStatusFromDocker(otherCtx, devContainerInfo.Name)
|
||
if stErr == nil && statusStr == "running" {
|
||
_, updateErr := db.GetEngine(otherCtx).Table("devcontainer").
|
||
Where("user_id = ? AND repo_id = ?", userID, repoID).
|
||
Update(&devcontainer_models.Devcontainer{DevcontainerStatus: 4})
|
||
if updateErr != nil {
|
||
log.Error("RestartDevContainer: Failed to update status to 4: %v", updateErr)
|
||
} else {
|
||
_, upOutErr := db.GetEngine(otherCtx).Table("devcontainer_output").
|
||
Where("user_id = ? AND repo_id = ? AND list_id = ?", userID, repoID, 6).
|
||
Update(&devcontainer_models.DevcontainerOutput{Status: "finished"})
|
||
if upOutErr != nil {
|
||
log.Error("RestartDevContainer: Failed to update output status: %v", upOutErr)
|
||
}
|
||
}
|
||
break
|
||
}
|
||
time.Sleep(2 * time.Second)
|
||
}
|
||
}
|
||
}
|
||
}()
|
||
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()
|
||
var stopErr error
|
||
if cfg.Section("k8s").Key("ENABLE").Value() == "true" {
|
||
// k8s 模式:调用 K8s Operator 停止 DevContainer
|
||
vo := &DevcontainerVO{
|
||
DevContainerName: devContainerInfo.Name,
|
||
UserId: userID,
|
||
RepoId: repoID,
|
||
}
|
||
stopErr = AssignDevcontainerStop2K8sOperator(&otherCtx, vo)
|
||
if stopErr != nil {
|
||
log.Error("StopDevContainer: K8s stop failed: %v", stopErr)
|
||
}
|
||
} else {
|
||
stopErr = StopDevContainerByDocker(otherCtx, devContainerInfo.Name)
|
||
if stopErr != nil {
|
||
log.Info("StopDevContainer: Docker stop failed: %v", stopErr)
|
||
}
|
||
}
|
||
|
||
// After stop operation completes, update status to 8 (stopped)
|
||
updateCtx := context.Background()
|
||
if stopErr == nil {
|
||
// Stop succeeded, update status to 8
|
||
_, updateErr := db.GetEngine(updateCtx).Table("devcontainer").
|
||
Where("user_id = ? AND repo_id = ?", userID, repoID).
|
||
Update(&devcontainer_models.Devcontainer{DevcontainerStatus: 8})
|
||
if updateErr != nil {
|
||
log.Error("StopDevContainer: Failed to update status: %v", updateErr)
|
||
} else {
|
||
log.Info("StopDevContainer: Container stopped, status updated to 8")
|
||
}
|
||
}
|
||
}()
|
||
return nil
|
||
}
|
||
|
||
func UpdateDevContainer(ctx context.Context, doer *user.User, repo *gitea_context.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.Repository.ID).
|
||
Get(&devContainerInfo)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
// 保存原始状态,以便出错时回滚
|
||
originalStatus := devContainerInfo.DevcontainerStatus
|
||
_, err = dbEngine.Table("devcontainer").
|
||
Where("user_id = ? AND repo_id = ? ", doer.ID, repo.Repository.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)
|
||
if updateErr != nil {
|
||
// 出错时回滚状态
|
||
_, rollbackErr := dbEngine.Table("devcontainer").
|
||
Where("user_id = ? AND repo_id = ? ", doer.ID, repo.Repository.ID).
|
||
Update(&devcontainer_models.Devcontainer{DevcontainerStatus: originalStatus})
|
||
if rollbackErr != nil {
|
||
log.Error("Failed to rollback devcontainer status: %v", rollbackErr)
|
||
}
|
||
return updateErr
|
||
}
|
||
_, err = dbEngine.Table("devcontainer").
|
||
Where("user_id = ? AND repo_id = ? ", doer.ID, repo.Repository.ID).
|
||
Update(&devcontainer_models.Devcontainer{DevcontainerStatus: 4})
|
||
if err != nil {
|
||
return err
|
||
}
|
||
}
|
||
|
||
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 {
|
||
exist, _, err := ContainerExists(ctx, devContainerInfo.Name)
|
||
if err != nil {
|
||
return "", "", err
|
||
}
|
||
if !exist {
|
||
_, 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
|
||
}
|
||
} else {
|
||
status, err := GetDevContainerStatusFromDocker(ctx, devContainerInfo.Name)
|
||
if err != nil {
|
||
return "", "", err
|
||
}
|
||
// 只要容器存在(无论是 created, exited 还是 running),都视为创建成功,进入下一阶段
|
||
if status == "created" || status == "exited" || status == "running" {
|
||
//添加脚本文件
|
||
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
|
||
}
|
||
}
|
||
|
||
// 统一在状态为4时追加 postAttachCommand,避免首次从3跳到4时遗漏
|
||
if realTimeStatus == 4 && cfg.Section("k8s").Key("ENABLE").Value() != "true" {
|
||
configurationString, err := GetDevcontainerConfigurationString(ctx, repo)
|
||
if err != nil {
|
||
return "", "", err
|
||
}
|
||
configurationModel, err := UnmarshalDevcontainerConfigContent(configurationString)
|
||
if err != nil {
|
||
return "", "", err
|
||
}
|
||
postAttachCommand := strings.TrimSpace(strings.Join(configurationModel.ParseCommand(configurationModel.PostAttachCommand), "\n"))
|
||
if _, ok := configurationModel.PostAttachCommand.(map[string]interface{}); ok {
|
||
cmdObj := configurationModel.PostAttachCommand.(map[string]interface{})
|
||
if pathValue, hasPath := cmdObj["path"]; hasPath {
|
||
fileCommand, err := GetFileContentByPath(ctx, repo, ".devcontainer/"+pathValue.(string))
|
||
if err != nil {
|
||
return "", "", err
|
||
}
|
||
postAttachCommand += "\n" + fileCommand
|
||
}
|
||
}
|
||
cmd += postAttachCommand
|
||
}
|
||
return cmd, fmt.Sprintf("%d", realTimeStatus), nil
|
||
}
|
||
func GetDevContainerOutput(ctx context.Context, user_id string, repo *repo.Repository) (string, error) {
|
||
var devContainerOutput string
|
||
dbEngine := db.GetEngine(ctx)
|
||
|
||
_, err := dbEngine.Table("devcontainer_output").
|
||
Select("output").
|
||
Where("user_id = ? AND repo_id = ? AND list_id = ?", user_id, repo.ID, 4).
|
||
Get(&devContainerOutput)
|
||
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
if devContainerOutput != "" {
|
||
_, err = dbEngine.Table("devcontainer_output").
|
||
Where("user_id = ? AND repo_id = ? AND list_id = ?", user_id, repo.ID, 4).
|
||
Update(map[string]interface{}{
|
||
"output": "",
|
||
})
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
}
|
||
|
||
return devContainerOutput, nil
|
||
}
|
||
func SaveDevContainerOutput(ctx context.Context, user_id string, repo *repo.Repository, newoutput string) error {
|
||
var devContainerOutput string
|
||
var finalOutput string
|
||
dbEngine := db.GetEngine(ctx)
|
||
|
||
// 从数据库中获取现有的输出内容
|
||
_, err := dbEngine.Table("devcontainer_output").
|
||
Select("output").
|
||
Where("user_id = ? AND repo_id = ? AND list_id = ?", user_id, repo.ID, 4).
|
||
Get(&devContainerOutput)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
devContainerOutput = strings.TrimSuffix(devContainerOutput, "\r\n")
|
||
if newoutput == "\b \b" {
|
||
finalOutput = devContainerOutput[:len(devContainerOutput)-1]
|
||
} else {
|
||
finalOutput = devContainerOutput + newoutput
|
||
}
|
||
_, err = dbEngine.Table("devcontainer_output").
|
||
Where("user_id = ? AND repo_id = ? AND list_id = ?", user_id, repo.ID, 4).
|
||
Update(map[string]interface{}{
|
||
"output": finalOutput + "\r\n",
|
||
})
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return 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
|
||
}
|
||
}
|
||
|
||
// GetDevcontainersList 获取 devcontainer 列表(保持向后兼容)
|
||
func GetDevcontainersList(ctx context.Context, doer *user.User, pageNum, pageSize int) (DevcontainerList, error) {
|
||
opts := FindDevcontainerOptions{
|
||
ListOptions: db.ListOptions{
|
||
Page: pageNum,
|
||
PageSize: pageSize,
|
||
},
|
||
UserID: doer.ID,
|
||
}
|
||
return FindDevcontainers(ctx, opts)
|
||
}
|
||
|
||
// FindDevcontainers 根据选项查询 devcontainer 列表
|
||
func FindDevcontainers(ctx context.Context, opts FindDevcontainerOptions) (DevcontainerList, error) {
|
||
// 0. 构造异常返回时的空数据
|
||
var resultDevContainerListVO = DevcontainerList{
|
||
Page: 0,
|
||
PageSize: 50,
|
||
PageTotalNum: 0,
|
||
ItemTotalNum: 0,
|
||
DevContainers: []DevcontainerListItem{},
|
||
}
|
||
|
||
// 获取用户信息
|
||
if opts.UserID > 0 {
|
||
doer, err := user.GetUserByID(ctx, opts.UserID)
|
||
if err != nil {
|
||
return resultDevContainerListVO, err
|
||
}
|
||
resultDevContainerListVO.UserID = doer.ID
|
||
resultDevContainerListVO.Username = doer.Name
|
||
}
|
||
|
||
opts.ListAll = false // 强制使用分页查询,禁止一次性列举所有 devContainers
|
||
if opts.Page <= 0 { // 未指定页码/无效页码:查询第 1 页
|
||
opts.Page = 1
|
||
}
|
||
if opts.PageSize <= 0 || opts.PageSize > 50 {
|
||
opts.PageSize = 50 // /无效页面大小/超过每页最大限制:自动调整到系统最大开发容器页面大小
|
||
}
|
||
resultDevContainerListVO.Page = opts.Page
|
||
resultDevContainerListVO.PageSize = opts.PageSize
|
||
|
||
// 2. SQL 条件构建
|
||
var sqlCondition builder.Cond = builder.NewCond()
|
||
|
||
// 用户ID过滤(0表示不过滤)
|
||
if opts.UserID > 0 {
|
||
sqlCondition = sqlCondition.And(builder.Eq{"devcontainer.user_id": opts.UserID})
|
||
}
|
||
|
||
// 仓库ID过滤(0表示不过滤)
|
||
if opts.RepoID > 0 {
|
||
sqlCondition = sqlCondition.And(builder.Eq{"devcontainer.repo_id": opts.RepoID})
|
||
}
|
||
|
||
// 执行数据库事务
|
||
err := db.WithTx(ctx, func(ctx context.Context) error {
|
||
// 组织ID过滤 - 需要先查询组织下的所有仓库
|
||
var orgRepoIDs []int64
|
||
if opts.OrgID > 0 {
|
||
var orgRepos []struct {
|
||
ID int64 `xorm:"id"`
|
||
}
|
||
err := db.GetEngine(ctx).
|
||
Table("repository").
|
||
Where("owner_id = ?", opts.OrgID).
|
||
Select("id").
|
||
Find(&orgRepos)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
for _, r := range orgRepos {
|
||
orgRepoIDs = append(orgRepoIDs, r.ID)
|
||
}
|
||
if len(orgRepoIDs) > 0 {
|
||
sqlCondition = sqlCondition.And(builder.In("devcontainer.repo_id", orgRepoIDs))
|
||
} else {
|
||
// 组织下没有仓库,返回空结果
|
||
return nil
|
||
}
|
||
}
|
||
|
||
// 关键词搜索 - 如果有关键词,需要先找到匹配的 repo_id
|
||
var repoIDs []int64
|
||
if opts.Keyword != "" {
|
||
keywordPattern := "%" + opts.Keyword + "%"
|
||
// 先搜索匹配的仓库
|
||
var matchingRepos []struct {
|
||
ID int64 `xorm:"id"`
|
||
}
|
||
repoQuery := db.GetEngine(ctx).Table("repository").Where("name LIKE ?", keywordPattern)
|
||
// 如果有组织过滤,只在组织的仓库中搜索
|
||
if opts.OrgID > 0 && len(orgRepoIDs) > 0 {
|
||
repoQuery = repoQuery.In("id", orgRepoIDs)
|
||
}
|
||
err := repoQuery.Select("id").Find(&matchingRepos)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
for _, r := range matchingRepos {
|
||
repoIDs = append(repoIDs, r.ID)
|
||
}
|
||
|
||
// 如果找到了匹配的仓库,或者容器名称匹配,则添加到条件中
|
||
if len(repoIDs) > 0 {
|
||
sqlCondition = sqlCondition.And(builder.Or(
|
||
builder.Like{"devcontainer.name", keywordPattern},
|
||
builder.In("devcontainer.repo_id", repoIDs),
|
||
))
|
||
} else {
|
||
// 只搜索容器名称
|
||
sqlCondition = sqlCondition.And(builder.Like{"devcontainer.name", keywordPattern})
|
||
}
|
||
}
|
||
|
||
// 查询总数
|
||
countSess := db.GetEngine(ctx).
|
||
Table("devcontainer").
|
||
Where(sqlCondition)
|
||
count, err := countSess.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)))
|
||
|
||
// 查询分页数据 - 使用 xorm 查询,但需要确保字段正确映射
|
||
// 先查询 devcontainer 数据
|
||
var devcontainers []devcontainer_models.Devcontainer
|
||
devcontainerSess := db.GetEngine(ctx).
|
||
Table("devcontainer").
|
||
Where(sqlCondition).
|
||
OrderBy("devcontainer.id ASC")
|
||
|
||
err = db.SetSessionPagination(devcontainerSess, &opts.ListOptions).
|
||
Find(&devcontainers)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
// 然后为每个 devcontainer 查询仓库信息、动态端口,并检测状态变化
|
||
resultDevContainerListVO.DevContainers = make([]DevcontainerListItem, 0, len(devcontainers))
|
||
for _, dc := range devcontainers {
|
||
item := DevcontainerListItem{
|
||
Devcontainer: dc,
|
||
}
|
||
// 查询仓库信息
|
||
repo, repoErr := repo.GetRepositoryByID(ctx, dc.RepoId)
|
||
if repoErr == nil && repo != nil {
|
||
item.RepoName = repo.Name
|
||
item.RepoOwnerName = repo.OwnerName
|
||
item.RepoLink = "/" + repo.OwnerName + "/" + repo.Name
|
||
}
|
||
|
||
// 注意:列表查询时不进行状态检测,只显示数据库中的状态
|
||
// 状态检测可以通过"刷新状态"按钮手动触发
|
||
// 这样可以避免每次列表查询都检测所有容器,提高性能
|
||
|
||
// 动态获取 SSH 端口(如果容器正在运行)
|
||
// 尝试获取端口,不管状态如何(因为状态可能不准确)
|
||
mappedPort, err := GetMappedPort(ctx, dc.Name, "22")
|
||
if err == nil && mappedPort > 0 {
|
||
item.DevcontainerPort = mappedPort
|
||
} else {
|
||
// 如果获取失败,尝试使用数据库中存储的端口(如果有)
|
||
if dc.DevcontainerPort > 0 {
|
||
item.DevcontainerPort = dc.DevcontainerPort
|
||
}
|
||
}
|
||
resultDevContainerListVO.DevContainers = append(resultDevContainerListVO.DevContainers, item)
|
||
}
|
||
|
||
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
|
||
}
|
||
|
||
configurationString, err := GetDevcontainerConfigurationString(ctx, repo.Repository)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
configurationModel, err := UnmarshalDevcontainerConfigContent(configurationString)
|
||
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("terminal_login_token") != nil {
|
||
access_token = ctx.Session.Get("terminal_login_token").(string)
|
||
// 验证 token 是否仍然有效
|
||
_, err := auth_model.GetAccessTokenBySHA(ctx, access_token)
|
||
if err != nil {
|
||
// token 验证失败,删除 session 中的 token 并重新生成
|
||
log.Warn("Get_IDE_TerminalURL: token 验证失败,删除 session 中的 token 并重新生成: %v", err)
|
||
ctx.Session.Delete("terminal_login_token")
|
||
access_token = "" // 清空,后续会重新生成
|
||
}
|
||
}
|
||
|
||
// 如果 session 中没有 token,才创建新的
|
||
if access_token == "" {
|
||
// 检查数据库中是否已存在同名 token
|
||
token := &auth_model.AccessToken{
|
||
UID: devContainerInfo.UserId,
|
||
Name: "terminal_login_token",
|
||
}
|
||
exist, err := auth_model.AccessTokenByNameExists(ctx, token)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
|
||
// 如果数据库中已存在同名 token,先删除旧的(避免 token 堆积)
|
||
// 因为 session 中没有 token,说明这些旧 token 可能已经不被使用了
|
||
if exist {
|
||
// 删除所有同名的旧 token
|
||
_, err = db.GetEngine(ctx).Table("access_token").
|
||
Where("uid = ? AND name = ?", doer.ID, "terminal_login_token").
|
||
Delete(&auth_model.AccessToken{})
|
||
if err != nil {
|
||
log.Warn("Get_IDE_TerminalURL: 删除旧 token 失败: %v", err)
|
||
// 继续创建新 token,不因为删除失败而中断
|
||
} else {
|
||
log.Info("Get_IDE_TerminalURL: 已删除旧的 terminal_login_token,创建新 token")
|
||
}
|
||
}
|
||
|
||
// 生成新 token
|
||
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)
|
||
}
|
||
|
||
// 读取 devcontainer 配置获取 forwardPorts 信息
|
||
|
||
var forwardPortsValue string
|
||
if len(configurationModel.ForwardPorts) > 0 {
|
||
var portStrings []string
|
||
for _, p := range configurationModel.ForwardPorts {
|
||
switch v := p.(type) {
|
||
case float64:
|
||
portStrings = append(portStrings, fmt.Sprintf("%.0f", v))
|
||
case string:
|
||
portStrings = append(portStrings, v)
|
||
case int:
|
||
portStrings = append(portStrings, fmt.Sprintf("%d", v))
|
||
default:
|
||
log.Warn("Get_IDE_TerminalURL: 未知的 forwardPorts 类型: %T", v)
|
||
}
|
||
}
|
||
if len(portStrings) > 0 {
|
||
forwardPortsValue = strings.Join(portStrings, ",")
|
||
}
|
||
}
|
||
|
||
// 构建并返回 URL
|
||
url := "://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()
|
||
|
||
// 添加 forwardPorts 参数(如果存在)
|
||
if forwardPortsValue != "" {
|
||
url += "&forwardPorts=" + forwardPortsValue
|
||
}
|
||
|
||
return url, 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_module.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_module.GetContainerID(cli, repoDevContainer.Name)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
log.Info("container id: %s, name: %s", containerID, repoDevContainer.Name)
|
||
// 检查容器状态
|
||
containerStatus, err := docker_module.GetContainerStatus(cli, containerID)
|
||
if err != nil {
|
||
continue
|
||
}
|
||
|
||
if containerStatus == "running" {
|
||
// 只为处于运行状态的容器添加公钥
|
||
_, err = docker_module.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
|
||
}
|