From 1d6ba90c2fc309996cc2518c208c42c3f876e657 Mon Sep 17 00:00:00 2001 From: panshuxiao Date: Tue, 25 Nov 2025 14:24:05 +0800 Subject: [PATCH] =?UTF-8?q?k8s=E5=AE=A2=E6=88=B7=E7=AB=AF=E8=8E=B7?= =?UTF-8?q?=E5=8F=96=E4=BB=8Ekubeconfig=E8=BF=81=E7=A7=BB=E5=88=B0token?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go.mod | 4 +- go.sum | 2 + models/appstore/user_app_instance.go | 8 +- models/migrations/devstar_v1_0/dv5.go | 29 +++ modules/k8s/k8s.go | 46 ++++ routers/web/user/setting/appstore.go | 34 +-- services/appstore/examples/mengning.json | 4 +- services/appstore/k8s_manager.go | 37 +++- services/appstore/manager.go | 271 +++++++++++++---------- templates/user/settings/appstore.tmpl | 90 ++++---- 10 files changed, 323 insertions(+), 202 deletions(-) create mode 100644 models/migrations/devstar_v1_0/dv5.go diff --git a/go.mod b/go.mod index 2fe65e580a..366b179476 100644 --- a/go.mod +++ b/go.mod @@ -161,10 +161,10 @@ require ( github.com/google/gnostic-models v0.6.9 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/morikuni/aec v1.0.0 // indirect + golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect golang.org/x/term v0.32.0 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect - k8s.io/klog/v2 v2.130.1 // indirect k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect @@ -179,9 +179,7 @@ require ( dario.cat/mergo v1.0.1 // indirect filippo.io/edwards25519 v1.1.0 // indirect git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078 // indirect - github.com/ArtisanCloud/PowerLibs/v3 v3.3.2 github.com/ArtisanCloud/PowerSocialite/v3 v3.0.8 // indirect - github.com/ArtisanCloud/PowerWeChat/v3 v3.4.21 github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 // indirect github.com/DataDog/zstd v1.5.7 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect diff --git a/go.sum b/go.sum index 6cc0148a6d..0dd6e1ae05 100644 --- a/go.sum +++ b/go.sum @@ -871,6 +871,8 @@ golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ug golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/image v0.26.0 h1:4XjIFEZWQmCZi6Wv8BoxsDhRU3RVnLX04dToTDAEPlY= golang.org/x/image v0.26.0/go.mod h1:lcxbMFAovzpnJxzXS3nyL83K27tmqtKzIJpctK8YO5c= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= diff --git a/models/appstore/user_app_instance.go b/models/appstore/user_app_instance.go index 43052eeb12..91a201901d 100644 --- a/models/appstore/user_app_instance.go +++ b/models/appstore/user_app_instance.go @@ -27,9 +27,11 @@ type UserAppInstance struct { MergedApp string `xorm:"TEXT"` // 合并后的完整应用配置JSON // 部署信息 - DeployType string `xorm:"NOT NULL"` // 实际部署类型 (kubernetes/docker) - Kubeconfig string `xorm:"TEXT"` // Kubeconfig内容(加密存储) - KubeconfigContext string // Kubeconfig context + DeployType string `xorm:"NOT NULL"` // 实际部署类型 (kubernetes/docker) + + // Kubernetes 凭据(URL + Token) + K8sURL string `xorm:"TEXT"` + K8sToken string `xorm:"TEXT"` // 元数据 CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` diff --git a/models/migrations/devstar_v1_0/dv5.go b/models/migrations/devstar_v1_0/dv5.go new file mode 100644 index 0000000000..9672aec74c --- /dev/null +++ b/models/migrations/devstar_v1_0/dv5.go @@ -0,0 +1,29 @@ +/* + * Copyright (c) Mengning Software. 2025. All rights reserved. + * Authors: DevStar Team, panshuxiao + * Create: 2025-11-24 + * Description: Migration dv5 adding Kubernetes credential columns. + */ + +package devstar_v1_0 + +import ( + "xorm.io/xorm" +) + +// AddK8sCredentialColumns adds k8s_url and k8s_token columns to user_app_instance. +func AddK8sCredentialColumns(x *xorm.Engine) error { + if _, err := x.Exec("ALTER TABLE user_app_instance ADD COLUMN k8s_url TEXT DEFAULT ''"); err != nil { + return ErrMigrateDevstarDatabase{ + Step: "add column 'k8s_url' to user_app_instance", + Message: err.Error(), + } + } + if _, err := x.Exec("ALTER TABLE user_app_instance ADD COLUMN k8s_token TEXT DEFAULT ''"); err != nil { + return ErrMigrateDevstarDatabase{ + Step: "add column 'k8s_token' to user_app_instance", + Message: err.Error(), + } + } + return nil +} diff --git a/modules/k8s/k8s.go b/modules/k8s/k8s.go index cc38d0a09b..3d5961d3ec 100644 --- a/modules/k8s/k8s.go +++ b/modules/k8s/k8s.go @@ -92,6 +92,52 @@ func GetKubernetesClient(ctx context.Context, kubeconfig []byte, contextName str return client, nil } +// GetKubernetesClientWithToken 通过用户提供的 k8sURL 和 token 获取动态客户端 +// 如果 k8sURL 和 token 为空,则优先使用配置文件中的 K8sConfig.Url 和 K8sConfig.Token +// 如果配置文件也未配置,则返回错误(不回退到 kubeconfig) +func GetKubernetesClientWithToken(ctx context.Context, k8sURL, token string) (dynamicclient.Interface, error) { + // 如果未提供 URL 和 token,优先使用配置文件中的设置 + if k8sURL == "" || token == "" { + // 优先使用配置文件中的 K8s 配置 + if setting.K8sConfig.Enable && setting.K8sConfig.Url != "" && setting.K8sConfig.Token != "" { + k8sURL = setting.K8sConfig.Url + token = setting.K8sConfig.Token + log.Info("使用配置文件中的 K8s 配置: URL=%s", k8sURL) + } + } + + // 如果仍然没有 URL 和 token,直接返回错误 + if k8sURL == "" || token == "" { + return nil, fmt.Errorf("k8sURL and token are required, neither provided nor found in config") + } + + // 使用 token 认证创建配置 + config := &clientgorest.Config{ + Host: k8sURL, + BearerToken: token, + TLSClientConfig: clientgorest.TLSClientConfig{ + Insecure: true, + }, + } + + applyClientDefaults(config) + + // 强制跳过 TLS 证书校验(与 GetKubernetesClient 保持一致) + // 同时清空 CA 配置 + config.TLSClientConfig.Insecure = true + config.TLSClientConfig.CAData = nil + config.TLSClientConfig.CAFile = "" + + // 尝试创建客户端,如果TLS验证失败则自动跳过验证 + client, err := dynamicclient.NewForConfig(config) + if err != nil { + // 再次兜底:若识别为 TLS 错误,已 Insecure,无需再次设置;否则将错误上抛 + return nil, fmt.Errorf("failed to create k8s client: %v", err) + } + + return client, nil +} + // restConfigFromKubeconfigBytes 基于 kubeconfig 内容构造 *rest.Config,支持指定 context(为空则使用 current-context) func restConfigFromKubeconfigBytes(kubeconfig []byte, contextName string) (*clientgorest.Config, error) { diff --git a/routers/web/user/setting/appstore.go b/routers/web/user/setting/appstore.go index f137bc0a6a..cf74dcdceb 100644 --- a/routers/web/user/setting/appstore.go +++ b/routers/web/user/setting/appstore.go @@ -257,8 +257,8 @@ func AppStoreInstall(ctx *context.Context) { appID := ctx.FormString("app_id") configJSON := ctx.FormString("config") installTarget := ctx.FormString("install_target") - kubeconfig := ctx.FormString("kubeconfig") - kubeconfigContext := ctx.FormString("kubeconfig_context") + k8sURL := ctx.FormString("k8s_url") + token := ctx.FormString("k8s_token") if appID == "" { ctx.JSON(400, map[string]string{"error": "应用ID不能为空"}) @@ -273,7 +273,7 @@ func AppStoreInstall(ctx *context.Context) { // 创建 manager 并执行安装 manager := appstore.NewManager(ctx, ctx.Doer.ID) - if err := manager.InstallApp(appID, configJSON, installTarget, kubeconfig, kubeconfigContext); err != nil { + if err := manager.InstallApp(appID, configJSON, installTarget, k8sURL, token); err != nil { // 根据错误类型返回相应的状态码和消息 if appErr, ok := err.(*appstore.AppStoreError); ok { switch appErr.Code { @@ -293,7 +293,7 @@ func AppStoreInstall(ctx *context.Context) { } // 安装成功 - if installTarget == "kubeconfig" && kubeconfig != "" { + if installTarget == "kubeconfig" && k8sURL != "" && token != "" { ctx.Flash.Success("应用已成功安装到自定义位置") } else { ctx.Flash.Success("应用已成功安装到默认位置") @@ -324,8 +324,8 @@ func AppStoreUpdate(ctx *context.Context) { appID := ctx.FormString("app_id") configJSON := ctx.FormString("config") installTarget := ctx.FormString("install_target") - kubeconfig := ctx.FormString("kubeconfig") - kubeconfigContext := ctx.FormString("kubeconfig_context") + k8sURL := ctx.FormString("k8s_url") + token := ctx.FormString("k8s_token") if appID == "" { ctx.JSON(400, map[string]string{"error": "应用ID不能为空"}) @@ -340,7 +340,7 @@ func AppStoreUpdate(ctx *context.Context) { // 创建 manager 并执行更新 manager := appstore.NewManager(ctx, ctx.Doer.ID) - if err := manager.UpdateInstalledApp(appID, configJSON, installTarget, kubeconfig, kubeconfigContext); err != nil { + if err := manager.UpdateInstalledApp(appID, configJSON, installTarget, k8sURL, token); err != nil { // 根据错误类型返回相应的状态码和消息 if appErr, ok := err.(*appstore.AppStoreError); ok { switch appErr.Code { @@ -360,7 +360,7 @@ func AppStoreUpdate(ctx *context.Context) { } // 更新成功 - if installTarget == "kubeconfig" && kubeconfig != "" { + if installTarget == "kubeconfig" && k8sURL != "" && token != "" { ctx.Flash.Success("应用已成功更新到自定义位置") } else { ctx.Flash.Success("应用已成功更新到默认位置") @@ -396,7 +396,7 @@ func AppStoreUninstall(ctx *context.Context) { } // 创建 manager 并执行卸载 - // UninstallApp 会自动从数据库读取 kubeconfig 判断是外部集群还是本地集群 + // UninstallApp 会自动从数据库读取保存的 Kubernetes 凭据判断是外部集群还是本地集群 manager := appstore.NewManager(ctx, ctx.Doer.ID) if err := manager.UninstallApp(appID); err != nil { // 根据错误类型返回相应的状态码和消息 @@ -445,8 +445,8 @@ func AppStoreResume(ctx *context.Context) { // 解析表单数据 appID := ctx.FormString("app_id") installTarget := ctx.FormString("install_target") - kubeconfig := ctx.FormString("kubeconfig") - kubeconfigContext := ctx.FormString("kubeconfig_context") + k8sURL := ctx.FormString("k8s_url") + token := ctx.FormString("k8s_token") if appID == "" { ctx.JSON(400, map[string]string{"error": "应用ID不能为空"}) @@ -455,7 +455,7 @@ func AppStoreResume(ctx *context.Context) { // 创建 manager 并执行恢复 manager := appstore.NewManager(ctx, ctx.Doer.ID) - if err := manager.ResumeApp(appID, installTarget, []byte(kubeconfig), kubeconfigContext); err != nil { + if err := manager.ResumeApp(appID, installTarget, k8sURL, token); err != nil { // 根据错误类型返回相应的状态码和消息 if appErr, ok := err.(*appstore.AppStoreError); ok { switch appErr.Code { @@ -475,7 +475,7 @@ func AppStoreResume(ctx *context.Context) { } // 恢复成功 - if installTarget == "kubeconfig" && kubeconfig != "" { + if installTarget == "kubeconfig" && k8sURL != "" && token != "" { ctx.Flash.Success("应用已成功在指定位置恢复") } else { ctx.Flash.Success("应用已成功恢复") @@ -506,8 +506,8 @@ func AppStoreStop(ctx *context.Context) { // 解析表单数据 appID := ctx.FormString("app_id") installTarget := ctx.FormString("install_target") - kubeconfig := ctx.FormString("kubeconfig") - kubeconfigContext := ctx.FormString("kubeconfig_context") + k8sURL := ctx.FormString("k8s_url") + token := ctx.FormString("k8s_token") if appID == "" { ctx.JSON(400, map[string]string{"error": "应用ID不能为空"}) @@ -516,7 +516,7 @@ func AppStoreStop(ctx *context.Context) { // 创建 manager 并执行暂停 manager := appstore.NewManager(ctx, ctx.Doer.ID) - if err := manager.StopApp(appID, installTarget, []byte(kubeconfig), kubeconfigContext); err != nil { + if err := manager.StopApp(appID, installTarget, k8sURL, token); err != nil { // 根据错误类型返回相应的状态码和消息 if appErr, ok := err.(*appstore.AppStoreError); ok { switch appErr.Code { @@ -536,7 +536,7 @@ func AppStoreStop(ctx *context.Context) { } // 暂停成功 - if installTarget == "kubeconfig" && kubeconfig != "" { + if installTarget == "kubeconfig" && k8sURL != "" && token != "" { ctx.Flash.Success("应用已成功在指定位置暂停") } else { ctx.Flash.Success("应用已成功暂停") diff --git a/services/appstore/examples/mengning.json b/services/appstore/examples/mengning.json index 7e9c3a16ff..341b3cafd3 100644 --- a/services/appstore/examples/mengning.json +++ b/services/appstore/examples/mengning.json @@ -1,6 +1,6 @@ { - "id": "mengningsoftware-2", - "name": "mengningsoftware-2", + "id": "mengningsoftware", + "name": "mengningsoftware", "description": "High-performance HTTP server and reverse proxy", "category": "web-server", "tags": ["web", "proxy", "http", "server"], diff --git a/services/appstore/k8s_manager.go b/services/appstore/k8s_manager.go index 53f303de80..2748a4808b 100644 --- a/services/appstore/k8s_manager.go +++ b/services/appstore/k8s_manager.go @@ -16,6 +16,7 @@ import ( "code.gitea.io/gitea/modules/k8s/application" "code.gitea.io/gitea/modules/log" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + dynamicclient "k8s.io/client-go/dynamic" ) // K8sManager handles Kubernetes-specific application operations @@ -23,6 +24,12 @@ type K8sManager struct { ctx context.Context } +// K8sCredential describes how to reach a Kubernetes cluster. +type K8sCredential struct { + K8sURL string + Token string +} + // NewK8sManager creates a new Kubernetes manager func NewK8sManager(ctx context.Context) *K8sManager { return &K8sManager{ @@ -30,8 +37,16 @@ func NewK8sManager(ctx context.Context) *K8sManager { } } +// buildK8sClient creates a dynamic client from the provided credential. +func (km *K8sManager) buildK8sClient(cred *K8sCredential) (dynamicclient.Interface, error) { + if cred != nil && cred.K8sURL != "" && cred.Token != "" { + return k8s.GetKubernetesClientWithToken(km.ctx, cred.K8sURL, cred.Token) + } + return k8s.GetKubernetesClientWithToken(km.ctx, "", "") +} + // InstallAppToKubernetes installs an application to a Kubernetes cluster -func (km *K8sManager) InstallAppToKubernetes(app *App, kubeconfig []byte, contextName string) error { +func (km *K8sManager) InstallAppToKubernetes(app *App, cred *K8sCredential) error { // Validate that the app supports Kubernetes deployment // 优先检查实际部署类型,如果为空则检查支持的部署类型 if app.Deploy.Type != "" && app.Deploy.Type != "kubernetes" { @@ -48,7 +63,7 @@ func (km *K8sManager) InstallAppToKubernetes(app *App, kubeconfig []byte, contex } // Get Kubernetes client - k8sClient, err := k8s.GetKubernetesClient(km.ctx, kubeconfig, contextName) + k8sClient, err := km.buildK8sClient(cred) if err != nil { return &AppStoreError{ Code: "KUBERNETES_CLIENT_ERROR", @@ -81,7 +96,7 @@ func (km *K8sManager) InstallAppToKubernetes(app *App, kubeconfig []byte, contex } // UninstallAppFromKubernetes uninstalls an application from a Kubernetes cluster -func (km *K8sManager) UninstallAppFromKubernetes(app *App, kubeconfig []byte, contextName string) error { +func (km *K8sManager) UninstallAppFromKubernetes(app *App, cred *K8sCredential) error { // Validate that the app supports Kubernetes deployment // 优先检查实际部署类型,如果为空则检查支持的部署类型 if app.Deploy.Type != "" && app.Deploy.Type != "kubernetes" { @@ -98,7 +113,7 @@ func (km *K8sManager) UninstallAppFromKubernetes(app *App, kubeconfig []byte, co } // Get Kubernetes client - k8sClient, err := k8s.GetKubernetesClient(km.ctx, kubeconfig, contextName) + k8sClient, err := km.buildK8sClient(cred) if err != nil { return &AppStoreError{ Code: "KUBERNETES_CLIENT_ERROR", @@ -131,7 +146,7 @@ func (km *K8sManager) UninstallAppFromKubernetes(app *App, kubeconfig []byte, co } // GetAppFromKubernetes gets an application from a Kubernetes cluster -func (km *K8sManager) GetAppFromKubernetes(app *App, kubeconfig []byte, contextName string) (interface{}, error) { +func (km *K8sManager) GetAppFromKubernetes(app *App, cred *K8sCredential) (interface{}, error) { // Validate that the app supports Kubernetes deployment // 优先检查实际部署类型,如果为空则检查支持的部署类型 if app.Deploy.Type != "" && app.Deploy.Type != "kubernetes" { @@ -148,7 +163,7 @@ func (km *K8sManager) GetAppFromKubernetes(app *App, kubeconfig []byte, contextN } // Get Kubernetes client - k8sClient, err := k8s.GetKubernetesClient(km.ctx, kubeconfig, contextName) + k8sClient, err := km.buildK8sClient(cred) if err != nil { return nil, &AppStoreError{ Code: "KUBERNETES_CLIENT_ERROR", @@ -172,7 +187,7 @@ func (km *K8sManager) GetAppFromKubernetes(app *App, kubeconfig []byte, contextN } // ListAppsFromKubernetes lists applications from a Kubernetes cluster -func (km *K8sManager) ListAppsFromKubernetes(app *App, kubeconfig []byte, contextName string) (*applicationv1.ApplicationList, error) { +func (km *K8sManager) ListAppsFromKubernetes(app *App, cred *K8sCredential) (*applicationv1.ApplicationList, error) { // Validate that the app supports Kubernetes deployment // 优先检查实际部署类型,如果为空则检查支持的部署类型 if app.Deploy.Type != "" && app.Deploy.Type != "kubernetes" { @@ -189,7 +204,7 @@ func (km *K8sManager) ListAppsFromKubernetes(app *App, kubeconfig []byte, contex } // Get Kubernetes client - k8sClient, err := k8s.GetKubernetesClient(km.ctx, kubeconfig, contextName) + k8sClient, err := km.buildK8sClient(cred) if err != nil { return nil, &AppStoreError{ Code: "KUBERNETES_CLIENT_ERROR", @@ -213,7 +228,7 @@ func (km *K8sManager) ListAppsFromKubernetes(app *App, kubeconfig []byte, contex } // UpdateAppInKubernetes updates an application in a Kubernetes cluster -func (km *K8sManager) UpdateAppInKubernetes(app *App, kubeconfig []byte, contextName string) error { +func (km *K8sManager) UpdateAppInKubernetes(app *App, cred *K8sCredential) error { // Validate that the app supports Kubernetes deployment // 优先检查实际部署类型,如果为空则检查支持的部署类型 if app.Deploy.Type != "" && app.Deploy.Type != "kubernetes" { @@ -230,7 +245,7 @@ func (km *K8sManager) UpdateAppInKubernetes(app *App, kubeconfig []byte, context } // Get Kubernetes client - k8sClient, err := k8s.GetKubernetesClient(km.ctx, kubeconfig, contextName) + k8sClient, err := km.buildK8sClient(cred) if err != nil { return &AppStoreError{ Code: "KUBERNETES_CLIENT_ERROR", @@ -240,7 +255,7 @@ func (km *K8sManager) UpdateAppInKubernetes(app *App, kubeconfig []byte, context } // 先获取现有的应用,以保留元数据(特别是 resourceVersion) - existingApp, err := km.GetAppFromKubernetes(app, kubeconfig, contextName) + existingApp, err := km.GetAppFromKubernetes(app, cred) if err != nil { // 如果应用不存在,尝试创建新应用 log.Warn("Application not found in Kubernetes, attempting to create new application: %v", err) diff --git a/services/appstore/manager.go b/services/appstore/manager.go index f267b7d5a0..1778a9e8cb 100644 --- a/services/appstore/manager.go +++ b/services/appstore/manager.go @@ -40,6 +40,29 @@ func NewManager(ctx *gitea_context.Context, userID int64) *Manager { } } +func buildCredentialFromInput(installTarget, k8sURL, token string) *K8sCredential { + if installTarget == "kubeconfig" && k8sURL != "" && token != "" { + return &K8sCredential{ + K8sURL: k8sURL, + Token: token, + } + } + return nil +} + +func credentialFromInstance(instance *user_app_instance.UserAppInstance) *K8sCredential { + if instance == nil { + return nil + } + if instance.K8sURL != "" && instance.K8sToken != "" { + return &K8sCredential{ + K8sURL: instance.K8sURL, + Token: instance.K8sToken, + } + } + return nil +} + // ListApps returns all available applications from database func (m *Manager) ListApps() ([]App, error) { appStores, err := appstore_model.ListAppStores(m.ctx, nil) @@ -220,7 +243,7 @@ func (m *Manager) SearchApps(query string, category string, tags []string) ([]Ap tagMatch := false for _, searchTag := range tags { for _, appTag := range app.Tags { - if strings.ToLower(appTag) == strings.ToLower(searchTag) { + if strings.EqualFold(appTag, searchTag) { tagMatch = true break } @@ -439,7 +462,7 @@ func (m *Manager) ValidateUserConfig(appID string, userConfig UserConfig) error } // InstallApp installs an application based on the provided parameters -func (m *Manager) InstallApp(appID string, configJSON string, installTarget string, kubeconfig string, kubeconfigContext string) error { +func (m *Manager) InstallApp(appID string, configJSON string, installTarget string, k8sURL string, token string) error { // 获取应用信息 app, err := m.GetApp(appID) if err != nil { @@ -465,50 +488,51 @@ func (m *Manager) InstallApp(appID string, configJSON string, installTarget stri deployType := mergedApp.Deploy.Type if deployType == "" { // 如果 Deploy.Type 为空,根据 DeploymentType 推断 - if mergedApp.DeploymentType == "kubernetes" || mergedApp.DeploymentType == "both" { + switch mergedApp.DeploymentType { + case "kubernetes", "both": deployType = "kubernetes" - } else if mergedApp.DeploymentType == "docker" { + case "docker": deployType = "docker" } } - // 根据安装目标和应用实际部署类型决定安装方式 - if installTarget == "kubeconfig" && kubeconfig != "" { - // 安装到外部 Kubernetes 集群 - if err := m.InstallAppToKubernetes(mergedApp, []byte(kubeconfig), kubeconfigContext); err != nil { + inputCred := buildCredentialFromInput(installTarget, k8sURL, token) + if installTarget == "kubeconfig" { + if inputCred == nil { + return &AppStoreError{ + Code: "INVALID_K8S_CREDENTIAL", + Message: "自定义 Kubernetes 安装需要提供 API 地址和 Token", + } + } + if deployType != "kubernetes" { + return &AppStoreError{ + Code: "DEPLOYMENT_TYPE_ERROR", + Message: "该应用不支持 Kubernetes 部署", + } + } + } + + // 根据应用实际部署类型决定安装方式 + log.Info("InstallApp: mergedApp.Deploy.Type = %s", mergedApp.Deploy.Type) + switch deployType { + case "kubernetes": + if err := m.InstallAppToKubernetes(mergedApp, inputCred); err != nil { return &AppStoreError{ Code: "KUBERNETES_INSTALL_ERROR", Message: "Kubernetes 安装失败", Details: err.Error(), } } - } else { - // 根据应用实际部署类型决定本地安装方式 - log.Info("InstallApp: mergedApp.Deploy.Type = %s", mergedApp.Deploy.Type) - switch deployType { - case "kubernetes": - // 应用要部署到 Kubernetes,安装到本地 K8s 集群 - if err := m.InstallAppToKubernetes(mergedApp, nil, ""); err != nil { - return &AppStoreError{ - Code: "KUBERNETES_INSTALL_ERROR", - Message: "本地 Kubernetes 安装失败", - Details: err.Error(), - } - } - case "docker": - // 应用要部署到 Docker,安装到本地 Docker - // TODO: 实现 Docker 安装逻辑 - return &AppStoreError{ - Code: "NOT_IMPLEMENTED", - Message: "本地 Docker 安装功能开发中", - } - default: - // 未知部署类型,默认尝试 Docker - // TODO: 实现 Docker 安装逻辑 - return &AppStoreError{ - Code: "NOT_IMPLEMENTED", - Message: "本地安装功能开发中", - } + case "docker": + // TODO: 实现 Docker 安装逻辑 + return &AppStoreError{ + Code: "NOT_IMPLEMENTED", + Message: "本地 Docker 安装功能开发中", + } + default: + return &AppStoreError{ + Code: "NOT_IMPLEMENTED", + Message: "本地安装功能开发中", } } @@ -524,14 +548,16 @@ func (m *Manager) InstallApp(appID string, configJSON string, installTarget stri } instance := &user_app_instance.UserAppInstance{ - UserID: m.userID, - AppID: appID, - InstanceName: mergedApp.Name, // 使用应用名称作为实例名称 - UserConfig: configJSON, - MergedApp: string(mergedAppJSON), - DeployType: deployType, - Kubeconfig: kubeconfig, - KubeconfigContext: kubeconfigContext, + UserID: m.userID, + AppID: appID, + InstanceName: mergedApp.Name, + UserConfig: configJSON, + MergedApp: string(mergedAppJSON), + DeployType: deployType, + } + if inputCred != nil && deployType == "kubernetes" { + instance.K8sURL = inputCred.K8sURL + instance.K8sToken = inputCred.Token } if err := user_app_instance.CreateUserAppInstance(m.ctx, instance); err != nil { @@ -549,7 +575,7 @@ func (m *Manager) InstallApp(appID string, configJSON string, installTarget stri // UpdateInstalledApp updates an already installed application with new configuration // The update flow mirrors InstallApp: merge config → choose target → call K8s/Docker updater -func (m *Manager) UpdateInstalledApp(appID string, configJSON string, installTarget string, kubeconfig string, kubeconfigContext string) error { +func (m *Manager) UpdateInstalledApp(appID string, configJSON string, installTarget string, k8sURL string, token string) error { // 获取用户的应用实例 instance, err := user_app_instance.GetUserAppInstanceByAppID(m.ctx, m.userID, appID) if err != nil { @@ -589,31 +615,38 @@ func (m *Manager) UpdateInstalledApp(appID string, configJSON string, installTar // 确定部署类型 deployType := mergedApp.Deploy.Type if deployType == "" { - deployType = instance.DeployType // 使用实例中保存的部署类型 + deployType = instance.DeployType } - // 根据安装目标和应用实际部署类型决定更新方式 - if installTarget == "kubeconfig" && kubeconfig != "" { - // 更新外部 Kubernetes 集群中的应用实例 - if deployType == "kubernetes" { - if err := m.UpdateAppInKubernetes(mergedApp, []byte(kubeconfig), kubeconfigContext); err != nil { - return &AppStoreError{Code: "KUBERNETES_UPDATE_ERROR", Message: "Kubernetes 更新失败", Details: err.Error()} - } - } else { - return &AppStoreError{Code: "NOT_IMPLEMENTED", Message: "外部环境 Docker 更新功能开发中"} + inputCred := buildCredentialFromInput(installTarget, k8sURL, token) + if installTarget == "kubeconfig" && inputCred == nil { + return &AppStoreError{ + Code: "INVALID_K8S_CREDENTIAL", + Message: "自定义 Kubernetes 更新需要提供 API 地址和 Token", } - } else { - // 本地更新(依据应用部署类型) - if deployType == "kubernetes" { - if err := m.UpdateAppInKubernetes(mergedApp, nil, ""); err != nil { - return &AppStoreError{Code: "KUBERNETES_UPDATE_ERROR", Message: "本地 Kubernetes 更新失败", Details: err.Error()} - } - } else { - return &AppStoreError{Code: "NOT_IMPLEMENTED", Message: "本地 Docker 更新功能开发中"} + } + if installTarget == "kubeconfig" && deployType != "kubernetes" { + return &AppStoreError{ + Code: "DEPLOYMENT_TYPE_ERROR", + Message: "该应用不支持 Kubernetes 部署", } } - // 更新成功后,更新用户应用实例 + // 根据部署类型执行更新 + switch deployType { + case "kubernetes": + targetCred := inputCred + if targetCred == nil { + targetCred = credentialFromInstance(instance) + } + if err := m.UpdateAppInKubernetes(mergedApp, targetCred); err != nil { + return &AppStoreError{Code: "KUBERNETES_UPDATE_ERROR", Message: "Kubernetes 更新失败", Details: err.Error()} + } + default: + return &AppStoreError{Code: "NOT_IMPLEMENTED", Message: "本地 Docker 更新功能开发中"} + } + + // 更新成功后,写回数据库 mergedAppJSON, err := json.Marshal(mergedApp) if err != nil { log.Error("Failed to marshal merged app: %v", err) @@ -627,8 +660,18 @@ func (m *Manager) UpdateInstalledApp(appID string, configJSON string, installTar instance.UserConfig = configJSON instance.MergedApp = string(mergedAppJSON) instance.DeployType = deployType - instance.Kubeconfig = kubeconfig - instance.KubeconfigContext = kubeconfigContext + if deployType == "kubernetes" { + if inputCred != nil { + instance.K8sURL = inputCred.K8sURL + instance.K8sToken = inputCred.Token + } else if installTarget != "kubeconfig" { + instance.K8sURL = "" + instance.K8sToken = "" + } + } else { + instance.K8sURL = "" + instance.K8sToken = "" + } if err := user_app_instance.UpdateUserAppInstance(m.ctx, instance); err != nil { log.Error("Failed to update user app instance: %v", err) @@ -645,7 +688,7 @@ func (m *Manager) UpdateInstalledApp(appID string, configJSON string, installTar // UninstallApp uninstalls an application // It automatically determines whether to uninstall from external cluster or local cluster -// based on the kubeconfig stored in the database instance +// based on the Kubernetes credential stored in the database instance func (m *Manager) UninstallApp(appID string) error { // 获取用户的应用实例 instance, err := user_app_instance.GetUserAppInstanceByAppID(m.ctx, m.userID, appID) @@ -673,45 +716,28 @@ func (m *Manager) UninstallApp(appID string) error { } } - // 根据数据库实例中保存的 kubeconfig 判断卸载方式 - // 如果数据库中有 kubeconfig,说明是安装在外部集群的,使用外部集群卸载 - // 如果数据库中没有 kubeconfig,说明是安装在本地集群的,使用本地卸载 - if instance.Kubeconfig != "" { - // 从外部 Kubernetes 集群卸载 - if instance.DeployType == "kubernetes" { - log.Info("UninstallApp: Uninstalling from external cluster, appID=%s, kubeconfig length=%d", appID, len(instance.Kubeconfig)) - if err := m.UninstallAppFromKubernetes(&app, []byte(instance.Kubeconfig), instance.KubeconfigContext); err != nil { - return &AppStoreError{ - Code: "KUBERNETES_UNINSTALL_ERROR", - Message: "Kubernetes 卸载失败", - Details: err.Error(), - } - } + // 根据数据库中保存的凭据判断卸载方式:存在 URL+Token 则视为外部集群,否则使用默认集群 + cred := credentialFromInstance(instance) + if instance.DeployType == "kubernetes" { + if cred != nil { + log.Info("UninstallApp: Uninstalling from external cluster, appID=%s, url=%s", appID, cred.K8sURL) } else { + log.Info("UninstallApp: Uninstalling from default cluster, appID=%s", appID) + } + + if err := m.UninstallAppFromKubernetes(&app, cred); err != nil { return &AppStoreError{ - Code: "NOT_IMPLEMENTED", - Message: "外部环境 Docker 卸载功能开发中", + Code: "KUBERNETES_UNINSTALL_ERROR", + Message: "Kubernetes 卸载失败", + Details: err.Error(), } } } else { - // 本地卸载(依据实例中保存的部署类型) - if instance.DeployType == "kubernetes" { - log.Info("UninstallApp: Uninstalling from local cluster, appID=%s", appID) - if err := m.UninstallAppFromKubernetes(&app, nil, ""); err != nil { - return &AppStoreError{ - Code: "KUBERNETES_UNINSTALL_ERROR", - Message: "本地 Kubernetes 卸载失败", - Details: err.Error(), - } - } - } else { - return &AppStoreError{ - Code: "NOT_IMPLEMENTED", - Message: "本地 Docker 卸载功能开发中", - } + return &AppStoreError{ + Code: "NOT_IMPLEMENTED", + Message: "本地 Docker 卸载功能开发中", } } - // 卸载成功后,删除用户应用实例 if err := user_app_instance.DeleteUserAppInstanceByAppID(m.ctx, m.userID, appID); err != nil { log.Error("Failed to delete user app instance: %v", err) @@ -725,28 +751,28 @@ func (m *Manager) UninstallApp(appID string) error { } // InstallAppToKubernetes installs an application to a Kubernetes cluster -func (m *Manager) InstallAppToKubernetes(app *App, kubeconfig []byte, contextName string) error { - return m.k8s.InstallAppToKubernetes(app, kubeconfig, contextName) +func (m *Manager) InstallAppToKubernetes(app *App, cred *K8sCredential) error { + return m.k8s.InstallAppToKubernetes(app, cred) } // UninstallAppFromKubernetes uninstalls an application from a Kubernetes cluster -func (m *Manager) UninstallAppFromKubernetes(app *App, kubeconfig []byte, contextName string) error { - return m.k8s.UninstallAppFromKubernetes(app, kubeconfig, contextName) +func (m *Manager) UninstallAppFromKubernetes(app *App, cred *K8sCredential) error { + return m.k8s.UninstallAppFromKubernetes(app, cred) } // GetAppFromKubernetes gets an application from a Kubernetes cluster -func (m *Manager) GetAppFromKubernetes(app *App, kubeconfig []byte, contextName string) (interface{}, error) { - return m.k8s.GetAppFromKubernetes(app, kubeconfig, contextName) +func (m *Manager) GetAppFromKubernetes(app *App, cred *K8sCredential) (interface{}, error) { + return m.k8s.GetAppFromKubernetes(app, cred) } // ListAppsFromKubernetes lists applications from a Kubernetes cluster -func (m *Manager) ListAppsFromKubernetes(app *App, kubeconfig []byte, contextName string) (*applicationv1.ApplicationList, error) { - return m.k8s.ListAppsFromKubernetes(app, kubeconfig, contextName) +func (m *Manager) ListAppsFromKubernetes(app *App, cred *K8sCredential) (*applicationv1.ApplicationList, error) { + return m.k8s.ListAppsFromKubernetes(app, cred) } // UpdateAppInKubernetes updates an application in a Kubernetes cluster -func (m *Manager) UpdateAppInKubernetes(app *App, kubeconfig []byte, contextName string) error { - return m.k8s.UpdateAppInKubernetes(app, kubeconfig, contextName) +func (m *Manager) UpdateAppInKubernetes(app *App, cred *K8sCredential) error { + return m.k8s.UpdateAppInKubernetes(app, cred) } // IsInstalledAppReady 提供给上层的就绪判断入口,避免直接依赖 k8s 层实现 @@ -788,14 +814,9 @@ func (m *Manager) GetAppStatus(appID string) (map[string]interface{}, error) { // 检查应用是否支持 Kubernetes 部署 if instance.DeployType == "kubernetes" { // 尝试从 Kubernetes 获取状态 - var kubeconfig []byte - var contextName string - if instance.Kubeconfig != "" { - kubeconfig = []byte(instance.Kubeconfig) - contextName = instance.KubeconfigContext - } + cred := credentialFromInstance(instance) - k8sApp, err := m.GetAppFromKubernetes(&app, kubeconfig, contextName) + k8sApp, err := m.GetAppFromKubernetes(&app, cred) if err != nil { // 如果应用实例存在于数据库中,但 Kubernetes 中找不到,可能是被暂停了 // 返回暂停状态而不是未安装状态 @@ -845,7 +866,7 @@ func (m *Manager) GetAppStatus(appID string) (map[string]interface{}, error) { } // StopApp stops an application by setting replicas to 0 -func (m *Manager) StopApp(appID string, installTarget string, kubeconfig []byte, kubeconfigContext string) error { +func (m *Manager) StopApp(appID string, installTarget string, k8sURL string, token string) error { // 获取用户的应用实例 instance, err := user_app_instance.GetUserAppInstanceByAppID(m.ctx, m.userID, appID) if err != nil { @@ -880,8 +901,12 @@ func (m *Manager) StopApp(appID string, installTarget string, kubeconfig []byte, stoppedApp.Deploy.Kubernetes.Replicas = 0 } - // 调用 k8s 层的 UpdateAppInKubernetes 函数来停止应用 - if err := m.k8s.UpdateAppInKubernetes(&stoppedApp, kubeconfig, kubeconfigContext); err != nil { + cred := buildCredentialFromInput(installTarget, k8sURL, token) + if cred == nil { + cred = credentialFromInstance(instance) + } + + if err := m.UpdateAppInKubernetes(&stoppedApp, cred); err != nil { return &AppStoreError{ Code: "KUBERNETES_STOP_ERROR", Message: "停止应用失败", @@ -899,7 +924,7 @@ func (m *Manager) StopApp(appID string, installTarget string, kubeconfig []byte, } // ResumeApp resumes an application by restoring its original replica count -func (m *Manager) ResumeApp(appID string, installTarget string, kubeconfig []byte, kubeconfigContext string) error { +func (m *Manager) ResumeApp(appID string, installTarget string, k8sURL string, token string) error { // 获取用户的应用实例 instance, err := user_app_instance.GetUserAppInstanceByAppID(m.ctx, m.userID, appID) if err != nil { @@ -937,8 +962,12 @@ func (m *Manager) ResumeApp(appID string, installTarget string, kubeconfig []byt } } - // 调用 k8s 层的 UpdateAppInKubernetes 函数来恢复应用 - if err := m.k8s.UpdateAppInKubernetes(&resumedApp, kubeconfig, kubeconfigContext); err != nil { + cred := buildCredentialFromInput(installTarget, k8sURL, token) + if cred == nil { + cred = credentialFromInstance(instance) + } + + if err := m.UpdateAppInKubernetes(&resumedApp, cred); err != nil { return &AppStoreError{ Code: "KUBERNETES_RESUME_ERROR", Message: "恢复应用失败", diff --git a/templates/user/settings/appstore.tmpl b/templates/user/settings/appstore.tmpl index a9dc7f1ae1..7c281b7c61 100644 --- a/templates/user/settings/appstore.tmpl +++ b/templates/user/settings/appstore.tmpl @@ -274,18 +274,18 @@
- +
- @@ -377,10 +377,10 @@ let filteredApps = []; let currentApp = null; let currentAppStatus = null; // 存储当前应用的运行状态 let storeSource = 'local'; // local | devstar -// 安装位置:local | kubeconfig +// 安装位置:local | remote let installTarget = 'local'; -let installKubeconfigContent = ''; -let installKubeconfigContext = ''; +let installK8sURL = ''; +let installK8sToken = ''; // Initialize the page document.addEventListener('DOMContentLoaded', function() { @@ -420,7 +420,7 @@ function setupInstallTargetUI() { const radioButtons = document.querySelectorAll('#install-modal input[name="installTargetRadio"]'); radioButtons.forEach(radio => { radio.addEventListener('change', function() { - const kubeconfigFields = document.querySelector('#install-modal #kubeconfig-fields'); + const kubeconfigFields = document.querySelector('#install-modal #k8s-credential-fields'); if (kubeconfigFields) { kubeconfigFields.style.display = (this.value === 'kubeconfig') ? 'block' : 'none'; } @@ -428,17 +428,17 @@ function setupInstallTargetUI() { installTarget = this.value; }); }); - // kubeconfig 内容变化时,同步到全局变量 - const kcContentEl = document.querySelector('#install-modal #kubeconfig-content'); + // Kubernetes URL/Token 变化时,同步到全局变量 + const kcContentEl = document.querySelector('#install-modal #k8s-url'); if (kcContentEl) { kcContentEl.addEventListener('input', function() { - installKubeconfigContent = this.value.trim(); + installK8sURL = this.value.trim(); }); } - const kcContextEl = document.querySelector('#install-modal #kubeconfig-context'); + const kcContextEl = document.querySelector('#install-modal #k8s-token'); if (kcContextEl) { kcContextEl.addEventListener('input', function() { - installKubeconfigContext = this.value.trim(); + installK8sToken = this.value.trim(); }); } } @@ -813,14 +813,14 @@ function showInstallModal(appId) { // 追加安装位置区块已保留在模板中,这里只同步状态 const radios = document.querySelectorAll('#install-modal input[name="installTargetRadio"]'); radios.forEach(r => { r.checked = (r.value === installTarget); }); - const kubeconfigFields = document.querySelector('#install-modal #kubeconfig-fields'); + const kubeconfigFields = document.querySelector('#install-modal #k8s-credential-fields'); if (kubeconfigFields) { kubeconfigFields.style.display = (installTarget === 'kubeconfig') ? 'block' : 'none'; } - const kcContent = document.querySelector('#install-modal #kubeconfig-content'); - const kcContext = document.querySelector('#install-modal #kubeconfig-context'); - if (kcContent) kcContent.value = installKubeconfigContent || ''; - if (kcContext) kcContext.value = installKubeconfigContext || ''; + const kcContent = document.querySelector('#install-modal #k8s-url'); + const kcContext = document.querySelector('#install-modal #k8s-token'); + if (kcContent) kcContent.value = installK8sURL || ''; + if (kcContext) kcContext.value = installK8sToken || ''; const modal = document.getElementById('install-modal'); if (modal) { @@ -932,14 +932,14 @@ function showUpdateModal(appId) { // 设置安装位置表单 const radios = document.querySelectorAll('#install-modal input[name="installTargetRadio"]'); radios.forEach(r => { r.checked = (r.value === installTarget); }); - const kubeconfigFields = document.querySelector('#install-modal #kubeconfig-fields'); + const kubeconfigFields = document.querySelector('#install-modal #k8s-credential-fields'); if (kubeconfigFields) { kubeconfigFields.style.display = (installTarget === 'kubeconfig') ? 'block' : 'none'; } - const kcContent = document.querySelector('#install-modal #kubeconfig-content'); - const kcContext = document.querySelector('#install-modal #kubeconfig-context'); - if (kcContent) kcContent.value = installKubeconfigContent || ''; - if (kcContext) kcContext.value = installKubeconfigContext || ''; + const kcContent = document.querySelector('#install-modal #k8s-url'); + const kcContext = document.querySelector('#install-modal #k8s-token'); + if (kcContent) kcContent.value = installK8sURL || ''; + if (kcContext) kcContext.value = installK8sToken || ''; // 修改安装按钮文字为"更新" const installBtn = document.querySelector('#install-modal .ui.primary.button[onclick="installApp()"]'); @@ -978,12 +978,12 @@ function updateApp() { const selectedTarget = document.querySelector('#install-modal input[name="installTargetRadio"]:checked'); installTarget = selectedTarget ? selectedTarget.value : 'local'; if (installTarget === 'kubeconfig') { - const kcContentEl = document.querySelector('#install-modal #kubeconfig-content'); - const kcContextEl = document.querySelector('#install-modal #kubeconfig-context'); - installKubeconfigContent = kcContentEl ? kcContentEl.value.trim() : ''; - installKubeconfigContext = kcContextEl ? kcContextEl.value.trim() : ''; - if (!installKubeconfigContent) { - alert('请输入 kubeconfig 内容'); + const kcContentEl = document.querySelector('#install-modal #k8s-url'); + const kcContextEl = document.querySelector('#install-modal #k8s-token'); + installK8sURL = kcContentEl ? kcContentEl.value.trim() : ''; + installK8sToken = kcContextEl ? kcContextEl.value.trim() : ''; + if (!installK8sURL || !installK8sToken) { + alert('请输入 Kubernetes API 地址和 Token'); return; } config.deploy = { type: 'kubernetes' }; @@ -1036,14 +1036,14 @@ function updateApp() { if (installTarget === 'kubeconfig') { const kcInput = document.createElement('input'); kcInput.type = 'hidden'; - kcInput.name = 'kubeconfig'; - kcInput.value = installKubeconfigContent; + kcInput.name = 'k8s_url'; + kcInput.value = installK8sURL; updateForm.appendChild(kcInput); const kctxInput = document.createElement('input'); kctxInput.type = 'hidden'; - kctxInput.name = 'kubeconfig_context'; - kctxInput.value = installKubeconfigContext; + kctxInput.name = 'k8s_token'; + kctxInput.value = installK8sToken; updateForm.appendChild(kctxInput); } @@ -1306,12 +1306,12 @@ async function installApp() { const selectedTarget = document.querySelector('#install-modal input[name="installTargetRadio"]:checked'); installTarget = selectedTarget ? selectedTarget.value : 'local'; if (installTarget === 'kubeconfig') { - const kcContentEl = document.querySelector('#install-modal #kubeconfig-content'); - const kcContextEl = document.querySelector('#install-modal #kubeconfig-context'); - installKubeconfigContent = kcContentEl ? kcContentEl.value.trim() : ''; - installKubeconfigContext = kcContextEl ? kcContextEl.value.trim() : ''; - if (!installKubeconfigContent) { - alert('请输入 kubeconfig 内容'); + const kcContentEl = document.querySelector('#install-modal #k8s-url'); + const kcContextEl = document.querySelector('#install-modal #k8s-token'); + installK8sURL = kcContentEl ? kcContentEl.value.trim() : ''; + installK8sToken = kcContextEl ? kcContextEl.value.trim() : ''; + if (!installK8sURL || !installK8sToken) { + alert('请输入 Kubernetes API 地址和 Token'); return; } config.deploy = { type: 'kubernetes' }; @@ -1359,13 +1359,13 @@ async function installApp() { if (installTarget === 'kubeconfig') { const kcInput = document.createElement('input'); kcInput.type = 'hidden'; - kcInput.name = 'kubeconfig'; - kcInput.value = installKubeconfigContent; + kcInput.name = 'k8s_url'; + kcInput.value = installK8sURL; installForm.appendChild(kcInput); const kctxInput = document.createElement('input'); kctxInput.type = 'hidden'; - kctxInput.name = 'kubeconfig_context'; - kctxInput.value = installKubeconfigContext; + kctxInput.name = 'k8s_token'; + kctxInput.value = installK8sToken; installForm.appendChild(kctxInput); } // 提交安装表单