Files
devstar/services/devcontainer/devcontainer.go
panshuxiao d33b8057cb devcontainer /root /etc/ssh 改为持久化
修改open with vscode返回连接
2025-07-03 08:24:54 +08:00

1100 lines
42 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 (
"context"
"fmt"
"io/ioutil"
"net/url"
"os"
"path/filepath"
"regexp"
"strings"
"time"
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
devcontainer_model "code.gitea.io/gitea/models/devcontainer"
devcontainer_models_errors "code.gitea.io/gitea/models/devcontainer/errors"
"code.gitea.io/gitea/models/perm"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/docker"
devcontainer_k8s_agent_module "code.gitea.io/gitea/modules/k8s"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
gitea_context "code.gitea.io/gitea/services/context"
devcontainer_service_errors "code.gitea.io/gitea/services/devcontainer/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"code.gitea.io/gitea/services/devstar_ssh_key_pair/api_service"
"github.com/google/uuid"
"xorm.io/builder"
)
// 封装查询 DevContainer 实时信息,与具体 agent 无关,返回前端
type OpenDevcontainerAbstractAgent struct {
NodePortAssigned uint16
}
// OpenDevcontainerService 获取 DevContainer 连接信息,抽象方法,适配多种 DevContainer Agent
func OpenDevcontainerService(ctx *gitea_context.Context, opts *OpenDevcontainerAppDispatcherOptions) (*OpenDevcontainerAbstractAgent, error) {
log.Info("OpenDevcontainerService: 开始获取 DevContainer 连接信息 name=%s, wait=%v",
opts.Name, opts.Wait)
// 0. 检查参数
if ctx == nil || opts == nil || len(opts.Name) == 0 {
log.Error("OpenDevcontainerService: 参数无效 ctx=%v, opts=%v", ctx != nil, opts != nil)
return nil, devcontainer_service_errors.ErrIllegalParams{
FieldNameList: []string{"ctx", "opts.Name"},
}
}
// 1. 检查 DevContainer 功能是否开启
if setting.Devcontainer.Enabled == false {
log.Warn("OpenDevcontainerService: DevContainer 功能已全局关闭")
return nil, devcontainer_service_errors.ErrOperateDevcontainer{
Action: "check availability of DevStar DevContainer",
Message: "DevContainer is turned off globally",
}
}
// 2. 根据 DevContainer Agent 类型分发任务
apiRequestContext := ctx.Req.Context()
openDevcontainerAbstractAgentVO := &OpenDevcontainerAbstractAgent{}
switch setting.Devcontainer.Agent {
case setting.KUBERNETES, "k8s":
log.Info("OpenDevcontainerService: 使用 K8s Agent 获取 DevContainer: %s", opts.Name)
devcontainerApp, err := AssignDevcontainerGetting2K8sOperator(&apiRequestContext, opts)
if err != nil {
log.Error("OpenDevcontainerService: K8s DevContainer 获取失败: %v", err)
return nil, devcontainer_service_errors.ErrOperateDevcontainer{
Action: "Open DevContainer in k8s",
Message: err.Error(),
}
}
openDevcontainerAbstractAgentVO.NodePortAssigned = devcontainerApp.Status.NodePortAssigned
log.Info("OpenDevcontainerService: K8s DevContainer 获取成功, name=%s, nodePort=%d, ready=%v",
opts.Name, devcontainerApp.Status.NodePortAssigned, devcontainerApp.Status.Ready)
case setting.DOCKER:
port, err := GetDevcontainer(&apiRequestContext, opts)
log.Info("port %d", port)
if err != nil {
return nil, devcontainer_service_errors.ErrOperateDevcontainer{
Action: "Open DevContainer in docker",
Message: err.Error(),
}
}
openDevcontainerAbstractAgentVO.NodePortAssigned = port
default:
log.Error("OpenDevcontainerService: 未知的 DevContainer Agent 类型: %s", setting.Devcontainer.Agent)
return nil, devcontainer_service_errors.ErrOperateDevcontainer{
Action: "Open DevContainer",
Message: "No Valid DevContainer Agent Found",
}
}
// 3. 封装返回结果
log.Info("OpenDevcontainerService: 获取 DevContainer 连接信息完成, nodePort=%d",
openDevcontainerAbstractAgentVO.NodePortAssigned)
return openDevcontainerAbstractAgentVO, nil
}
// GetRepoDevcontainerDetails 获取仓库对应 DevContainer 信息
func GetRepoDevcontainerDetails(ctx context.Context, opts *RepoDevcontainerOptions) (RepoDevContainer, error) {
log.Info("GetRepoDevcontainerDetails: 开始查询仓库 DevContainer 信息")
// 0. 构造异常返回时候的空数据
resultRepoDevcontainerDetail := RepoDevContainer{}
// 1. 检查参数是否有效
if opts == nil || opts.Actor == nil || opts.Repository == nil {
log.Error("GetRepoDevcontainerDetails: 参数无效 opts=%v, actor=%v, repo=%v",
opts != nil, opts != nil && opts.Actor != nil, opts != nil && opts.Repository != nil)
return resultRepoDevcontainerDetail, devcontainer_models_errors.ErrFailedToOperateDevcontainerDB{
Action: "construct query condition for devContainer user list",
Message: "invalid search condition",
}
}
log.Info("GetRepoDevcontainerDetails: 查询用户=%s (ID=%d) 的仓库=%s (ID=%d) 的 DevContainer",
opts.Actor.Name, opts.Actor.ID, opts.Repository.Name, opts.Repository.ID)
// 2. 查询数据库
_, err := db.GetEngine(ctx).
Table("devcontainer").
Select(""+
"devcontainer.id AS devcontainer_id,"+
"devcontainer.name AS devcontainer_name,"+
"devcontainer.devcontainer_host AS devcontainer_host,"+
"devcontainer.devcontainer_status AS devcontainer_status,"+
"devcontainer.devcontainer_username AS devcontainer_username,"+
"devcontainer.devcontainer_work_dir AS devcontainer_work_dir,"+
"devcontainer.repo_id AS repo_id,"+
"devcontainer.user_id AS user_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").
Join("INNER", "repository", "devcontainer.repo_id = repository.id").
Where("devcontainer.user_id = ? AND devcontainer.repo_id = ?", opts.Actor.ID, opts.Repository.ID).
Get(&resultRepoDevcontainerDetail)
// 3. 返回
if err != nil {
log.Error("GetRepoDevcontainerDetails: 数据库查询失败: %v", err)
return resultRepoDevcontainerDetail, devcontainer_models_errors.ErrFailedToOperateDevcontainerDB{
Action: fmt.Sprintf("query devcontainer with repo '%v' and username '%v'", opts.Repository.Name, opts.Actor.Name),
Message: err.Error(),
}
}
return resultRepoDevcontainerDetail, nil
}
// CreateRepoDevcontainer 创建 DevContainer
/*
必要假设:前置中间件已完成检查,确保数据有效:
- 当前用户为已登录用户
- 当前用户拥有 repo code写入权限
- 数据库此前不存在 该用户在该repo创建的 Dev Container
*/
func CreateRepoDevcontainer(ctx context.Context, opts *CreateRepoDevcontainerOptions) error {
username := opts.Actor.Name
repoName := opts.Repository.Name
log.Info("CreateRepoDevcontainer: 开始创建 DevContainer, user=%s, repo=%s, repoID=%d",
username, repoName, opts.Repository.ID)
// unixTimestamp is the number of seconds elapsed since January 1, 1970 UTC.
unixTimestamp := time.Now().Unix()
log.Info("CreateRepoDevcontainer: 获取 DevContainer JSON 模型")
devContainerJson, err := GetDevcontainerJsonModel(ctx, opts.Repository)
if err != nil {
log.Error("CreateRepoDevcontainer: 获取 DevContainer JSON 失败: %v", err)
return devcontainer_service_errors.ErrOperateDevcontainer{
Action: "Get DevContainer Error",
Message: err.Error(),
}
}
log.Info("CreateRepoDevcontainer: DevContainer JSON 获取成功, image=%s, dockerfilePath=%s",
devContainerJson.Image, devContainerJson.DockerfilePath)
var dockerfileContent string
if devContainerJson.DockerfilePath != "" {
log.Info("CreateRepoDevcontainer: 获取 Dockerfile 内容, path=%s", devContainerJson.DockerfilePath)
dockerfileContent, err = GetDockerfileContent(ctx, opts.Repository)
if err != nil {
log.Error("CreateRepoDevcontainer: 获取 Dockerfile 内容失败: %v", err)
return devcontainer_service_errors.ErrOperateDevcontainer{
Action: "Get DockerFileContent Error",
Message: err.Error(),
}
}
log.Debug("CreateRepoDevcontainer: Dockerfile 内容获取成功, 长度=%d", len(dockerfileContent))
}
cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf)
if err != nil {
log.Error("CreateRepoDevcontainer: 加载配置文件失败: %v", err)
}
containerName := getSanitizedDevcontainerName(username, repoName)
log.Info("CreateRepoDevcontainer: 生成 DevContainer 名称: %s", containerName)
newDevcontainer := &CreateDevcontainerDTO{
Devcontainer: devcontainer_model.Devcontainer{
Name: containerName,
DevcontainerHost: cfg.Section("server").Key("DOMAIN").Value(),
DevcontainerUsername: "root",
DevcontainerWorkDir: "/data/workspace",
DevcontainerStatus: 0,
RepoId: opts.Repository.ID,
UserId: opts.Actor.ID,
CreatedUnix: unixTimestamp,
UpdatedUnix: unixTimestamp,
},
DockerfileContent: dockerfileContent,
Image: devContainerJson.Image,
GitRepositoryURL: strings.TrimSuffix(setting.AppURL, "/") + opts.Repository.Link(),
}
log.Info("CreateRepoDevcontainer: 初始化 DevContainer 对象, host=%s, workDir=%s, gitURL=%s",
newDevcontainer.DevcontainerHost, newDevcontainer.DevcontainerWorkDir, newDevcontainer.GitRepositoryURL)
if setting.Devcontainer.Agent == setting.DOCKER {
rowsAffect, err := db.GetEngine(ctx).
Table("devcontainer").
Insert(newDevcontainer.Devcontainer)
if err != nil {
return devcontainer_models_errors.ErrFailedToOperateDevcontainerDB{
Action: fmt.Sprintf("insert new DevContainer for user '%s' in repo '%s'", username, repoName),
Message: err.Error(),
}
} else if rowsAffect == 0 {
return devcontainer_models_errors.ErrFailedToOperateDevcontainerDB{
Action: fmt.Sprintf("insert new DevContainer for user '%s' in repo '%s'", username, repoName),
Message: "expected 1 row to be inserted, but got 0",
}
}
}
// 在数据库事务中创建 Dev Container 分配资源,出错时自动回滚相对应数据库字段,保证数据一致
dbTransactionErr := db.WithTx(ctx, func(ctx context.Context) error {
var err error
log.Info("CreateRepoDevcontainer: 开始数据库事务")
// 0. 查询数据库,收集用户 SSH 公钥合并用户临时填入SSH公钥
log.Info("CreateRepoDevcontainer: 查询用户 SSH 公钥, userID=%d", opts.Actor.ID)
var userSSHPublicKeyList []string
err = db.GetEngine(ctx).
Table("public_key").
Select("content").
Where("owner_id = ?", opts.Actor.ID).
Find(&userSSHPublicKeyList)
if err != nil {
log.Error("CreateRepoDevcontainer: 查询用户 SSH 公钥失败: %v", err)
return devcontainer_service_errors.ErrOperateDevcontainer{
Action: fmt.Sprintf("query SSH Public Key List for User %s", opts.Actor.Name),
Message: err.Error(),
}
}
log.Info("CreateRepoDevcontainer: 找到用户 SSH 公钥 %d 个", len(userSSHPublicKeyList))
newDevcontainer.SSHPublicKeyList = append(userSSHPublicKeyList, opts.SSHPublicKeyList...)
log.Info("CreateRepoDevcontainer: 合并 SSH 公钥后共 %d 个", len(newDevcontainer.SSHPublicKeyList))
devstarPublicKey := getDevStarPublicKey()
if devstarPublicKey == "" {
log.Error("CreateRepoDevcontainer: 获取 DevStar SSH 公钥失败")
return devcontainer_service_errors.ErrOperateDevcontainer{
Action: fmt.Sprintf("devstar SSH Public Key Error "),
Message: err.Error(),
}
}
log.Info("CreateRepoDevcontainer: 获取 DevStar SSH 公钥成功")
newDevcontainer.SSHPublicKeyList = append(newDevcontainer.SSHPublicKeyList)
// 1. 调用 k8s Agent创建 DevContainer 资源同时更新k8s调度器分配的 NodePort
if setting.Devcontainer.Agent == setting.KUBERNETES || setting.Devcontainer.Agent == "k8s" {
log.Info("CreateRepoDevcontainer: 调用 K8s controller 创建 DevContainer 资源")
}
err = claimDevcontainerResource(&ctx, newDevcontainer, devContainerJson)
if err != nil {
if setting.Devcontainer.Agent == setting.KUBERNETES || setting.Devcontainer.Agent == "k8s" {
log.Error("CreateRepoDevcontainer: K8s controller 创建失败: %v", err)
}
return devcontainer_models_errors.ErrFailedToOperateDevcontainerDB{
Action: fmt.Sprintf("claim resource for Dev Container %v", newDevcontainer),
Message: err.Error(),
}
}
if setting.Devcontainer.Agent == setting.KUBERNETES || setting.Devcontainer.Agent == "k8s" {
log.Info("CreateRepoDevcontainer: K8s controller 创建成功, nodePort=%d", newDevcontainer.DevcontainerPort)
// 2. 根据分配的 NodePort 更新数据库字段
log.Info("CreateRepoDevcontainer: 在数据库中创建 DevContainer 记录, nodePort=%d",
newDevcontainer.DevcontainerPort)
rowsAffect, err := db.GetEngine(ctx).
Table("devcontainer").
Insert(newDevcontainer.Devcontainer)
if err != nil {
log.Error("CreateRepoDevcontainer: 数据库插入失败: %v", err)
return devcontainer_models_errors.ErrFailedToOperateDevcontainerDB{
Action: fmt.Sprintf("insert new DevContainer for user '%s' in repo '%s'", username, repoName),
Message: err.Error(),
}
} else if rowsAffect == 0 {
log.Error("CreateRepoDevcontainer: 数据库插入失败: 影响行数为0")
return devcontainer_models_errors.ErrFailedToOperateDevcontainerDB{
Action: fmt.Sprintf("insert new DevContainer for user '%s' in repo '%s'", username, repoName),
Message: "expected 1 row to be inserted, but got 0",
}
}
log.Info("CreateRepoDevcontainer: 数据库插入成功, 影响行数=%d", rowsAffect)
}
return nil
})
if dbTransactionErr != nil {
log.Error("CreateRepoDevcontainer: 创建失败: %v", dbTransactionErr)
return dbTransactionErr
}
log.Info("CreateRepoDevcontainer: DevContainer 创建成功, name=%s", newDevcontainer.Name)
return nil
}
func getDevStarPublicKey() string {
// 获取当前用户的主目录
homeDir, err := os.UserHomeDir()
if err != nil {
log.Info("Failed to get home directory: %s", err)
}
// 构建公钥文件的路径
publicKeyPath := filepath.Join(homeDir, ".ssh", "id_rsa_devstar.pub")
privateKeyPath := filepath.Join(homeDir, ".ssh", "id_rsa_devstar")
if !fileExists(publicKeyPath) || !fileExists(privateKeyPath) {
err, key := api_service.GenerateNewRSASSHSessionKeyPair()
if err != nil {
log.Info("无法创建密钥:", err)
return ""
}
// 确保~/.ssh目录存在
sshDir := filepath.Join(homeDir, ".ssh")
if err := os.MkdirAll(sshDir, 0700); err != nil {
log.Info("无法创建~/.ssh目录:", err)
return ""
}
// 创建密钥文件
if err := ioutil.WriteFile(privateKeyPath, []byte(key.PrivateKeyPEM), 0600); err != nil {
log.Info("无法写入私钥文件:", err)
return ""
}
if err := ioutil.WriteFile(publicKeyPath, []byte(key.PublicKeySsh), 0600); err != nil {
log.Info("无法写入公钥文件:", err)
return ""
}
}
// 读取公钥文件内容
publicKeyBytes, err := ioutil.ReadFile(publicKeyPath)
if err != nil {
log.Info("Failed to read public key file: %s", err)
return ""
}
// 将文件内容转换为字符串
return string(publicKeyBytes)
}
func fileExists(filename string) bool {
info, err := os.Stat(filename)
if os.IsNotExist(err) {
return false
}
return !info.IsDir()
}
func GetWebTerminalURL(ctx context.Context, devcontainerName string) (string, error) {
switch setting.Devcontainer.Agent {
case setting.KUBERNETES, "k8s":
log.Info("GetWebTerminalURL: 开始查找 K8s DevContainer ttyd 端口, name=%s", devcontainerName)
// 创建 K8s 客户端,直接查询 CRD 以获取 ttyd 端口
k8sClient, err := devcontainer_k8s_agent_module.GetKubernetesClient(&ctx)
if err != nil {
log.Error("GetWebTerminalURL: 获取 K8s 客户端失败: %v", err)
return "", err
}
log.Info("GetWebTerminalURL: K8s 客户端创建成功")
// 直接从K8s获取CRD信息不依赖数据库
opts := &devcontainer_k8s_agent_module.GetDevcontainerOptions{
GetOptions: metav1.GetOptions{},
Name: devcontainerName,
Namespace: setting.Devcontainer.Namespace,
Wait: false,
}
log.Info("GetWebTerminalURL: 从 K8s 获取 DevcontainerApp %s, namespace=%s",
devcontainerName, setting.Devcontainer.Namespace)
devcontainerApp, err := devcontainer_k8s_agent_module.GetDevcontainer(&ctx, k8sClient, opts)
if err != nil {
log.Error("GetWebTerminalURL: 获取 DevcontainerApp 失败: %v", err)
return "", err
}
log.Info("GetWebTerminalURL: 成功获取 DevcontainerApp, extraPorts=%d",
len(devcontainerApp.Status.ExtraPortsAssigned))
// 在额外端口中查找 ttyd 端口,使用多个条件匹配
var ttydNodePort uint16 = 0
for _, portInfo := range devcontainerApp.Status.ExtraPortsAssigned {
// 检查各种可能的情况名称为ttyd、名称包含ttyd、名称为port-7681、端口为7681
log.Debug("GetWebTerminalURL: 检查端口 name=%s, containerPort=%d, nodePort=%d",
portInfo.Name, portInfo.ContainerPort, portInfo.NodePort)
if portInfo.Name == "ttyd" ||
strings.Contains(portInfo.Name, "ttyd") ||
portInfo.Name == "port-7681" ||
portInfo.ContainerPort == 7681 {
ttydNodePort = portInfo.NodePort
log.Info("GetWebTerminalURL: 找到 ttyd 端口: %d, 名称: %s", ttydNodePort, portInfo.Name)
break
}
}
// 如果找到 ttyd 端口,构建 URL
if ttydNodePort > 0 {
cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf)
if err != nil {
log.Error("GetWebTerminalURL: 加载配置文件失败: %v", err)
return "", err
}
// 检查是否启用了基于路径的访问方式
domain := cfg.Section("server").Key("DOMAIN").Value()
scheme := "https"
// 从容器名称中提取用户名和仓库名
parts := strings.Split(devcontainerName, "-")
if len(parts) >= 2 {
username := parts[0]
repoName := parts[1]
// 构建访问路径
path := fmt.Sprintf("/%s/%s/dev-container-webterminal", username, repoName)
terminalURL := fmt.Sprintf("%s://%s%s", scheme, domain, path)
log.Info("GetWebTerminalURL: 使用 Ingress 路径方式生成 ttyd URL: %s", terminalURL)
return terminalURL, nil
}
}
// 如果没有找到ttyd端口记录详细的调试信息
log.Warn("GetWebTerminalURL: 未找到 ttyd 端口 (7681), 可用的额外端口: %v",
devcontainerApp.Status.ExtraPortsAssigned)
return "", fmt.Errorf("ttyd port (7681) not found for container: %s", devcontainerName)
case setting.DOCKER:
cli, err := docker.CreateDockerClient(&ctx)
if err != nil {
return "", err
}
defer cli.Close()
containerID, err := docker.GetContainerID(cli, devcontainerName)
if err != nil {
return "", err
}
port, err := docker.GetMappedPort(cli, containerID, "7681")
if err != nil {
return "", err
}
cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf)
if err != nil {
log.Error("Failed to load custom conf '%s': %v", setting.CustomConf, err)
return "", err
}
return "http://" + cfg.Section("server").Key("DOMAIN").Value() + ":" + port + "/", nil
default:
return "", fmt.Errorf("unknown agent")
}
}
func Get_IDE_TerminalURL(ctx *gitea_context.Context, devcontainer *RepoDevContainer) (string, error) {
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: devcontainer.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 = ?", devcontainer.UserId, "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
switch setting.Devcontainer.Agent {
case setting.KUBERNETES, "k8s":
// 创建 K8s 客户端
apiRequestContext := ctx.Req.Context()
k8sClient, err := devcontainer_k8s_agent_module.GetKubernetesClient(&apiRequestContext)
if err != nil {
log.Error("Get_IDE_TerminalURL: 创建 K8s 客户端失败: %v", err)
return "", err
}
// 获取 DevcontainerApp 资源
opts := &devcontainer_k8s_agent_module.GetDevcontainerOptions{
GetOptions: metav1.GetOptions{},
Name: devcontainer.DevContainerName,
Namespace: setting.Devcontainer.Namespace,
Wait: false,
}
log.Info("Get_IDE_TerminalURL: 从 K8s 获取 DevcontainerApp %s, namespace=%s",
devcontainer.DevContainerName, setting.Devcontainer.Namespace)
devcontainerApp, err := devcontainer_k8s_agent_module.GetDevcontainer(&apiRequestContext, k8sClient, opts)
if err != nil {
log.Error("Get_IDE_TerminalURL: 获取 DevcontainerApp 失败: %v", err)
return "", err
}
// 使用 NodePort 作为 SSH 端口
port = fmt.Sprintf("%d", devcontainerApp.Status.NodePortAssigned)
log.Info("Get_IDE_TerminalURL: K8s 环境使用 NodePort %s 作为 SSH 端口", port)
case setting.DOCKER:
// 原有 Docker 处理逻辑
defalut_ctx := context.Background()
cli, err := docker.CreateDockerClient(&defalut_ctx)
if err != nil {
return "", err
}
defer cli.Close()
containerID, err := docker.GetContainerID(cli, devcontainer.DevContainerName)
if err != nil {
return "", err
}
mappedPort, err := docker.GetMappedPort(cli, containerID, "22")
if err != nil {
return "", err
}
port = mappedPort
default:
return "", fmt.Errorf("不支持的 DevContainer Agent 类型: %s", setting.Devcontainer.Agent)
}
// 加载配置文件
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())
// 构建并返回 URL
return "://mengning.devstar/" +
"openProject?host=" + devcontainer.RepoName +
"&hostname=" + devcontainer.DevContainerHost +
"&port=" + port +
"&username=" + devcontainer.DevContainerUsername +
"&path=" + devcontainer.DevContainerWorkDir +
"&access_token=" + access_token +
"&devstar_username=" + devcontainer.RepoOwnerName +
"&devstar_domain=" + cfg.Section("server").Key("ROOT_URL").Value(), nil
}
func AddPublicKeyToAllRunningDevContainer(ctx context.Context, user *user_model.User, publicKey string) error {
switch setting.Devcontainer.Agent {
case setting.KUBERNETES, "k8s":
log.Info("AddPublicKeyToAllRunningDevContainer: 开始为用户 %s (ID=%d) 的所有运行中容器添加公钥",
user.Name, user.ID)
// 1. 获取用户的所有 DevContainer
opts := &SearchUserDevcontainerListItemVoOptions{
Actor: user,
}
userDevcontainersVO, err := GetUserDevcontainersList(ctx, opts)
if err != nil {
log.Error("AddPublicKeyToAllRunningDevContainer: 获取用户容器列表失败: %v", err)
return err
}
repoDevContainerList := userDevcontainersVO.DevContainers
if len(repoDevContainerList) == 0 {
log.Info("AddPublicKeyToAllRunningDevContainer: 用户 %s 没有任何 DevContainer", user.Name)
return nil
}
log.Info("AddPublicKeyToAllRunningDevContainer: 找到 %d 个 DevContainer", len(repoDevContainerList))
// 2. 获取 K8s 客户端
k8sClient, err := devcontainer_k8s_agent_module.GetKubernetesClient(&ctx)
if err != nil {
log.Error("AddPublicKeyToAllRunningDevContainer: 获取 K8s 客户端失败: %v", err)
return err
}
// 3. 获取标准 K8s 客户端用于执行命令
stdClient, err := getStandardKubernetesClient()
if err != nil {
log.Error("AddPublicKeyToAllRunningDevContainer: 获取标准 K8s 客户端失败: %v", err)
return err
}
// 4. 遍历所有容器,检查状态并添加公钥
successCount := 0
errorCount := 0
for _, repoDevContainer := range repoDevContainerList {
log.Info("AddPublicKeyToAllRunningDevContainer: 处理容器 %s", repoDevContainer.DevContainerName)
// 4.1 检查 DevContainer 是否运行
getOpts := &devcontainer_k8s_agent_module.GetDevcontainerOptions{
GetOptions: metav1.GetOptions{},
Name: repoDevContainer.DevContainerName,
Namespace: setting.Devcontainer.Namespace,
Wait: false,
}
devcontainerApp, err := devcontainer_k8s_agent_module.GetDevcontainer(&ctx, k8sClient, getOpts)
if err != nil {
log.Error("AddPublicKeyToAllRunningDevContainer: 获取容器 %s 状态失败: %v",
repoDevContainer.DevContainerName, err)
errorCount++
continue
}
// 4.2 检查容器是否就绪
if !devcontainerApp.Status.Ready {
log.Info("AddPublicKeyToAllRunningDevContainer: 容器 %s 未就绪,跳过",
repoDevContainer.DevContainerName)
continue
}
log.Info("AddPublicKeyToAllRunningDevContainer: 容器 %s 就绪,开始添加公钥",
repoDevContainer.DevContainerName)
// 4.3 构建添加公钥的命令
// 使用更安全的方式添加公钥,避免重复添加
addKeyCommand := fmt.Sprintf(`
# 确保 .ssh 目录存在
mkdir -p ~/.ssh
chmod 700 ~/.ssh
# 检查公钥是否已存在
if ! grep -Fxq "%s" ~/.ssh/authorized_keys 2>/dev/null; then
echo "%s" >> ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys
echo "Public key added successfully"
else
echo "Public key already exists"
fi
# 验证文件内容
wc -l ~/.ssh/authorized_keys
`, publicKey, publicKey)
// 4.4 在容器中执行命令
err = executeCommandInK8sPod(&ctx, stdClient,
setting.Devcontainer.Namespace,
repoDevContainer.DevContainerName, // 传递 DevContainer 名称而不是 Pod 名称
repoDevContainer.DevContainerName, // 容器名通常与 DevContainer 名相同
[]string{"/bin/bash", "-c", addKeyCommand})
if err != nil {
log.Error("AddPublicKeyToAllRunningDevContainer: 在容器 %s 中执行添加公钥命令失败: %v",
repoDevContainer.DevContainerName, err)
errorCount++
} else {
log.Info("AddPublicKeyToAllRunningDevContainer: 成功为容器 %s 添加公钥",
repoDevContainer.DevContainerName)
successCount++
}
}
log.Info("AddPublicKeyToAllRunningDevContainer: 完成处理 - 成功: %d, 失败: %d",
successCount, errorCount)
if errorCount > 0 && successCount == 0 {
return fmt.Errorf("所有容器添加公钥都失败了,错误数量: %d", errorCount)
}
return nil
case setting.DOCKER:
cli, err := docker.CreateDockerClient(&ctx)
if err != nil {
return err
}
defer cli.Close()
// 查询所有打开的容器
opts := &SearchUserDevcontainerListItemVoOptions{
Actor: user,
}
userDevcontainersVO, err := GetUserDevcontainersList(ctx, opts)
if err != nil {
return err
}
repoDevContainerlist := userDevcontainersVO.DevContainers
if len(repoDevContainerlist) > 0 {
// 将公钥写入这些打开的容器中
for _, repoDevContainer := range repoDevContainerlist {
containerID, err := docker.GetContainerID(cli, repoDevContainer.DevContainerName)
if err != nil {
return err
}
log.Info("container id: %s, name: %s", containerID, repoDevContainer.DevContainerName)
// 检查容器状态
containerStatus, err := docker.GetContainerStatus(cli, containerID)
if err != nil {
continue
}
if containerStatus == "running" {
// 只为处于运行状态的容器添加公钥
_, err = docker.ExecCommandInContainer(&ctx, cli, containerID, fmt.Sprintf("echo '%s' >> ~/.ssh/authorized_keys", publicKey))
if err != nil {
return err
}
}
}
}
return nil
default:
return fmt.Errorf("unknown agent: %s", setting.Devcontainer.Agent)
}
}
// DeleteRepoDevcontainer 按照 仓库 和/或 用户信息删除 DevContainer(s)
func DeleteRepoDevcontainer(ctx context.Context, opts *RepoDevcontainerOptions) error {
log.Info("DeleteRepoDevcontainer: 开始删除 DevContainer")
if ctx == nil || opts == nil || (opts.Actor == nil && opts.Repository == nil) {
log.Error("DeleteRepoDevcontainer: 参数无效 ctx=%v, opts=%v, actor=%v, repo=%v",
ctx != nil, opts != nil,
opts != nil && opts.Actor != nil,
opts != nil && opts.Repository != nil)
return devcontainer_models_errors.ErrFailedToOperateDevcontainerDB{
Action: "construct query parameters",
Message: "Invalid parameters",
}
}
// 1. 构造查询条件
sqlDevcontainerCondition := builder.NewCond()
if opts.Actor != nil {
sqlDevcontainerCondition = sqlDevcontainerCondition.And(builder.Eq{"user_id": opts.Actor.ID})
log.Info("DeleteRepoDevcontainer: 添加用户条件, userID=%d", opts.Actor.ID)
}
if opts.Repository != nil {
sqlDevcontainerCondition = sqlDevcontainerCondition.And(builder.Eq{"repo_id": opts.Repository.ID})
log.Info("DeleteRepoDevcontainer: 添加仓库条件, repoID=%d", opts.Repository.ID)
}
log.Info("DeleteRepoDevcontainer: 查询条件构建完成: %v", sqlDevcontainerCondition)
var devcontainersList []devcontainer_model.Devcontainer
// 2. 开启事务:先获取 devcontainer列表后删除
dbTransactionErr := db.WithTx(ctx, func(ctx context.Context) error {
var err error
log.Info("DeleteRepoDevcontainer: 开始数据库事务")
// 2.1 条件查询: user_id 和/或 repo_id
log.Info("DeleteRepoDevcontainer: 查询符合条件的 DevContainer")
err = db.GetEngine(ctx).
Table("devcontainer").
Where(sqlDevcontainerCondition).
Find(&devcontainersList)
if err != nil {
log.Error("DeleteRepoDevcontainer: 查询失败: %v", err)
return devcontainer_models_errors.ErrFailedToOperateDevcontainerDB{
Action: fmt.Sprintf("find devcontainer(s) with condition '%v'", sqlDevcontainerCondition),
Message: err.Error(),
}
}
log.Info("DeleteRepoDevcontainer: 找到 %d 个符合条件的 DevContainer", len(devcontainersList))
// 2.2 空列表,直接结束事务(由于前一个操作只是查询,所以回滚事务不会导致数据不一致问题)
if len(devcontainersList) == 0 {
log.Warn("DeleteRepoDevcontainer: 未找到符合条件的 DevContainer")
return devcontainer_models_errors.ErrFailedToOperateDevcontainerDB{
Action: fmt.Sprintf("find devcontainer(s) with condition '%v'", sqlDevcontainerCondition),
Message: "No DevContainer found",
}
}
// 2.3 条件删除: user_id 和/或 repo_id
log.Info("DeleteRepoDevcontainer: 从数据库删除 DevContainer 记录")
rowsAffected, err := db.GetEngine(ctx).
Table("devcontainer").
Where(sqlDevcontainerCondition).
Delete()
if err != nil {
log.Error("DeleteRepoDevcontainer: 删除 DevContainer 记录失败: %v", err)
return devcontainer_models_errors.ErrFailedToOperateDevcontainerDB{
Action: fmt.Sprintf("MARK devcontainer(s) as DELETED with condition '%v'", sqlDevcontainerCondition),
Message: err.Error(),
}
}
log.Info("DeleteRepoDevcontainer: DevContainer 记录删除成功, 影响行数=%d", rowsAffected)
// 删除对应的输出记录
log.Info("DeleteRepoDevcontainer: 删除 DevContainer 输出记录")
outputRowsAffected, err := db.GetEngine(ctx).
Table("devcontainer_output").
Where(sqlDevcontainerCondition).
Delete()
if err != nil {
log.Error("DeleteRepoDevcontainer: 删除输出记录失败: %v", err)
return err
}
log.Info("DeleteRepoDevcontainer: DevContainer 输出记录删除成功, 影响行数=%d", outputRowsAffected)
return nil
})
if dbTransactionErr != nil {
log.Error("DeleteRepoDevcontainer: 数据库操作失败: %v", dbTransactionErr)
return dbTransactionErr
}
// 3. 后台启动一个goroutine慢慢回收 Dev Container 资源 (如果回收失败,将会产生孤儿 Dev Container只能管理员手动识别、删除)
log.Info("DeleteRepoDevcontainer: 启动异步资源回收, DevContainer数量=%d", len(devcontainersList))
go func() {
// 注意:由于执行删除 k8s 资源 与 数据库交互和Web页面更新是异步的因此在 goroutine 中必须重新创建 context否则报错
// Delete "https://192.168.49.2:8443/apis/devcontainer.devstar.cn/v1/...": context canceled
isolatedContextToPurgeK8sResource, cancelFunc := context.WithCancel(context.Background())
defer cancelFunc()
err := purgeDevcontainersResource(&isolatedContextToPurgeK8sResource, &devcontainersList)
if err != nil {
log.Error("DeleteRepoDevcontainer: 异步资源回收失败: %v", err)
}
}()
log.Info("DeleteRepoDevcontainer: DevContainer 删除操作完成")
return dbTransactionErr
}
// getSanitizedDevcontainerName 辅助获取当前用户在当前仓库创建的 devContainer名称
// DevContainer命名规则 用户名、仓库名各由小于15位的小写字母和数字组成中间使用'-'分隔,后面使用'-'分隔符拼接32位UUID小写字母数字字符串
func getSanitizedDevcontainerName(username, repoName string) string {
regexpNonAlphaNum := regexp.MustCompile(`[^a-zA-Z0-9]`)
sanitizedUsername := regexpNonAlphaNum.ReplaceAllString(username, "")
sanitizedRepoName := regexpNonAlphaNum.ReplaceAllString(repoName, "")
if len(sanitizedUsername) > 15 {
sanitizedUsername = strings.ToLower(sanitizedUsername[:15])
}
if len(sanitizedRepoName) > 31 {
sanitizedRepoName = strings.ToLower(sanitizedRepoName[:31])
}
newUUID, _ := uuid.NewUUID()
uuidStr := newUUID.String()
uuidStr = regexpNonAlphaNum.ReplaceAllString(uuidStr, "")[:15]
return fmt.Sprintf("%s-%s-%s", sanitizedUsername, sanitizedRepoName, uuidStr)
}
// purgeDevcontainersResource 辅助函数用于goroutine后台执行回收DevContainer资源
func purgeDevcontainersResource(ctx *context.Context, devcontainersList *[]devcontainer_model.Devcontainer) error {
// 1. 检查 DevContainer 功能是否启用,若禁用,则直接结束,不会真正执行删除操作
if !setting.Devcontainer.Enabled {
log.Warn("purgeDevcontainersResource: DevContainer 功能已全局禁用, 跳过资源回收")
// 如果用户设置禁用 DevContainer无法删除资源会直接忽略而数据库相关记录会继续清空、不会发生回滚
log.Warn("Orphan DevContainers in namespace `%s` left undeleted: %v", setting.Devcontainer.Namespace, devcontainersList)
return nil
}
// 2. 根据配置文件中指定的 DevContainer Agent 派遣创建任务
switch setting.Devcontainer.Agent {
case setting.KUBERNETES, "k8s":
log.Info("purgeDevcontainersResource: 调用 K8s Operator 删除 %d 个资源", len(*devcontainersList))
err := AssignDevcontainerDeletion2K8sOperator(ctx, devcontainersList)
if err != nil {
log.Error("purgeDevcontainersResource: K8s 资源删除失败: %v", err)
} else {
log.Info("purgeDevcontainersResource: K8s 资源删除成功")
}
return err
case setting.DOCKER:
return DeleteDevcontainer(ctx, devcontainersList)
default:
// 未知 Agent直接报错
return devcontainer_models_errors.ErrFailedToOperateDevcontainerDB{
Action: "dispatch DevContainer deletion",
Message: fmt.Sprintf("unknown DevContainer agent `%s`, please check your config files", setting.Devcontainer.Agent),
}
}
}
// claimDevcontainerResource 分发创建 DevContainer 任务到配置文件指定的执行器
func claimDevcontainerResource(ctx *context.Context, newDevContainer *CreateDevcontainerDTO, devContainerJSON *DevStarJSON) error {
log.Info("claimDevcontainerResource: 开始分发 DevContainer 创建任务, name=%s", newDevContainer.Name)
// 1. 检查 DevContainer 功能是否启用,若禁用,则直接结束
if !setting.Devcontainer.Enabled {
log.Error("claimDevcontainerResource: DevContainer 功能已全局禁用")
return devcontainer_models_errors.ErrFailedToOperateDevcontainerDB{
Action: "Check for DevContainer functionality switch",
Message: "DevContainer is disabled globally, please check your configuration files",
}
}
// 解析仓库 URL
parsedURL, err := url.Parse(newDevContainer.GitRepositoryURL)
if err != nil {
log.Info("解析仓库URL失败: %v", err)
return err
}
hostParts := strings.Split(parsedURL.Host, ":")
port := ""
if len(hostParts) > 1 {
port = hostParts[1]
}
newHost := "host.docker.internal"
if port != "" {
newHost += ":" + port
}
parsedURL.Host = newHost
// 生成git仓库的 URL
newURL := parsedURL.String()
// Read the init script from file
var initializeScriptContent, restartScriptContent []byte
_, err = os.Stat("devcontainer_init.sh")
if os.IsNotExist(err) {
_, err = os.Stat("/app/gitea/devcontainer_init.sh")
if os.IsNotExist(err) {
return fmt.Errorf("读取初始化脚本失败: %v", err)
} else {
initializeScriptContent, err = os.ReadFile("/app/gitea/devcontainer_init.sh")
if err != nil {
return fmt.Errorf("读取初始化脚本失败: %v", err)
}
}
} else {
initializeScriptContent, err = os.ReadFile("devcontainer_init.sh")
if err != nil {
return fmt.Errorf("读取初始化脚本失败: %v", err)
}
}
_, err = os.Stat("devcontainer_restart.sh")
if os.IsNotExist(err) {
_, err = os.Stat("/app/gitea/devcontainer_restart.sh")
if os.IsNotExist(err) {
return fmt.Errorf("读取初始化脚本失败: %v", err)
} else {
restartScriptContent, err = os.ReadFile("/app/gitea/devcontainer_restart.sh")
if err != nil {
return fmt.Errorf("读取初始化脚本失败: %v", err)
}
}
} else {
restartScriptContent, err = os.ReadFile("devcontainer_restart.sh")
if err != nil {
return fmt.Errorf("读取初始化脚本失败: %v", err)
}
}
cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf)
if err != nil {
log.Error("Failed to load custom conf '%s': %v", setting.CustomConf, err)
return err
}
initializeScript := strings.ReplaceAll(string(initializeScriptContent), "$AUTHORIZED_KEYS", strings.Join(newDevContainer.SSHPublicKeyList, "\n"))
initializeScript = strings.ReplaceAll(initializeScript, "$HOST_DOCKER_INTERNAL", cfg.Section("server").Key("DOMAIN").Value())
initializeScript = strings.ReplaceAll(initializeScript, "$WORKDIR", newDevContainer.DevcontainerWorkDir)
initializeScript = strings.ReplaceAll(initializeScript, "$REPO_URL", newURL)
restartScript := strings.ReplaceAll(string(restartScriptContent), "$WORKDIR", newDevContainer.DevcontainerWorkDir)
// 2. 根据配置文件中指定的 DevContainer Agent 派遣创建任务
log.Info("claimDevcontainerResource: 使用 %s Agent 创建 DevContainer", setting.Devcontainer.Agent)
switch setting.Devcontainer.Agent {
case setting.KUBERNETES, "k8s":
// k8s Operator
log.Info("claimDevcontainerResource: 调用 K8s Operator 创建 DevContainer, image=%s",
newDevContainer.Image)
err := AssignDevcontainerCreation2K8sOperator(ctx, newDevContainer)
if err != nil {
log.Error("claimDevcontainerResource: K8s 创建 DevContainer 失败: %v", err)
} else {
log.Info("claimDevcontainerResource: K8s 创建 DevContainer 成功, nodePort=%d",
newDevContainer.DevcontainerPort)
}
return err
case setting.DOCKER:
return CreateDevcontainer(ctx, newDevContainer, devContainerJSON, initializeScript, restartScript)
default:
// 未知 Agent直接报错
return devcontainer_models_errors.ErrFailedToOperateDevcontainerDB{
Action: "dispatch DevContainer creation",
Message: fmt.Sprintf("unknown DevContainer agent `%s`, please check your config files", setting.Devcontainer.Agent),
}
}
}
func RestartDevcontainer(gitea_ctx gitea_context.Context, opts *RepoDevContainer) error {
log.Info("RestartDevcontainer: 开始重启 DevContainer, name=%s", opts.DevContainerName)
switch setting.Devcontainer.Agent {
case setting.KUBERNETES, "k8s":
log.Info("RestartDevcontainer: 使用 K8s Agent 重启容器 %s", opts.DevContainerName)
ctx := gitea_ctx.Req.Context()
err := AssignDevcontainerRestart2K8sOperator(&ctx, opts)
if err != nil {
log.Error("RestartDevcontainer: K8s 重启容器失败: %v", err)
} else {
log.Info("RestartDevcontainer: K8s 重启容器成功")
}
return err
case setting.DOCKER:
return DockerRestartContainer(&gitea_ctx, opts)
default:
return fmt.Errorf("不支持的Agent")
//默认处理
}
}
func StopDevcontainer(gitea_ctx context.Context, opts *RepoDevContainer) error {
log.Info("StopDevcontainer: 开始停止 DevContainer, name=%s", opts.DevContainerName)
switch setting.Devcontainer.Agent {
case setting.KUBERNETES, "k8s":
log.Info("StopDevcontainer: 使用 K8s Agent 停止容器 %s", opts.DevContainerName)
err := AssignDevcontainerStop2K8sOperator(&gitea_ctx, opts)
if err != nil {
log.Error("StopDevcontainer: K8s 停止容器失败: %v", err)
} else {
log.Info("StopDevcontainer: K8s 停止容器成功")
}
return err
case setting.DOCKER:
return DockerStopContainer(&gitea_ctx, opts)
default:
return fmt.Errorf("不支持的Agent")
//默认处理
}
}
func CanCreateDevcontainer(gitea_ctx context.Context, repoID, userID int64) (bool, error) {
e := db.GetEngine(gitea_ctx)
teamMember, err := e.Table("team_user").
Join("INNER", "team_repo", "`team_repo`.team_id = `team_user`.team_id").
Join("INNER", "team_unit", "`team_unit`.team_id = `team_user`.team_id").
Where("`team_repo`.repo_id = ? AND (`team_unit`.access_mode >= ? OR (`team_unit`.access_mode = ? AND `team_unit`.`type` = ?))",
repoID, perm.AccessModeWrite, perm.AccessModeRead, unit.TypeCode).
And("team_user.uid = ?", userID).Exist()
if err != nil {
return false, nil
}
if teamMember {
return true, nil
}
return repo_model.IsCollaborator(gitea_ctx, repoID, userID)
}
func IsOwner(gitea_ctx context.Context, repoID, userID int64) (bool, error) {
e := db.GetEngine(gitea_ctx)
teamMember, err := e.Table("team_user").
Join("INNER", "team_repo", "`team_repo`.team_id = `team_user`.team_id").
Join("INNER", "team_unit", "`team_unit`.team_id = `team_user`.team_id").
Where("`team_repo`.repo_id = ? AND `team_unit`.access_mode = ? ",
repoID, perm.AccessModeAdmin).
And("team_user.uid = ?", userID).Exist()
if err != nil {
return false, nil
}
if teamMember {
return true, nil
}
return e.Get(&repo_model.Collaboration{RepoID: repoID, UserID: userID, Mode: 3})
}