Files
devstar/services/devcontainer/devcontainer.go

736 lines
27 KiB
Go
Raw Normal View History

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"
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"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
gitea_context "code.gitea.io/gitea/services/context"
devcontainer_service_errors "code.gitea.io/gitea/services/devcontainer/errors"
"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) {
// 0. 检查参数
if ctx == nil || opts == nil || len(opts.Name) == 0 {
return nil, devcontainer_service_errors.ErrIllegalParams{
FieldNameList: []string{"ctx", "opts.Name"},
}
}
// 1. 检查 DevContainer 功能是否开启
if setting.Devcontainer.Enabled == false {
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:
devcontainerApp, err := AssignDevcontainerGetting2K8sOperator(&apiRequestContext, opts)
if err != nil {
return nil, devcontainer_service_errors.ErrOperateDevcontainer{
Action: "Open DevContainer in k8s",
Message: err.Error(),
}
}
openDevcontainerAbstractAgentVO.NodePortAssigned = devcontainerApp.Status.NodePortAssigned
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:
return nil, devcontainer_service_errors.ErrOperateDevcontainer{
Action: "Open DevContainer",
Message: "No Valid DevContainer Agent Found",
}
}
// 3. 封装返回结果
return openDevcontainerAbstractAgentVO, nil
}
// GetRepoDevcontainerDetails 获取仓库对应 DevContainer 信息
func GetRepoDevcontainerDetails(ctx context.Context, opts *RepoDevcontainerOptions) (RepoDevContainer, error) {
// 0. 构造异常返回时候的空数据
resultRepoDevcontainerDetail := RepoDevContainer{}
// 1. 检查参数是否有效
if opts == nil || opts.Actor == nil || opts.Repository == nil {
return resultRepoDevcontainerDetail, devcontainer_models_errors.ErrFailedToOperateDevcontainerDB{
Action: "construct query condition for devContainer user list",
Message: "invalid search condition",
}
}
// 2. 查询数据库
/*
SELECT
devcontainer.id AS devcontainer_id,
devcontainer.name AS devcontainer_name,
devcontainer.devcontainer_host AS devcontainer_host,
devcontainer.devcontainer_port AS devcontainer_port,
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
FROM devcontainer
INNER JOIN repository on devcontainer.repo_id = repository.id
WHERE
devcontainer.user_id = #{opts.Actor.ID}
AND
devcontainer.repo_id = #{opts.Repository.ID};
*/
_, 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_port AS devcontainer_port,"+
"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 {
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
// unixTimestamp is the number of seconds elapsed since January 1, 1970 UTC.
unixTimestamp := time.Now().Unix()
devContainerJson, err := GetDevcontainerJsonModel(ctx, opts.Repository)
if err != nil {
return devcontainer_service_errors.ErrOperateDevcontainer{
Action: "Get DevContainer Error",
Message: err.Error(),
}
}
var dockerfileContent string
if devContainerJson.DockerfilePath != "" {
dockerfileContent, err = GetDockerfileContent(ctx, opts.Repository)
if err != nil {
return devcontainer_service_errors.ErrOperateDevcontainer{
Action: "Get DockerFileContent Error",
Message: err.Error(),
}
}
}
cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf)
if err != nil {
log.Error("Failed to load custom conf '%s': %v", setting.CustomConf, err)
}
newDevcontainer := &CreateDevcontainerDTO{
Devcontainer: devcontainer_model.Devcontainer{
Name: getSanitizedDevcontainerName(username, repoName),
DevcontainerHost: cfg.Section("server").Key("DOMAIN").Value(),
DevcontainerUsername: "root",
DevcontainerWorkDir: "/data/workspace",
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(),
}
// 在数据库事务中创建 Dev Container 分配资源,出错时自动回滚相对应数据库字段,保证数据一致
dbTransactionErr := db.WithTx(ctx, func(ctx context.Context) error {
var err error
// 0. 查询数据库,收集用户 SSH 公钥合并用户临时填入SSH公钥
// (若用户合计 SSH公钥个数为0拒绝创建DevContainer
/**
SELECT content FROM public_key where owner_id = #{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 {
return devcontainer_service_errors.ErrOperateDevcontainer{
Action: fmt.Sprintf("query SSH Public Key List for User %s", opts.Actor.Name),
Message: err.Error(),
}
}
newDevcontainer.SSHPublicKeyList = append(userSSHPublicKeyList, opts.SSHPublicKeyList...)
devstarPublicKey := getDevStarPublicKey()
if devstarPublicKey == "" {
return devcontainer_service_errors.ErrOperateDevcontainer{
Action: fmt.Sprintf("devstar SSH Public Key Error "),
Message: err.Error(),
}
}
newDevcontainer.SSHPublicKeyList = append(newDevcontainer.SSHPublicKeyList)
// if len(userSSHPublicKeyList) <= 0 {
// // API没提供临时SSH公钥用户后台也没有永久SSH公钥直接结束并回滚事务
// return devcontainer_service_errors.ErrOperateDevcontainer{
// Action: "Check SSH Public Key List",
// Message: "禁止创建无法连通的DevContainer用户未提供 SSH 公钥请先使用API临时创建SSH密钥对、或在Web端手动添加SSH公钥",
// }
// }
// 1. 调用 k8s Operator Agent创建 DevContainer 资源同时更新k8s调度器分配的 NodePort
err = claimDevcontainerResource(&ctx, newDevcontainer, devContainerJson)
if err != nil {
return devcontainer_models_errors.ErrFailedToOperateDevcontainerDB{
Action: fmt.Sprintf("claim resource for Dev Container %v", newDevcontainer),
Message: err.Error(),
}
}
// 2. 根据分配的 NodePort 更新数据库字段
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",
}
}
return nil
})
return dbTransactionErr
}
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 客户端,直接查询 CRD 以获取 ttyd 端口
k8sClient, err := devcontainer_k8s_agent_module.GetKubernetesClient(&ctx)
if err != nil {
return "", err
}
// 直接从K8s获取CRD信息不依赖数据库
opts := &devcontainer_k8s_agent_module.GetDevcontainerOptions{
GetOptions: metav1.GetOptions{},
Name: devcontainerName,
Namespace: setting.Devcontainer.Namespace,
Wait: false,
}
devcontainerApp, err := devcontainer_k8s_agent_module.GetDevcontainer(&ctx, k8sClient, opts)
if err != nil {
return "", err
}
// 在额外端口中查找 ttyd 端口,使用多个条件匹配
var ttydNodePort uint16 = 0
for _, portInfo := range devcontainerApp.Status.ExtraPortsAssigned {
// 检查各种可能的情况名称为ttyd、名称包含ttyd、名称为port-7681、端口为7681
if portInfo.Name == "ttyd" ||
strings.Contains(portInfo.Name, "ttyd") ||
portInfo.Name == "port-7681" ||
portInfo.ContainerPort == 7681 {
ttydNodePort = portInfo.NodePort
log.Info("Found ttyd port: %d for port named: %s", ttydNodePort, portInfo.Name)
break
}
}
// 如果找到 ttyd 端口,构建 URL
if ttydNodePort > 0 {
cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf)
if err != nil {
log.Error("Failed to load custom conf '%s': %v", setting.CustomConf, err)
return "", err
}
domain := cfg.Section("server").Key("DOMAIN").Value()
return fmt.Sprintf("http://%s:%d/", domain, ttydNodePort), nil
}
// 如果没有找到ttyd端口记录详细的调试信息
log.Info("Available extra ports for %s: %v", devcontainerName, 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 "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
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
}
port, err := docker.GetMappedPort(cli, containerID, "22")
if err != nil {
return "", err
}
// 检查session
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
}
return "://mengning.devstar/" +
"openProject?host=" + devcontainer.DevContainerHost +
"&port=" + port +
"&username=" + devcontainer.DevContainerUsername +
"&path=" + devcontainer.DevContainerWorkDir +
"&access_token=" + access_token +
"&devstar_username=" + devcontainer.RepoOwnerName, nil
}
func AddPublicKeyToAllRunningDevContainer(ctx context.Context, user *user_model.User, publicKey string) error {
switch setting.Devcontainer.Agent {
case setting.KUBERNETES:
return fmt.Errorf("unsupported agent")
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")
}
}
// DeleteRepoDevcontainer 按照 仓库 和/或 用户信息删除 DevContainer(s)
func DeleteRepoDevcontainer(ctx context.Context, opts *RepoDevcontainerOptions) error {
if ctx == nil || opts == nil || (opts.Actor == 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})
}
if opts.Repository != nil {
sqlDevcontainerCondition = sqlDevcontainerCondition.And(builder.Eq{"repo_id": opts.Repository.ID})
}
var devcontainersList []devcontainer_model.Devcontainer
// 2. 开启事务:先获取 devcontainer列表后删除
dbTransactionErr := db.WithTx(ctx, func(ctx context.Context) error {
var err error
// 2.1 条件查询: user_id 和/或 repo_id
err = db.GetEngine(ctx).
Table("devcontainer").
Where(sqlDevcontainerCondition).
Find(&devcontainersList)
if err != nil {
return devcontainer_models_errors.ErrFailedToOperateDevcontainerDB{
Action: fmt.Sprintf("find devcontainer(s) with condition '%v'", sqlDevcontainerCondition),
Message: err.Error(),
}
}
// 2.2 空列表,直接结束事务(由于前一个操作只是查询,所以回滚事务不会导致数据不一致问题)
if len(devcontainersList) == 0 {
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
_, err = db.GetEngine(ctx).
Table("devcontainer").
Where(sqlDevcontainerCondition).
Delete()
if err != nil {
return devcontainer_models_errors.ErrFailedToOperateDevcontainerDB{
Action: fmt.Sprintf("MARK devcontainer(s) as DELETED with condition '%v'", sqlDevcontainerCondition),
Message: err.Error(),
}
}
_, err = db.GetEngine(ctx).
Table("devcontainer_output").
Where(sqlDevcontainerCondition).
Delete()
return nil
})
if dbTransactionErr != nil {
return dbTransactionErr
}
// 3. 后台启动一个goroutine慢慢回收 Dev Container 资源 (如果回收失败,将会产生孤儿 Dev Container只能管理员手动识别、删除)
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()
_ = purgeDevcontainersResource(&isolatedContextToPurgeK8sResource, &devcontainersList)
}()
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 {
// 如果用户设置禁用 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:
return AssignDevcontainerDeletion2K8sOperator(ctx, devcontainersList)
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 {
// 1. 检查 DevContainer 功能是否启用,若禁用,则直接结束
if !setting.Devcontainer.Enabled {
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 派遣创建任务
switch setting.Devcontainer.Agent {
case setting.KUBERNETES, "k8s":
// k8s Operator
return AssignDevcontainerCreation2K8sOperator(ctx, newDevContainer)
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 {
switch setting.Devcontainer.Agent {
case setting.KUBERNETES:
//k8s处理
ctx := gitea_ctx.Req.Context()
return AssignDevcontainerRestart2K8sOperator(&ctx, opts)
case setting.DOCKER:
return DockerRestartContainer(&gitea_ctx, opts)
default:
return fmt.Errorf("不支持的Agent")
//默认处理
}
}
func StopDevcontainer(gitea_ctx context.Context, opts *RepoDevContainer) error {
switch setting.Devcontainer.Agent {
case setting.KUBERNETES:
//k8s处理
return AssignDevcontainerStop2K8sOperator(&gitea_ctx, opts)
case setting.DOCKER:
return DockerStopContainer(&gitea_ctx, opts)
default:
return fmt.Errorf("不支持的Agent")
//默认处理
}
}