!81 让devcontainer相关功能在 k8s上工作起来
Merge pull request !81 from panshuxiao/k8s-devcontainer
This commit is contained in:
@@ -13,6 +13,7 @@ type Devcontainer struct {
|
||||
Id int64 `xorm:"BIGINT pk NOT NULL autoincr 'id' comment('主键,devContainerId')"`
|
||||
Name string `xorm:"VARCHAR(64) charset=utf8mb4 collate=utf8mb4_bin UNIQUE NOT NULL 'name' comment('devContainer名称,自动生成')"`
|
||||
DevcontainerHost string `xorm:"VARCHAR(256) charset=utf8mb4 collate=utf8mb4_bin NOT NULL 'devcontainer_host' comment('SSH Host')"`
|
||||
DevcontainerPort uint16 `xorm:"SMALLINT UNSIGNED NOT NULL 'devcontainer_port' comment('SSH Port')"`
|
||||
DevcontainerStatus uint16 `xorm:"SMALLINT UNSIGNED NOT NULL 'devcontainer_status' comment('SSH Status')"`
|
||||
DevcontainerUsername string `xorm:"VARCHAR(32) charset=utf8mb4 collate=utf8mb4_bin NOT NULL 'devcontainer_username' comment('SSH Username')"`
|
||||
DevcontainerWorkDir string `xorm:"VARCHAR(256) charset=utf8mb4 collate=utf8mb4_bin NOT NULL 'devcontainer_work_dir' comment('SSH 工作路径,典型值 ~/${project_name},256字节以内')"`
|
||||
|
||||
@@ -19,6 +19,7 @@ package devcontainer
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
@@ -46,6 +47,7 @@ type DevcontainerAppReconciler struct {
|
||||
// +kubebuilder:rbac:groups=devcontainer.devstar.cn,resources=devcontainerapps/finalizers,verbs=update
|
||||
// +kubebuilder:rbac:groups=apps,resources=statefulsets,verbs=create;delete;get;list;watch
|
||||
// +kubebuilder:rbac:groups="",resources=services,verbs=create;delete;get;list;watch
|
||||
// +kubebuilder:rbac:groups="",resources=persistentvolumeclaims,verbs=get;list;watch;delete
|
||||
|
||||
// Reconcile is part of the main kubernetes reconciliation loop which aims to
|
||||
// move the current state of the cluster closer to the desired state.
|
||||
@@ -69,6 +71,44 @@ func (r *DevcontainerAppReconciler) Reconcile(ctx context.Context, req ctrl.Requ
|
||||
return ctrl.Result{}, client.IgnoreNotFound(err)
|
||||
}
|
||||
|
||||
// 添加 finalizer 处理逻辑
|
||||
finalizerName := "devcontainer.devstar.cn/finalizer"
|
||||
|
||||
// 检查对象是否正在被删除
|
||||
if !app.ObjectMeta.DeletionTimestamp.IsZero() {
|
||||
// 对象正在被删除 - 处理 finalizer
|
||||
if k8s_sigs_controller_runtime_utils.ContainsFinalizer(app, finalizerName) {
|
||||
// 执行清理操作
|
||||
logger.Info("Cleaning up resources before deletion", "name", app.Name)
|
||||
|
||||
// 查找并删除关联的 PVC
|
||||
if err := r.cleanupPersistentVolumeClaims(ctx, app); err != nil {
|
||||
logger.Error(err, "Failed to clean up PVCs")
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
// 删除完成后移除 finalizer
|
||||
k8s_sigs_controller_runtime_utils.RemoveFinalizer(app, finalizerName)
|
||||
if err := r.Update(ctx, app); err != nil {
|
||||
logger.Error(err, "Failed to remove finalizer")
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
}
|
||||
|
||||
// 已标记为删除且处理完成,允许继续删除流程
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
// 如果对象不包含 finalizer,就添加它
|
||||
if !k8s_sigs_controller_runtime_utils.ContainsFinalizer(app, finalizerName) {
|
||||
logger.Info("Adding finalizer", "name", app.Name)
|
||||
k8s_sigs_controller_runtime_utils.AddFinalizer(app, finalizerName)
|
||||
if err := r.Update(ctx, app); err != nil {
|
||||
logger.Error(err, "Failed to add finalizer")
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
}
|
||||
|
||||
// 检查停止容器的注解
|
||||
if desiredReplicas, exists := app.Annotations["devstar.io/desiredReplicas"]; exists && desiredReplicas == "0" {
|
||||
logger.Info("DevContainer stop requested via annotation", "name", app.Name)
|
||||
@@ -353,6 +393,52 @@ func (r *DevcontainerAppReconciler) Reconcile(ctx context.Context, req ctrl.Requ
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
// cleanupPersistentVolumeClaims 查找并删除与 DevcontainerApp 关联的所有 PVC
|
||||
func (r *DevcontainerAppReconciler) cleanupPersistentVolumeClaims(ctx context.Context, app *devcontainer_v1.DevcontainerApp) error {
|
||||
logger := log.FromContext(ctx)
|
||||
|
||||
// 查找关联的 PVC
|
||||
pvcList := &core_v1.PersistentVolumeClaimList{}
|
||||
|
||||
// 按标签筛选
|
||||
labelSelector := client.MatchingLabels{
|
||||
"app": app.Name,
|
||||
}
|
||||
if err := r.List(ctx, pvcList, client.InNamespace(app.Namespace), labelSelector); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 如果按标签没找到,尝试按名称模式查找
|
||||
if len(pvcList.Items) == 0 {
|
||||
if err := r.List(ctx, pvcList, client.InNamespace(app.Namespace)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 筛选出名称包含 DevcontainerApp 名称的 PVC
|
||||
var filteredItems []core_v1.PersistentVolumeClaim
|
||||
for _, pvc := range pvcList.Items {
|
||||
// StatefulSet PVC 命名格式通常为: <volumeClaimTemplate名称>-<StatefulSet名称>-<序号>
|
||||
// 检查是否包含 app 名称作为名称的一部分
|
||||
if strings.Contains(pvc.Name, app.Name+"-") {
|
||||
filteredItems = append(filteredItems, pvc)
|
||||
logger.Info("Found PVC to delete", "name", pvc.Name)
|
||||
}
|
||||
}
|
||||
pvcList.Items = filteredItems
|
||||
}
|
||||
|
||||
// 删除找到的 PVC
|
||||
for i := range pvcList.Items {
|
||||
logger.Info("Deleting PVC", "name", pvcList.Items[i].Name)
|
||||
if err := r.Delete(ctx, &pvcList.Items[i]); err != nil && !errors.IsNotFound(err) {
|
||||
logger.Error(err, "Failed to delete PVC", "name", pvcList.Items[i].Name)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetupWithManager sets up the controller with the Manager.
|
||||
func (r *DevcontainerAppReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
|
||||
@@ -2,10 +2,6 @@ package utils
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"text/template"
|
||||
|
||||
devcontainer_apps_v1 "code.gitea.io/gitea/modules/k8s/api/v1"
|
||||
@@ -14,53 +10,24 @@ import (
|
||||
yaml_util "k8s.io/apimachinery/pkg/util/yaml"
|
||||
)
|
||||
|
||||
// const (
|
||||
// TemplatePath = "modules/k8s/controller/devcontainer/templates/"
|
||||
// )
|
||||
const (
|
||||
TemplatePath = "modules/k8s/controller/devcontainer/templates/"
|
||||
)
|
||||
|
||||
// parseTemplate 解析 Go Template 模板文件
|
||||
func parseTemplate(templateName string, app *devcontainer_apps_v1.DevcontainerApp) []byte {
|
||||
// 获取当前代码文件的绝对路径
|
||||
_, filename, _, ok := runtime.Caller(0)
|
||||
if !ok {
|
||||
panic("无法获取当前文件路径")
|
||||
}
|
||||
|
||||
// 通过当前代码文件的位置计算模板文件的位置
|
||||
// utils 目录
|
||||
utilsDir := filepath.Dir(filename)
|
||||
// controller/devcontainer 目录
|
||||
controllerDir := filepath.Dir(utilsDir)
|
||||
// templates 目录
|
||||
templatesDir := filepath.Join(controllerDir, "templates")
|
||||
// 完整模板文件路径
|
||||
templatePath := filepath.Join(templatesDir, templateName+".yaml")
|
||||
|
||||
// 打印调试信息
|
||||
fmt.Printf("当前代码文件: %s\n", filename)
|
||||
fmt.Printf("模板目录: %s\n", templatesDir)
|
||||
fmt.Printf("使用模板文件: %s\n", templatePath)
|
||||
|
||||
// 检查模板文件是否存在
|
||||
if _, err := os.Stat(templatePath); os.IsNotExist(err) {
|
||||
panic(fmt.Errorf("模板文件不存在: %s", templatePath))
|
||||
}
|
||||
|
||||
// 解析模板
|
||||
tmpl, err := template.
|
||||
New(filepath.Base(templatePath)).
|
||||
New(templateName + ".yaml").
|
||||
Funcs(template.FuncMap{"default": DefaultFunc}).
|
||||
ParseFiles(templatePath)
|
||||
ParseFiles(TemplatePath + templateName + ".yaml")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
b := new(bytes.Buffer)
|
||||
err = tmpl.Execute(b, app)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return b.Bytes()
|
||||
}
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ type SSHKeyPairType struct {
|
||||
type CloudType struct {
|
||||
Enabled bool
|
||||
Provider string
|
||||
Tencent CloudProviderTencentType `ini:"devcontainer.cloud.tencent"`
|
||||
Tencent CloudProviderTencentType // 移除 ini 标签,通过代码处理
|
||||
}
|
||||
|
||||
type CloudProviderTencentType struct {
|
||||
@@ -127,7 +127,7 @@ func validateDevcontainerCloudSettings() {
|
||||
Cloud.Enabled = false
|
||||
}
|
||||
|
||||
if Cloud.Tencent.IpProtocol != "TCP" && Cloud.Tencent.IpProtocol != "UDP" {
|
||||
if Cloud.Tencent.IpProtocol != "TCP" && Cloud.Tencent.IpProtocol != "UDP" && Cloud.Tencent.IpProtocol != "tcp" && Cloud.Tencent.IpProtocol != "udp" {
|
||||
log.Warn("INVALID IP Protocol '%v' for DevStar Cloud Provider Tencent", Cloud.Tencent.IpProtocol)
|
||||
Cloud.Enabled = false
|
||||
}
|
||||
@@ -163,15 +163,109 @@ func validateDevcontainerCloudSettings() {
|
||||
|
||||
}
|
||||
|
||||
// 修改 loadDevcontainerFrom 函数以支持新旧配置节
|
||||
func loadDevcontainerFrom(rootCfg ConfigProvider) {
|
||||
mustMapSetting(rootCfg, "devcontainer", &Devcontainer)
|
||||
// 检查是否存在新的配置节
|
||||
hasNewConfig := true
|
||||
if _, err := rootCfg.GetSection("devcontainer"); err != nil {
|
||||
hasNewConfig = false
|
||||
}
|
||||
|
||||
// 检查是否存在旧的配置节
|
||||
hasOldConfig := true
|
||||
if _, err := rootCfg.GetSection("devstar.devcontainer"); err != nil {
|
||||
hasOldConfig = false
|
||||
}
|
||||
|
||||
// 根据存在的配置节处理
|
||||
if hasNewConfig {
|
||||
// 新配置节存在,直接使用
|
||||
mustMapSetting(rootCfg, "devcontainer", &Devcontainer)
|
||||
log.Info("从 [devcontainer] 节加载配置")
|
||||
} else if hasOldConfig {
|
||||
// 只有旧配置节存在,直接从旧配置节加载
|
||||
mustMapSetting(rootCfg, "devstar.devcontainer", &Devcontainer)
|
||||
log.Info("从 [devstar.devcontainer] 节加载配置")
|
||||
}
|
||||
|
||||
// 进行配置验证
|
||||
validateDevcontainerSettings()
|
||||
|
||||
// 加载其他配置
|
||||
mustMapSetting(rootCfg, "ssh_key_pair", &SSHKeypair)
|
||||
validateSSHKeyPairSettings()
|
||||
|
||||
if Devcontainer.Agent == "k8s" || Devcontainer.Agent == KUBERNETES {
|
||||
mustMapSetting(rootCfg, "devcontainer.cloud", &Cloud)
|
||||
validateDevcontainerCloudSettings()
|
||||
// 调用新的云配置加载函数
|
||||
loadCloudConfigWithFallback(rootCfg)
|
||||
}
|
||||
|
||||
// 打印最终使用的命名空间
|
||||
log.Info("DevContainer 将在命名空间 '%s' 中创建", Devcontainer.Namespace)
|
||||
}
|
||||
|
||||
// 新增: 处理云配置加载的函数,支持新旧两种配置节
|
||||
func loadCloudConfigWithFallback(rootCfg ConfigProvider) {
|
||||
// 1. 先尝试加载主配置节
|
||||
hasDevcontainerCloud := true
|
||||
if _, err := rootCfg.GetSection("devcontainer.cloud"); err != nil {
|
||||
hasDevcontainerCloud = false
|
||||
}
|
||||
|
||||
hasDevstarCloud := true
|
||||
if _, err := rootCfg.GetSection("devstar.cloud"); err != nil {
|
||||
hasDevstarCloud = false
|
||||
}
|
||||
|
||||
// 2. 优先使用新配置节,不存在则使用旧配置节
|
||||
var cloudSectionName string
|
||||
if hasDevcontainerCloud {
|
||||
cloudSectionName = "devcontainer.cloud"
|
||||
log.Info("从 [devcontainer.cloud] 节加载云配置")
|
||||
} else if hasDevstarCloud {
|
||||
cloudSectionName = "devstar.cloud"
|
||||
log.Info("从 [devstar.cloud] 节加载云配置")
|
||||
} else {
|
||||
log.Warn("未找到云配置节,Cloud 功能将被禁用")
|
||||
Cloud.Enabled = false
|
||||
return
|
||||
}
|
||||
|
||||
// 3. 加载基本云配置
|
||||
if err := rootCfg.Section(cloudSectionName).MapTo(&Cloud); err != nil {
|
||||
log.Error("加载云配置时出错: %v", err)
|
||||
Cloud.Enabled = false
|
||||
return
|
||||
}
|
||||
|
||||
// 4. 根据选择的配置节路径,决定腾讯云配置节路径
|
||||
var tencentSectionName string
|
||||
if cloudSectionName == "devcontainer.cloud" {
|
||||
tencentSectionName = "devcontainer.cloud.tencent"
|
||||
} else {
|
||||
tencentSectionName = "devstar.cloud.tencent"
|
||||
}
|
||||
|
||||
// 5. 检查腾讯云配置节是否存在
|
||||
if _, err := rootCfg.GetSection(tencentSectionName); err != nil {
|
||||
log.Warn("未找到腾讯云配置节 [%s]", tencentSectionName)
|
||||
if Cloud.Provider == CLOUD_PROVIDER_TENCENT {
|
||||
log.Error("虽然指定使用腾讯云,但未找到对应配置,Cloud 功能将被禁用")
|
||||
Cloud.Enabled = false
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 6. 加载腾讯云配置
|
||||
if Cloud.Provider == CLOUD_PROVIDER_TENCENT {
|
||||
log.Info("从 [%s] 节加载腾讯云配置", tencentSectionName)
|
||||
if err := rootCfg.Section(tencentSectionName).MapTo(&Cloud.Tencent); err != nil {
|
||||
log.Error("加载腾讯云配置时出错: %v", err)
|
||||
Cloud.Enabled = false
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 7. 验证云配置
|
||||
validateDevcontainerCloudSettings()
|
||||
}
|
||||
|
||||
@@ -39,9 +39,12 @@ type OpenDevcontainerAbstractAgent struct {
|
||||
|
||||
// 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"},
|
||||
}
|
||||
@@ -49,6 +52,7 @@ func OpenDevcontainerService(ctx *gitea_context.Context, opts *OpenDevcontainerA
|
||||
|
||||
// 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",
|
||||
@@ -59,15 +63,19 @@ func OpenDevcontainerService(ctx *gitea_context.Context, opts *OpenDevcontainerA
|
||||
apiRequestContext := ctx.Req.Context()
|
||||
openDevcontainerAbstractAgentVO := &OpenDevcontainerAbstractAgent{}
|
||||
switch setting.Devcontainer.Agent {
|
||||
case setting.KUBERNETES:
|
||||
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)
|
||||
@@ -79,6 +87,7 @@ func OpenDevcontainerService(ctx *gitea_context.Context, opts *OpenDevcontainerA
|
||||
}
|
||||
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",
|
||||
@@ -86,44 +95,32 @@ func OpenDevcontainerService(ctx *gitea_context.Context, opts *OpenDevcontainerA
|
||||
}
|
||||
|
||||
// 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. 查询数据库
|
||||
/*
|
||||
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(""+
|
||||
@@ -145,6 +142,7 @@ func GetRepoDevcontainerDetails(ctx context.Context, opts *RepoDevcontainerOptio
|
||||
|
||||
// 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(),
|
||||
@@ -164,32 +162,49 @@ func CreateRepoDevcontainer(ctx context.Context, opts *CreateRepoDevcontainerOpt
|
||||
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("Failed to load custom conf '%s': %v", setting.CustomConf, err)
|
||||
log.Error("CreateRepoDevcontainer: 加载配置文件失败: %v", err)
|
||||
}
|
||||
|
||||
containerName := getSanitizedDevcontainerName(username, repoName)
|
||||
log.Info("CreateRepoDevcontainer: 生成 DevContainer 名称: %s", containerName)
|
||||
|
||||
newDevcontainer := &CreateDevcontainerDTO{
|
||||
Devcontainer: devcontainer_model.Devcontainer{
|
||||
Name: getSanitizedDevcontainerName(username, repoName),
|
||||
Name: containerName,
|
||||
DevcontainerHost: cfg.Section("server").Key("DOMAIN").Value(),
|
||||
DevcontainerUsername: "root",
|
||||
DevcontainerWorkDir: "/data/workspace",
|
||||
@@ -203,28 +218,32 @@ func CreateRepoDevcontainer(ctx context.Context, opts *CreateRepoDevcontainerOpt
|
||||
Image: devContainerJson.Image,
|
||||
GitRepositoryURL: strings.TrimSuffix(setting.AppURL, "/") + opts.Repository.Link(),
|
||||
}
|
||||
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",
|
||||
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公钥
|
||||
// (若用户合计 SSH公钥个数为0,拒绝创建DevContainer)
|
||||
/**
|
||||
SELECT content FROM public_key where owner_id = #{opts.Actor.ID}
|
||||
*/
|
||||
log.Info("CreateRepoDevcontainer: 查询用户 SSH 公钥, userID=%d", opts.Actor.ID)
|
||||
var userSSHPublicKeyList []string
|
||||
err = db.GetEngine(ctx).
|
||||
Table("public_key").
|
||||
@@ -232,40 +251,78 @@ func CreateRepoDevcontainer(ctx context.Context, opts *CreateRepoDevcontainerOpt
|
||||
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)
|
||||
// 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
|
||||
// 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
|
||||
})
|
||||
|
||||
return dbTransactionErr
|
||||
if dbTransactionErr != nil {
|
||||
log.Error("CreateRepoDevcontainer: 创建失败: %v", dbTransactionErr)
|
||||
return dbTransactionErr
|
||||
}
|
||||
|
||||
log.Info("CreateRepoDevcontainer: DevContainer 创建成功, name=%s", newDevcontainer.Name)
|
||||
return nil
|
||||
}
|
||||
func getDevStarPublicKey() string {
|
||||
// 获取当前用户的主目录
|
||||
@@ -318,12 +375,16 @@ func fileExists(filename string) bool {
|
||||
}
|
||||
func GetWebTerminalURL(ctx context.Context, devcontainerName string) (string, error) {
|
||||
switch setting.Devcontainer.Agent {
|
||||
case setting.KUBERNETES:
|
||||
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{
|
||||
@@ -332,22 +393,30 @@ func GetWebTerminalURL(ctx context.Context, devcontainerName string) (string, er
|
||||
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("Found ttyd port: %d for port named: %s", ttydNodePort, portInfo.Name)
|
||||
log.Info("GetWebTerminalURL: 找到 ttyd 端口: %d, 名称: %s", ttydNodePort, portInfo.Name)
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -356,17 +425,34 @@ func GetWebTerminalURL(ctx context.Context, devcontainerName string) (string, er
|
||||
if ttydNodePort > 0 {
|
||||
cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf)
|
||||
if err != nil {
|
||||
log.Error("Failed to load custom conf '%s': %v", setting.CustomConf, err)
|
||||
log.Error("GetWebTerminalURL: 加载配置文件失败: %v", err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 检查是否启用了基于路径的访问方式
|
||||
|
||||
domain := cfg.Section("server").Key("DOMAIN").Value()
|
||||
return fmt.Sprintf("http://%s:%d/", domain, ttydNodePort), nil
|
||||
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.Info("Available extra ports for %s: %v", devcontainerName, devcontainerApp.Status.ExtraPortsAssigned)
|
||||
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 {
|
||||
@@ -384,6 +470,7 @@ func GetWebTerminalURL(ctx context.Context, devcontainerName string) (string, er
|
||||
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:
|
||||
@@ -392,26 +479,12 @@ func GetWebTerminalURL(ctx context.Context, devcontainerName string) (string, er
|
||||
}
|
||||
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
|
||||
|
||||
// 检查 session 中是否已存在 token
|
||||
if ctx.Session.Get("access_token") != nil {
|
||||
access_token = ctx.Session.Get("access_token").(string)
|
||||
} else {
|
||||
// 生成token
|
||||
// 生成 token
|
||||
token := &auth_model.AccessToken{
|
||||
UID: devcontainer.UserId,
|
||||
Name: "terminal_login_token",
|
||||
@@ -434,8 +507,67 @@ func Get_IDE_TerminalURL(ctx *gitea_context.Context, devcontainer *RepoDevContai
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
// 构建并返回 URL
|
||||
return "://mengning.devstar/" +
|
||||
"openProject?host=" + devcontainer.RepoName +
|
||||
"&hostname=" + devcontainer.DevContainerHost +
|
||||
@@ -444,12 +576,11 @@ func Get_IDE_TerminalURL(ctx *gitea_context.Context, devcontainer *RepoDevContai
|
||||
"&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:
|
||||
case setting.KUBERNETES, "k8s":
|
||||
return fmt.Errorf("unsupported agent")
|
||||
case setting.DOCKER:
|
||||
cli, err := docker.CreateDockerClient(&ctx)
|
||||
@@ -499,7 +630,13 @@ func AddPublicKeyToAllRunningDevContainer(ctx context.Context, user *user_model.
|
||||
|
||||
// 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",
|
||||
@@ -510,29 +647,39 @@ func DeleteRepoDevcontainer(ctx context.Context, opts *RepoDevcontainerOptions)
|
||||
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",
|
||||
@@ -540,37 +687,55 @@ func DeleteRepoDevcontainer(ctx context.Context, opts *RepoDevcontainerOptions)
|
||||
}
|
||||
|
||||
// 2.3 条件删除: user_id 和/或 repo_id
|
||||
_, err = db.GetEngine(ctx).
|
||||
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(),
|
||||
}
|
||||
}
|
||||
_, err = db.GetEngine(ctx).
|
||||
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()
|
||||
|
||||
_ = purgeDevcontainersResource(&isolatedContextToPurgeK8sResource, &devcontainersList)
|
||||
err := purgeDevcontainersResource(&isolatedContextToPurgeK8sResource, &devcontainersList)
|
||||
if err != nil {
|
||||
log.Error("DeleteRepoDevcontainer: 异步资源回收失败: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
log.Info("DeleteRepoDevcontainer: DevContainer 删除操作完成")
|
||||
return dbTransactionErr
|
||||
}
|
||||
|
||||
@@ -596,14 +761,23 @@ func getSanitizedDevcontainerName(username, repoName string) string {
|
||||
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:
|
||||
return AssignDevcontainerDeletion2K8sOperator(ctx, devcontainersList)
|
||||
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:
|
||||
@@ -617,8 +791,11 @@ func purgeDevcontainersResource(ctx *context.Context, devcontainersList *[]devco
|
||||
|
||||
// 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",
|
||||
@@ -693,10 +870,20 @@ func claimDevcontainerResource(ctx *context.Context, newDevContainer *CreateDevc
|
||||
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
|
||||
return AssignDevcontainerCreation2K8sOperator(ctx, newDevContainer)
|
||||
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:
|
||||
@@ -708,12 +895,19 @@ func claimDevcontainerResource(ctx *context.Context, newDevContainer *CreateDevc
|
||||
}
|
||||
}
|
||||
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处理
|
||||
case setting.KUBERNETES, "k8s":
|
||||
log.Info("RestartDevcontainer: 使用 K8s Agent 重启容器 %s", opts.DevContainerName)
|
||||
ctx := gitea_ctx.Req.Context()
|
||||
return AssignDevcontainerRestart2K8sOperator(&ctx, opts)
|
||||
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:
|
||||
@@ -723,10 +917,18 @@ func RestartDevcontainer(gitea_ctx gitea_context.Context, opts *RepoDevContainer
|
||||
|
||||
}
|
||||
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处理
|
||||
return AssignDevcontainerStop2K8sOperator(&gitea_ctx, opts)
|
||||
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:
|
||||
|
||||
@@ -3,6 +3,7 @@ package devcontainer
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
@@ -15,10 +16,14 @@ import (
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/services/devcontainer/errors"
|
||||
"code.gitea.io/gitea/services/devstar_cloud_provider"
|
||||
networkingv1 "k8s.io/api/networking/v1"
|
||||
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
)
|
||||
|
||||
var k8sGroupVersionResource = schema.GroupVersionResource{
|
||||
@@ -37,6 +42,8 @@ func (err ErrIllegalK8sAgentParams) Error() string {
|
||||
|
||||
// AssignDevcontainerGetting2K8sOperator 获取 k8s CRD 资源 DevcontainerApp 最新状态(需要根据用户传入的 wait 参数决定是否要阻塞等待 DevContainer 就绪)
|
||||
func AssignDevcontainerGetting2K8sOperator(ctx *context.Context, opts *OpenDevcontainerAppDispatcherOptions) (*k8s_api_v1.DevcontainerApp, error) {
|
||||
log.Info("AssignDevcontainerGetting2K8sOperator: Starting lookup for container: %s, wait=%v",
|
||||
opts.Name, opts.Wait)
|
||||
|
||||
// 0. 检查参数
|
||||
if ctx == nil || opts == nil || len(opts.Name) == 0 {
|
||||
@@ -54,6 +61,7 @@ func AssignDevcontainerGetting2K8sOperator(ctx *context.Context, opts *OpenDevco
|
||||
Message: err.Error(),
|
||||
}
|
||||
}
|
||||
log.Info("AssignDevcontainerGetting2K8sOperator: K8s client created successfully")
|
||||
|
||||
// 2. 调用 modules 层 k8s Agent 获取 k8s CRD 资源 DevcontainerApp
|
||||
optsGetDevcontainer := &devcontainer_dto.GetDevcontainerOptions{
|
||||
@@ -62,13 +70,28 @@ func AssignDevcontainerGetting2K8sOperator(ctx *context.Context, opts *OpenDevco
|
||||
Namespace: setting.Devcontainer.Namespace,
|
||||
Wait: opts.Wait,
|
||||
}
|
||||
log.Info("AssignDevcontainerGetting2K8sOperator: Retrieving DevcontainerApp %s in namespace %s (wait=%v)",
|
||||
opts.Name, setting.Devcontainer.Namespace, opts.Wait)
|
||||
devcontainerApp, err := devcontainer_k8s_agent_module.GetDevcontainer(ctx, client, optsGetDevcontainer)
|
||||
if err != nil {
|
||||
log.Error("AssignDevcontainerGetting2K8sOperator: Failed to get DevcontainerApp: %v", err)
|
||||
return nil, errors.ErrOperateDevcontainer{
|
||||
Action: fmt.Sprintf("Open Devcontainer '%s' (wait=%v)", opts.Name, opts.Wait),
|
||||
Message: err.Error(),
|
||||
}
|
||||
}
|
||||
log.Info("AssignDevcontainerGetting2K8sOperator: DevcontainerApp retrieved successfully - Name: %s, NodePort: %d, Ready: %v",
|
||||
devcontainerApp.Name, devcontainerApp.Status.NodePortAssigned, devcontainerApp.Status.Ready)
|
||||
|
||||
// 添加额外端口的日志
|
||||
if len(devcontainerApp.Status.ExtraPortsAssigned) > 0 {
|
||||
for i, port := range devcontainerApp.Status.ExtraPortsAssigned {
|
||||
log.Info("AssignDevcontainerGetting2K8sOperator: Extra port %d - Name: %s, NodePort: %d, ContainerPort: %d",
|
||||
i, port.Name, port.NodePort, port.ContainerPort)
|
||||
}
|
||||
} else {
|
||||
log.Info("AssignDevcontainerGetting2K8sOperator: No extra ports found for DevcontainerApp %s", devcontainerApp.Name)
|
||||
}
|
||||
|
||||
// 3. 成功获取最新的 DevcontainerApp,返回
|
||||
return devcontainerApp, nil
|
||||
@@ -79,7 +102,7 @@ func AssignDevcontainerGetting2K8sOperator(ctx *context.Context, opts *OpenDevco
|
||||
// - services/ 进行了封装,简化用户界面使用
|
||||
|
||||
func AssignDevcontainerDeletion2K8sOperator(ctx *context.Context, devcontainersList *[]devcontainer_model.Devcontainer) error {
|
||||
|
||||
log.Info("AssignDevcontainerDeletion2K8sOperator: Starting Deletion for containers")
|
||||
// 1. 获取 Dynamic Client
|
||||
client, err := devcontainer_k8s_agent_module.GetKubernetesClient(ctx)
|
||||
if err != nil {
|
||||
@@ -87,6 +110,32 @@ func AssignDevcontainerDeletion2K8sOperator(ctx *context.Context, devcontainersL
|
||||
return err
|
||||
}
|
||||
|
||||
// 获取标准 Kubernetes 客户端,用于删除 Ingress
|
||||
stdClient, err := getStandardKubernetesClient()
|
||||
if err != nil {
|
||||
log.Warn("AssignDevcontainerDeletion2K8sOperator: 获取标准 K8s 客户端失败: %v", err)
|
||||
// 继续执行,不阻止主流程
|
||||
} else {
|
||||
// 先删除与 DevContainer 相关的 Ingress 资源
|
||||
for _, devcontainer := range *devcontainersList {
|
||||
ingressName := fmt.Sprintf("%s-ttyd-ingress", devcontainer.Name)
|
||||
log.Info("AssignDevcontainerDeletion2K8sOperator: 删除 Ingress %s", ingressName)
|
||||
|
||||
err := stdClient.NetworkingV1().Ingresses(setting.Devcontainer.Namespace).Delete(*ctx, ingressName, metav1.DeleteOptions{})
|
||||
if err != nil {
|
||||
if k8serrors.IsNotFound(err) {
|
||||
// Ingress 已经不存在,视为正常情况
|
||||
log.Info("AssignDevcontainerDeletion2K8sOperator: Ingress %s 不存在,跳过删除", ingressName)
|
||||
} else {
|
||||
log.Warn("AssignDevcontainerDeletion2K8sOperator: 删除 Ingress %s 失败: %v", ingressName, err)
|
||||
// 继续执行,不阻止主流程
|
||||
}
|
||||
} else {
|
||||
log.Info("AssignDevcontainerDeletion2K8sOperator: 成功删除 Ingress %s", ingressName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 调用 modules 层 k8s Agent,执行删除资源
|
||||
opts := &devcontainer_dto.DeleteDevcontainerOptions{
|
||||
DeleteOptions: metav1.DeleteOptions{},
|
||||
@@ -102,11 +151,6 @@ func AssignDevcontainerDeletion2K8sOperator(ctx *context.Context, devcontainersL
|
||||
for _, devcontainer := range *devcontainersList {
|
||||
opts.Name = devcontainer.Name
|
||||
_ = devcontainer_k8s_agent_module.DeleteDevcontainer(ctx, client, opts)
|
||||
devcontainerNatRuleDescription := setting.DEVCONTAINER_CLOUD_NAT_RULE_DESCRIPTION_PREFIX + devcontainer.Name
|
||||
err := devstar_cloud_provider.DeleteNATRulePort(devcontainerNatRuleDescription)
|
||||
if err != nil {
|
||||
log.Warn("[Cloud NAT DELETION Error]: " + err.Error())
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -119,6 +163,9 @@ func AssignDevcontainerDeletion2K8sOperator(ctx *context.Context, devcontainersL
|
||||
//
|
||||
// 注意:本方法仍然在数据库事务中,因此不适合执行长时间操作,故需要后期异步判断 DevContainer 是否就绪
|
||||
func AssignDevcontainerCreation2K8sOperator(ctx *context.Context, newDevContainer *CreateDevcontainerDTO) error {
|
||||
log.Info("AssignDevcontainerCreation2K8sOperator: Starting creation for container: %s", newDevContainer.Name)
|
||||
log.Info("AssignDevcontainerCreation2K8sOperator: Container details - Image: %s, RepoURL: %s, SSHKeys: %d",
|
||||
newDevContainer.Image, newDevContainer.GitRepositoryURL, len(newDevContainer.SSHPublicKeyList))
|
||||
|
||||
// 1. 获取 Dynamic Client
|
||||
client, err := devcontainer_k8s_agent_module.GetKubernetesClient(ctx)
|
||||
@@ -183,6 +230,7 @@ func AssignDevcontainerCreation2K8sOperator(ctx *context.Context, newDevContaine
|
||||
}
|
||||
|
||||
// 添加 ttyd 端口配置 - WebTerminal 功能
|
||||
log.Info("AssignDevcontainerCreation2K8sOperator: Adding ttyd port configuration (7681)")
|
||||
extraPorts := []k8s_api_v1.ExtraPortSpec{
|
||||
{
|
||||
Name: "ttyd",
|
||||
@@ -202,6 +250,7 @@ func AssignDevcontainerCreation2K8sOperator(ctx *context.Context, newDevContaine
|
||||
"nohup ttyd -p 7681 -W bash > /dev/null 2>&1 & " +
|
||||
"while true; do sleep 60; done",
|
||||
}
|
||||
log.Info("AssignDevcontainerCreation2K8sOperator: Command includes ttyd installation and startup")
|
||||
|
||||
// 2. 调用 modules 层 k8s Agent,执行创建资源
|
||||
opts := &devcontainer_dto.CreateDevcontainerOptions{
|
||||
@@ -238,19 +287,22 @@ func AssignDevcontainerCreation2K8sOperator(ctx *context.Context, newDevContaine
|
||||
}
|
||||
|
||||
// 2. 创建成功,取回集群中的 DevContainer
|
||||
log.Info("AssignDevcontainerCreation2K8sOperator: Creating DevcontainerApp %s in namespace %s",
|
||||
opts.Name, opts.Namespace)
|
||||
devcontainerInCluster, err := devcontainer_k8s_agent_module.CreateDevcontainer(ctx, client, opts)
|
||||
if err != nil {
|
||||
log.Error("AssignDevcontainerCreation2K8sOperator: Failed to create DevcontainerApp: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// // 3. 将分配的 NodePort Service 写回 newDevcontainer,供写入数据库进行下一步操作
|
||||
// newDevContainer.DevcontainerPort = devcontainerInCluster.Status.NodePortAssigned
|
||||
log.Info("AssignDevcontainerCreation2K8sOperator: DevcontainerApp created successfully - Name: %s",
|
||||
devcontainerInCluster.Name)
|
||||
|
||||
// 3. 处理 NodePort - 检查是否为0(尚未分配)
|
||||
nodePort := devcontainerInCluster.Status.NodePortAssigned
|
||||
|
||||
if nodePort == 0 {
|
||||
log.Info("NodePort not yet assigned by K8s controller, setting temporary port")
|
||||
log.Info("AssignDevcontainerCreation2K8sOperator: NodePort not yet assigned, starting async updater for %s",
|
||||
devcontainerInCluster.Name)
|
||||
|
||||
// 将端口设为0,数据库中记录特殊标记
|
||||
newDevContainer.DevcontainerPort = 0
|
||||
@@ -265,6 +317,9 @@ func AssignDevcontainerCreation2K8sOperator(ctx *context.Context, newDevContaine
|
||||
newDevContainer.UserId,
|
||||
newDevContainer.RepoId)
|
||||
} else {
|
||||
log.Info("AssignDevcontainerCreation2K8sOperator: NodePort %d assigned immediately to %s",
|
||||
nodePort, devcontainerInCluster.Name)
|
||||
|
||||
// 端口已分配,直接使用
|
||||
newDevContainer.DevcontainerPort = nodePort
|
||||
log.Info("DevContainer created in cluster - Name: %s, NodePort: %d",
|
||||
@@ -274,12 +329,24 @@ func AssignDevcontainerCreation2K8sOperator(ctx *context.Context, newDevContaine
|
||||
log.Info("DevContainer created in cluster - Name: %s, NodePort: %d",
|
||||
devcontainerInCluster.Name,
|
||||
devcontainerInCluster.Status.NodePortAssigned)
|
||||
|
||||
// 为 ttyd 服务创建 Ingress
|
||||
err = createDevContainerTTYDIngress(ctx, devcontainerInCluster.Name)
|
||||
if err != nil {
|
||||
log.Warn("AssignDevcontainerCreation2K8sOperator: 创建 ttyd Ingress 失败: %v", err)
|
||||
// 不阻止主流程,只记录警告
|
||||
} else {
|
||||
log.Info("AssignDevcontainerCreation2K8sOperator: 创建 ttyd Ingress 成功")
|
||||
}
|
||||
|
||||
// 4. 层层返回 nil,自动提交数据库事务,完成 DevContainer 创建
|
||||
return nil
|
||||
}
|
||||
|
||||
// AssignDevcontainerRestart2K8sOperator 将 DevContainer 重启任务派遣至 K8s 控制器
|
||||
func AssignDevcontainerRestart2K8sOperator(ctx *context.Context, opts *RepoDevContainer) error {
|
||||
log.Info("AssignDevcontainerRestart2K8sOperator: Starting restart for container: %s", opts.DevContainerName)
|
||||
|
||||
// 1. 获取 Dynamic Client
|
||||
client, err := devcontainer_k8s_agent_module.GetKubernetesClient(ctx)
|
||||
if err != nil {
|
||||
@@ -297,6 +364,9 @@ func AssignDevcontainerRestart2K8sOperator(ctx *context.Context, opts *RepoDevCo
|
||||
}
|
||||
}
|
||||
}`, time.Now().Format(time.RFC3339))
|
||||
log.Info("AssignDevcontainerRestart2K8sOperator: Applying patch to restart container %s",
|
||||
opts.DevContainerName)
|
||||
log.Debug("AssignDevcontainerRestart2K8sOperator: Patch data: %s", patchData)
|
||||
|
||||
// 应用补丁到 DevcontainerApp CRD
|
||||
_, err = client.Resource(k8sGroupVersionResource).
|
||||
@@ -313,6 +383,8 @@ func AssignDevcontainerRestart2K8sOperator(ctx *context.Context, opts *RepoDevCo
|
||||
|
||||
// 记录重启操作日志
|
||||
log.Info("DevContainer restarted: %s", opts.DevContainerName)
|
||||
log.Info("AssignDevcontainerRestart2K8sOperator: Restart patch applied successfully for %s",
|
||||
opts.DevContainerName)
|
||||
|
||||
// 将重启操作记录到数据库
|
||||
dbEngine := db.GetEngine(*ctx)
|
||||
@@ -387,6 +459,9 @@ func AssignDevcontainerStop2K8sOperator(ctx *context.Context, opts *RepoDevConta
|
||||
|
||||
// 异步更新 NodePort 的辅助函数
|
||||
func updateNodePortAsync(containerName string, namespace string, userId, repoId int64) {
|
||||
log.Info("updateNodePortAsync: Starting for container: %s in namespace: %s", containerName, namespace)
|
||||
log.Info("updateNodePortAsync: Waiting 20 seconds for K8s controller to assign port")
|
||||
|
||||
// 等待K8s控制器完成端口分配
|
||||
time.Sleep(20 * time.Second)
|
||||
|
||||
@@ -397,9 +472,11 @@ func updateNodePortAsync(containerName string, namespace string, userId, repoId
|
||||
log.Error("Failed to get K8s client in async updater: %v", err)
|
||||
return
|
||||
}
|
||||
log.Info("updateNodePortAsync: K8s client created successfully")
|
||||
|
||||
// 尝试最多5次获取端口
|
||||
for i := 0; i < 5; i++ {
|
||||
// 尝试最多10次获取端口
|
||||
for i := 0; i < 10; i++ {
|
||||
log.Info("updateNodePortAsync: Attempt %d/10 to retrieve NodePort for %s", i+1, containerName)
|
||||
getOpts := &devcontainer_k8s_agent_module.GetDevcontainerOptions{
|
||||
GetOptions: metav1.GetOptions{},
|
||||
Name: containerName,
|
||||
@@ -409,6 +486,8 @@ func updateNodePortAsync(containerName string, namespace string, userId, repoId
|
||||
|
||||
devcontainer, err := devcontainer_k8s_agent_module.GetDevcontainer(&ctx, client, getOpts)
|
||||
if err == nil && devcontainer != nil && devcontainer.Status.NodePortAssigned > 0 {
|
||||
log.Info("updateNodePortAsync: Success! Found NodePort %d for %s",
|
||||
devcontainer.Status.NodePortAssigned, containerName)
|
||||
// 获取到正确的端口,更新数据库
|
||||
realNodePort := devcontainer.Status.NodePortAssigned
|
||||
|
||||
@@ -439,8 +518,127 @@ func updateNodePortAsync(containerName string, namespace string, userId, repoId
|
||||
return
|
||||
}
|
||||
|
||||
log.Info("updateNodePortAsync: Port not yet assigned, waiting 5 seconds before next attempt")
|
||||
time.Sleep(5 * time.Second)
|
||||
}
|
||||
|
||||
log.Warn("Failed to retrieve real NodePort after multiple attempts")
|
||||
log.Warn("updateNodePortAsync: Failed to retrieve real NodePort after multiple attempts")
|
||||
}
|
||||
|
||||
// 创建 DevContainer ttyd 服务的 Ingress 资源
|
||||
func createDevContainerTTYDIngress(ctx *context.Context, devcontainerName string) error {
|
||||
log.Info("createDevContainerTTYDIngress: 开始为 %s 创建 ttyd Ingress", devcontainerName)
|
||||
|
||||
// 获取标准 Kubernetes 客户端
|
||||
stdClient, err := getStandardKubernetesClient()
|
||||
if err != nil {
|
||||
log.Error("createDevContainerTTYDIngress: 获取 K8s 客户端失败: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// 从配置中读取域名
|
||||
cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf)
|
||||
if err != nil {
|
||||
log.Error("createDevContainerTTYDIngress: 加载配置文件失败: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
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"
|
||||
}
|
||||
|
||||
// 构建 PATH 和 Ingress 配置
|
||||
path := fmt.Sprintf("/%s/%s/dev-container-webterminal", username, repoName)
|
||||
ingressName := fmt.Sprintf("%s-ttyd-ingress", devcontainerName)
|
||||
serviceName := fmt.Sprintf("%s-svc", devcontainerName)
|
||||
|
||||
// 创建 Ingress 资源
|
||||
pathType := networkingv1.PathTypePrefix
|
||||
ingressClassName := "nginx"
|
||||
|
||||
ingress := &networkingv1.Ingress{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: ingressName,
|
||||
Namespace: setting.Devcontainer.Namespace,
|
||||
Annotations: map[string]string{
|
||||
"nginx.ingress.kubernetes.io/proxy-send-timeout": "3600",
|
||||
"nginx.ingress.kubernetes.io/proxy-read-timeout": "3600",
|
||||
"nginx.ingress.kubernetes.io/websocket-services": serviceName,
|
||||
"nginx.ingress.kubernetes.io/proxy-http-version": "1.1",
|
||||
"nginx.ingress.kubernetes.io/rewrite-target": "/",
|
||||
"nginx.ingress.kubernetes.io/configuration-snippet": `
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
`,
|
||||
},
|
||||
},
|
||||
Spec: networkingv1.IngressSpec{
|
||||
IngressClassName: &ingressClassName,
|
||||
Rules: []networkingv1.IngressRule{
|
||||
{
|
||||
Host: domain,
|
||||
IngressRuleValue: networkingv1.IngressRuleValue{
|
||||
HTTP: &networkingv1.HTTPIngressRuleValue{
|
||||
Paths: []networkingv1.HTTPIngressPath{
|
||||
{
|
||||
Path: path,
|
||||
PathType: &pathType,
|
||||
Backend: networkingv1.IngressBackend{
|
||||
Service: &networkingv1.IngressServiceBackend{
|
||||
Name: serviceName,
|
||||
Port: networkingv1.ServiceBackendPort{
|
||||
Number: 7681,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// 创建 Ingress 资源
|
||||
_, err = stdClient.NetworkingV1().Ingresses(setting.Devcontainer.Namespace).Create(*ctx, ingress, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
log.Error("createDevContainerTTYDIngress: 创建 Ingress 失败: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
log.Info("createDevContainerTTYDIngress: 成功创建 Ingress %s,ttyd 可通过 https://%s%s 访问", ingressName, domain, path)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 获取标准 Kubernetes 客户端
|
||||
func getStandardKubernetesClient() (*kubernetes.Clientset, error) {
|
||||
// 使用与 GetKubernetesClient 相同的逻辑获取配置
|
||||
config, err := clientcmd.BuildConfigFromFlags("", clientcmd.RecommendedHomeFile)
|
||||
if err != nil {
|
||||
// 如果集群外配置失败,尝试集群内配置
|
||||
log.Warn("Failed to obtain Kubernetes config outside of cluster: %v", err)
|
||||
config, err = rest.InClusterConfig()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取 K8s 配置失败 (集群内外均失败): %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 创建标准客户端
|
||||
stdClient, err := kubernetes.NewForConfig(config)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建标准 K8s 客户端失败: %v", err)
|
||||
}
|
||||
|
||||
return stdClient, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user