k8s客户端获取从kubeconfig迁移到token

This commit is contained in:
panshuxiao
2025-11-25 14:24:05 +08:00
repo.diff.parent f08a0d02c3
repo.diff.commit 1d6ba90c2f
repo.diff.stats_desc%!(EXTRA int=10, int=323, int=202)

4
go.mod
repo.diff.view_file

@@ -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

2
go.sum
repo.diff.view_file

@@ -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=

repo.diff.view_file

@@ -28,8 +28,10 @@ type UserAppInstance struct {
// 部署信息
DeployType string `xorm:"NOT NULL"` // 实际部署类型 (kubernetes/docker)
Kubeconfig string `xorm:"TEXT"` // Kubeconfig内容加密存储
KubeconfigContext string // Kubeconfig context
// Kubernetes 凭据URL + Token
K8sURL string `xorm:"TEXT"`
K8sToken string `xorm:"TEXT"`
// 元数据
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`

repo.diff.view_file

@@ -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
}

repo.diff.view_file

@@ -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) {

repo.diff.view_file

@@ -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("应用已成功暂停")

repo.diff.view_file

@@ -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"],

repo.diff.view_file

@@ -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)

repo.diff.view_file

@@ -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,52 +488,53 @@ 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: "本地安装功能开发中",
}
}
}
// 安装成功后,保存用户应用实例到数据库
mergedAppJSON, err := json.Marshal(mergedApp)
@@ -526,12 +550,14 @@ func (m *Manager) InstallApp(appID string, configJSON string, installTarget stri
instance := &user_app_instance.UserAppInstance{
UserID: m.userID,
AppID: appID,
InstanceName: mergedApp.Name, // 使用应用名称作为实例名称
InstanceName: mergedApp.Name,
UserConfig: configJSON,
MergedApp: string(mergedAppJSON),
DeployType: deployType,
Kubeconfig: kubeconfig,
KubeconfigContext: kubeconfigContext,
}
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 {
inputCred := buildCredentialFromInput(installTarget, k8sURL, token)
if installTarget == "kubeconfig" && inputCred == nil {
return &AppStoreError{
Code: "INVALID_K8S_CREDENTIAL",
Message: "自定义 Kubernetes 更新需要提供 API 地址和 Token",
}
}
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()}
}
} else {
return &AppStoreError{Code: "NOT_IMPLEMENTED", Message: "外部环境 Docker 更新功能开发中"}
}
} else {
// 本地更新(依据应用部署类型)
if deployType == "kubernetes" {
if err := m.UpdateAppInKubernetes(mergedApp, nil, ""); err != nil {
return &AppStoreError{Code: "KUBERNETES_UPDATE_ERROR", Message: "本地 Kubernetes 更新失败", Details: err.Error()}
}
} else {
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 集群卸载
// 根据数据库中保存的凭据判断卸载方式:存在 URL+Token 则视为外部集群,否则使用默认集群
cred := credentialFromInstance(instance)
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 {
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: "KUBERNETES_UNINSTALL_ERROR",
Message: "Kubernetes 卸载失败",
Details: err.Error(),
}
}
} else {
return &AppStoreError{
Code: "NOT_IMPLEMENTED",
Message: "外部环境 Docker 卸载功能开发中",
}
}
} 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 卸载功能开发中",
}
}
}
// 卸载成功后,删除用户应用实例
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: "恢复应用失败",

repo.diff.view_file

@@ -274,18 +274,18 @@
<div class="field">
<div class="ui radio checkbox">
<input type="radio" name="installTargetRadio" value="kubeconfig" {{if not .IsAdmin}}checked{{end}}>
<label>自定义位置Kubeconfig</label>
<label>自定义位置Kubernetes URL + Token</label>
</div>
</div>
</div>
<div id="kubeconfig-fields" style="display:none;">
<div id="k8s-credential-fields" style="display:none;">
<div class="field">
<label>Kubeconfig粘贴内容</label>
<textarea id="kubeconfig-content" rows="8" placeholder="粘贴 kubeconfig 内容"></textarea>
<label>Kubernetes API 地址</label>
<input type="text" id="k8s-url" placeholder="例如https://your-cluster:6443">
</div>
<div class="field">
<label>Context 名称(可选)</label>
<input type="text" id="kubeconfig-context" placeholder="不填则使用 current-context">
<label>Bearer Token</label>
<textarea id="k8s-token" rows="4" placeholder="粘贴访问该集群的 Bearer Token"></textarea>
</div>
</div>
</div>
@@ -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);
}
// 提交安装表单