将应用商店的用户实例保存到数据库
This commit is contained in:
133
models/appstore/user_app_instance.go
Normal file
133
models/appstore/user_app_instance.go
Normal file
@@ -0,0 +1,133 @@
|
||||
// Copyright 2024 The Devstar Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package appstore
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
// UserAppInstance 用户应用实例
|
||||
type UserAppInstance struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
UserID int64 `xorm:"INDEX NOT NULL"` // 用户ID
|
||||
AppID string `xorm:"INDEX NOT NULL"` // 应用模板ID
|
||||
InstanceName string `xorm:"NOT NULL"` // 实例名称(用户自定义)
|
||||
|
||||
// 用户配置
|
||||
UserConfig string `xorm:"TEXT"` // 用户配置JSON
|
||||
MergedApp string `xorm:"TEXT"` // 合并后的完整应用配置JSON
|
||||
|
||||
// 部署信息
|
||||
DeployType string `xorm:"NOT NULL"` // 实际部署类型 (kubernetes/docker)
|
||||
Kubeconfig string `xorm:"TEXT"` // Kubeconfig内容(加密存储)
|
||||
KubeconfigContext string // Kubeconfig context
|
||||
|
||||
// 元数据
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
|
||||
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
|
||||
}
|
||||
|
||||
// TableName 设置表名
|
||||
func (uai *UserAppInstance) TableName() string {
|
||||
return "user_app_instance"
|
||||
}
|
||||
|
||||
// BeforeInsert 插入前的钩子
|
||||
func (uai *UserAppInstance) BeforeInsert() {
|
||||
uai.CreatedUnix = timeutil.TimeStampNow()
|
||||
uai.UpdatedUnix = timeutil.TimeStampNow()
|
||||
}
|
||||
|
||||
// BeforeUpdate 更新前的钩子
|
||||
func (uai *UserAppInstance) BeforeUpdate() {
|
||||
uai.UpdatedUnix = timeutil.TimeStampNow()
|
||||
}
|
||||
|
||||
// GetUserAppInstances 按用户查询应用实例
|
||||
func GetUserAppInstances(ctx context.Context, userID int64) ([]*UserAppInstance, error) {
|
||||
return db.Find[UserAppInstance](ctx, FindUserAppInstanceOptions{
|
||||
UserID: userID,
|
||||
})
|
||||
}
|
||||
|
||||
// GetUserAppInstance 查询特定应用实例
|
||||
func GetUserAppInstance(ctx context.Context, userID, instanceID int64) (*UserAppInstance, error) {
|
||||
instance := new(UserAppInstance)
|
||||
has, err := db.GetEngine(ctx).Where("id = ? AND user_id = ?", instanceID, userID).Get(instance)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !has {
|
||||
return nil, nil
|
||||
}
|
||||
return instance, nil
|
||||
}
|
||||
|
||||
// GetUserAppInstanceByAppID 按应用ID查询用户的实例
|
||||
func GetUserAppInstanceByAppID(ctx context.Context, userID int64, appID string) (*UserAppInstance, error) {
|
||||
instance := new(UserAppInstance)
|
||||
has, err := db.GetEngine(ctx).Where("user_id = ? AND app_id = ?", userID, appID).Get(instance)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !has {
|
||||
return nil, nil
|
||||
}
|
||||
return instance, nil
|
||||
}
|
||||
|
||||
// CreateUserAppInstance 创建用户应用实例
|
||||
func CreateUserAppInstance(ctx context.Context, instance *UserAppInstance) error {
|
||||
return db.Insert(ctx, instance)
|
||||
}
|
||||
|
||||
// UpdateUserAppInstance 更新用户应用实例
|
||||
func UpdateUserAppInstance(ctx context.Context, instance *UserAppInstance) error {
|
||||
_, err := db.GetEngine(ctx).ID(instance.ID).Update(instance)
|
||||
return err
|
||||
}
|
||||
|
||||
// DeleteUserAppInstance 删除用户应用实例
|
||||
func DeleteUserAppInstance(ctx context.Context, userID, instanceID int64) error {
|
||||
_, err := db.GetEngine(ctx).Where("id = ? AND user_id = ?", instanceID, userID).Delete(&UserAppInstance{})
|
||||
return err
|
||||
}
|
||||
|
||||
// DeleteUserAppInstanceByAppID 按应用ID删除用户实例
|
||||
func DeleteUserAppInstanceByAppID(ctx context.Context, userID int64, appID string) error {
|
||||
_, err := db.GetEngine(ctx).Where("user_id = ? AND app_id = ?", userID, appID).Delete(&UserAppInstance{})
|
||||
return err
|
||||
}
|
||||
|
||||
// FindUserAppInstanceOptions 查询选项
|
||||
type FindUserAppInstanceOptions struct {
|
||||
db.ListOptions
|
||||
UserID int64 // 用户ID
|
||||
AppID string // 应用ID(可选)
|
||||
}
|
||||
|
||||
// ToConds 转换为查询条件
|
||||
func (opts FindUserAppInstanceOptions) ToConds() builder.Cond {
|
||||
conds := builder.NewCond()
|
||||
if opts.UserID != 0 {
|
||||
conds = conds.And(builder.Eq{"user_id": opts.UserID})
|
||||
}
|
||||
if opts.AppID != "" {
|
||||
conds = conds.And(builder.Eq{"app_id": opts.AppID})
|
||||
}
|
||||
return conds
|
||||
}
|
||||
|
||||
// ToOrders 转换为排序条件
|
||||
func (opts FindUserAppInstanceOptions) ToOrders() string {
|
||||
return "id DESC"
|
||||
}
|
||||
|
||||
func init() {
|
||||
db.RegisterModel(new(UserAppInstance))
|
||||
}
|
||||
23
models/migrations/devstar_v1_0/dv4.go
Normal file
23
models/migrations/devstar_v1_0/dv4.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package devstar_v1_0
|
||||
|
||||
// 构建 DevStar Studio v1.0 所需数据库类型
|
||||
// 从 dv3 到 dv4 - 添加用户应用实例表
|
||||
|
||||
import (
|
||||
appstore_model "code.gitea.io/gitea/models/appstore"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
// AddUserAppInstanceTable 创建用户应用实例表
|
||||
func AddUserAppInstanceTable(x *xorm.Engine) error {
|
||||
// 创建用户应用实例表
|
||||
err := x.Sync(new(appstore_model.UserAppInstance))
|
||||
if err != nil {
|
||||
return ErrMigrateDevstarDatabase{
|
||||
Step: "create table 'user_app_instance'",
|
||||
Message: err.Error(),
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -11,7 +11,8 @@ import (
|
||||
func AppStorePublicApps(ctx *context.APIContext) {
|
||||
// 将 APIContext 转换为 Context
|
||||
webCtx := &context.Context{Base: ctx.Base}
|
||||
manager := appstore.NewManager(webCtx)
|
||||
// 对于公开API,使用0作为用户ID
|
||||
manager := appstore.NewManager(webCtx, 0)
|
||||
apps, err := manager.ListApps()
|
||||
if err != nil {
|
||||
ctx.JSON(http.StatusInternalServerError, map[string]interface{}{"error": err.Error()})
|
||||
|
||||
@@ -55,7 +55,7 @@ func handleGetApps(ctx *context.Context) {
|
||||
deployment := ctx.FormString("deployment") // docker, kubernetes
|
||||
source := ctx.FormString("source") // local | devstar
|
||||
|
||||
manager := appstore.NewManager(ctx)
|
||||
manager := appstore.NewManager(ctx, ctx.Doer.ID)
|
||||
|
||||
var apps []appstore.App
|
||||
var err error
|
||||
@@ -215,7 +215,7 @@ func AppStoreStatus(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
manager := appstore.NewManager(ctx)
|
||||
manager := appstore.NewManager(ctx, ctx.Doer.ID)
|
||||
status, err := manager.GetAppStatus(appID)
|
||||
if err != nil {
|
||||
if appErr, ok := err.(*appstore.AppStoreError); ok {
|
||||
@@ -265,7 +265,7 @@ func AppStoreInstall(ctx *context.Context) {
|
||||
}
|
||||
|
||||
// 创建 manager 并执行安装
|
||||
manager := appstore.NewManager(ctx)
|
||||
manager := appstore.NewManager(ctx, ctx.Doer.ID)
|
||||
if err := manager.InstallApp(appID, configJSON, installTarget, kubeconfig, kubeconfigContext); err != nil {
|
||||
// 根据错误类型返回相应的状态码和消息
|
||||
if appErr, ok := err.(*appstore.AppStoreError); ok {
|
||||
@@ -332,7 +332,7 @@ func AppStoreUpdate(ctx *context.Context) {
|
||||
}
|
||||
|
||||
// 创建 manager 并执行更新
|
||||
manager := appstore.NewManager(ctx)
|
||||
manager := appstore.NewManager(ctx, ctx.Doer.ID)
|
||||
if err := manager.UpdateInstalledApp(appID, configJSON, installTarget, kubeconfig, kubeconfigContext); err != nil {
|
||||
// 根据错误类型返回相应的状态码和消息
|
||||
if appErr, ok := err.(*appstore.AppStoreError); ok {
|
||||
@@ -392,7 +392,7 @@ func AppStoreUninstall(ctx *context.Context) {
|
||||
}
|
||||
|
||||
// 创建 manager 并执行卸载
|
||||
manager := appstore.NewManager(ctx)
|
||||
manager := appstore.NewManager(ctx, ctx.Doer.ID)
|
||||
if err := manager.UninstallApp(appID, installTarget, kubeconfig, kubeconfigContext); err != nil {
|
||||
// 根据错误类型返回相应的状态码和消息
|
||||
if appErr, ok := err.(*appstore.AppStoreError); ok {
|
||||
@@ -416,7 +416,68 @@ func AppStoreUninstall(ctx *context.Context) {
|
||||
if installTarget == "kubeconfig" && kubeconfig != "" {
|
||||
ctx.Flash.Success("应用已成功从指定位置卸载")
|
||||
} else {
|
||||
ctx.Flash.Success("本地卸载功能开发中")
|
||||
ctx.Flash.Success("应用已成功从默认位置卸载")
|
||||
}
|
||||
|
||||
// 根据来源页面决定重定向位置
|
||||
referer := ctx.Req.Header.Get("Referer")
|
||||
if strings.Contains(referer, "/admin/appstore") {
|
||||
ctx.Redirect(setting.AppSubURL + "/admin/appstore")
|
||||
} else {
|
||||
ctx.Redirect(setting.AppSubURL + "/user/settings/appstore")
|
||||
}
|
||||
}
|
||||
|
||||
// AppStoreResume handles app resuming (restoring original replica count)
|
||||
func AppStoreResume(ctx *context.Context) {
|
||||
// 检查用户登录状态
|
||||
if !ctx.IsSigned {
|
||||
ctx.JSON(401, map[string]string{"error": "未登录"})
|
||||
return
|
||||
}
|
||||
|
||||
if ctx.Req.Method != "POST" {
|
||||
ctx.JSON(405, map[string]string{"error": "Method Not Allowed"})
|
||||
return
|
||||
}
|
||||
|
||||
// 解析表单数据
|
||||
appID := ctx.FormString("app_id")
|
||||
installTarget := ctx.FormString("install_target")
|
||||
kubeconfig := ctx.FormString("kubeconfig")
|
||||
kubeconfigContext := ctx.FormString("kubeconfig_context")
|
||||
|
||||
if appID == "" {
|
||||
ctx.JSON(400, map[string]string{"error": "应用ID不能为空"})
|
||||
return
|
||||
}
|
||||
|
||||
// 创建 manager 并执行恢复
|
||||
manager := appstore.NewManager(ctx, ctx.Doer.ID)
|
||||
if err := manager.ResumeApp(appID, installTarget, []byte(kubeconfig), kubeconfigContext); err != nil {
|
||||
// 根据错误类型返回相应的状态码和消息
|
||||
if appErr, ok := err.(*appstore.AppStoreError); ok {
|
||||
switch appErr.Code {
|
||||
case "APP_NOT_FOUND":
|
||||
ctx.JSON(400, map[string]string{"error": appErr.Message + ": " + appErr.Details})
|
||||
case "KUBERNETES_RESUME_ERROR":
|
||||
ctx.JSON(500, map[string]string{"error": appErr.Message + ": " + appErr.Details})
|
||||
case "UNSUPPORTED_DEPLOYMENT_TYPE":
|
||||
ctx.JSON(400, map[string]string{"error": appErr.Message})
|
||||
default:
|
||||
ctx.JSON(500, map[string]string{"error": appErr.Message})
|
||||
}
|
||||
} else {
|
||||
ctx.JSON(500, map[string]string{"error": "恢复失败: " + err.Error()})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 恢复成功
|
||||
if installTarget == "kubeconfig" && kubeconfig != "" {
|
||||
ctx.Flash.Success("应用已成功在指定位置恢复")
|
||||
} else {
|
||||
ctx.Flash.Success("应用已成功恢复")
|
||||
}
|
||||
|
||||
// 根据来源页面决定重定向位置
|
||||
@@ -453,7 +514,7 @@ func AppStoreStop(ctx *context.Context) {
|
||||
}
|
||||
|
||||
// 创建 manager 并执行暂停
|
||||
manager := appstore.NewManager(ctx)
|
||||
manager := appstore.NewManager(ctx, ctx.Doer.ID)
|
||||
if err := manager.StopApp(appID, installTarget, []byte(kubeconfig), kubeconfigContext); err != nil {
|
||||
// 根据错误类型返回相应的状态码和消息
|
||||
if appErr, ok := err.(*appstore.AppStoreError); ok {
|
||||
@@ -523,7 +584,7 @@ func AddAppAPI(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
manager := appstore.NewManager(ctx)
|
||||
manager := appstore.NewManager(ctx, ctx.Doer.ID)
|
||||
if ctx.Req.Method != "POST" {
|
||||
ctx.JSON(405, map[string]string{"error": "Method Not Allowed"})
|
||||
return
|
||||
@@ -540,3 +601,55 @@ func AddAppAPI(ctx *context.Context) {
|
||||
}
|
||||
ctx.JSON(200, map[string]string{"msg": "ok"})
|
||||
}
|
||||
|
||||
// AppStoreDelete handles app deletion from database
|
||||
func AppStoreDelete(ctx *context.Context) {
|
||||
// 检查用户登录状态
|
||||
if !ctx.IsSigned {
|
||||
ctx.JSON(401, map[string]string{"error": "未登录"})
|
||||
return
|
||||
}
|
||||
|
||||
if ctx.Req.Method != "POST" {
|
||||
ctx.JSON(405, map[string]string{"error": "Method Not Allowed"})
|
||||
return
|
||||
}
|
||||
|
||||
// 解析表单数据
|
||||
appID := ctx.FormString("app_id")
|
||||
|
||||
if appID == "" {
|
||||
ctx.JSON(400, map[string]string{"error": "应用ID不能为空"})
|
||||
return
|
||||
}
|
||||
|
||||
// 创建 manager 并执行删除
|
||||
manager := appstore.NewManager(ctx, ctx.Doer.ID)
|
||||
if err := manager.RemoveApp(appID); err != nil {
|
||||
// 根据错误类型返回相应的状态码和消息
|
||||
if appErr, ok := err.(*appstore.AppStoreError); ok {
|
||||
switch appErr.Code {
|
||||
case "APP_NOT_FOUND":
|
||||
ctx.JSON(400, map[string]string{"error": appErr.Message + ": " + appErr.Details})
|
||||
case "DATABASE_ERROR":
|
||||
ctx.JSON(500, map[string]string{"error": appErr.Message + ": " + appErr.Details})
|
||||
default:
|
||||
ctx.JSON(500, map[string]string{"error": appErr.Message})
|
||||
}
|
||||
} else {
|
||||
ctx.JSON(500, map[string]string{"error": "删除失败: " + err.Error()})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 删除成功
|
||||
ctx.Flash.Success("应用已成功删除")
|
||||
|
||||
// 根据来源页面决定重定向位置
|
||||
referer := ctx.Req.Header.Get("Referer")
|
||||
if strings.Contains(referer, "/admin/appstore") {
|
||||
ctx.Redirect(setting.AppSubURL + "/admin/appstore")
|
||||
} else {
|
||||
ctx.Redirect(setting.AppSubURL + "/user/settings/appstore")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -711,6 +711,7 @@ func registerWebRoutes(m *web.Router) {
|
||||
m.Post("/install/{app_id}", user_setting.AppStoreInstall)
|
||||
m.Post("/update/{app_id}", user_setting.AppStoreUpdate)
|
||||
m.Post("/stop/{app_id}", user_setting.AppStoreStop)
|
||||
m.Post("/resume/{app_id}", user_setting.AppStoreResume)
|
||||
m.Post("/uninstall/{app_id}", user_setting.AppStoreUninstall)
|
||||
m.Get("/configure/{app_id}", user_setting.AppStoreConfigure)
|
||||
m.Post("/configure/{app_id}", user_setting.AppStoreConfigurePost)
|
||||
|
||||
@@ -103,7 +103,7 @@ func BuildK8sCreateOptions(app *App) (*application.CreateApplicationOptions, err
|
||||
|
||||
// Replicas
|
||||
var replicasPtr *int32
|
||||
if k.Replicas > 0 {
|
||||
if k.Replicas >= 0 {
|
||||
r := int32(k.Replicas)
|
||||
replicasPtr = &r
|
||||
}
|
||||
@@ -261,7 +261,7 @@ func BuildK8sUpdateOptions(app *App, namespaceOverride string, existing *applica
|
||||
}
|
||||
|
||||
var replicasPtr *int32
|
||||
if k.Replicas > 0 {
|
||||
if k.Replicas >= 0 {
|
||||
r := int32(k.Replicas)
|
||||
replicasPtr = &r
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
k8s "code.gitea.io/gitea/modules/k8s"
|
||||
applicationv1 "code.gitea.io/gitea/modules/k8s/api/application/v1"
|
||||
"code.gitea.io/gitea/modules/k8s/application"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
@@ -237,10 +238,11 @@ func (km *K8sManager) UpdateAppInKubernetes(app *App, kubeconfig []byte, context
|
||||
// 先获取现有的应用,以保留元数据(特别是 resourceVersion)
|
||||
existingApp, err := km.GetAppFromKubernetes(app, kubeconfig, contextName)
|
||||
if err != nil {
|
||||
// 如果应用不存在,返回错误
|
||||
// 如果应用不存在,尝试创建新应用
|
||||
log.Warn("Application not found in Kubernetes, attempting to create new application: %v", err)
|
||||
return &AppStoreError{
|
||||
Code: "APP_NOT_FOUND",
|
||||
Message: "应用在 Kubernetes 中不存在,无法更新",
|
||||
Message: "应用在 Kubernetes 中不存在,请先安装应用",
|
||||
Details: err.Error(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"time"
|
||||
|
||||
appstore_model "code.gitea.io/gitea/models/appstore"
|
||||
user_app_instance "code.gitea.io/gitea/models/appstore"
|
||||
applicationv1 "code.gitea.io/gitea/modules/k8s/api/application/v1"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
gitea_context "code.gitea.io/gitea/services/context"
|
||||
@@ -22,14 +23,16 @@ type Manager struct {
|
||||
parser *Parser
|
||||
ctx context.Context
|
||||
k8s *K8sManager
|
||||
userID int64 // 当前用户ID
|
||||
}
|
||||
|
||||
// NewManager creates a new app store manager for database operations
|
||||
func NewManager(ctx *gitea_context.Context) *Manager {
|
||||
func NewManager(ctx *gitea_context.Context, userID int64) *Manager {
|
||||
return &Manager{
|
||||
parser: NewParser(),
|
||||
ctx: *ctx,
|
||||
k8s: NewK8sManager(*ctx),
|
||||
userID: userID,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,6 +57,32 @@ func (m *Manager) ListApps() ([]App, error) {
|
||||
return apps, nil
|
||||
}
|
||||
|
||||
// ListUserAppInstances returns user's installed application instances
|
||||
func (m *Manager) ListUserAppInstances() ([]App, error) {
|
||||
// 获取用户的应用实例
|
||||
instances, err := user_app_instance.GetUserAppInstances(m.ctx, m.userID)
|
||||
if err != nil {
|
||||
return nil, &AppStoreError{
|
||||
Code: "DATABASE_ERROR",
|
||||
Message: "获取用户应用实例失败",
|
||||
Details: err.Error(),
|
||||
}
|
||||
}
|
||||
|
||||
var apps []App
|
||||
for _, instance := range instances {
|
||||
// 从实例的 MergedApp JSON 中解析应用信息
|
||||
var app App
|
||||
if err := json.Unmarshal([]byte(instance.MergedApp), &app); err != nil {
|
||||
log.Error("Failed to unmarshal merged app for instance %d: %v", instance.ID, err)
|
||||
continue
|
||||
}
|
||||
apps = append(apps, app)
|
||||
}
|
||||
|
||||
return apps, nil
|
||||
}
|
||||
|
||||
// ListAppsFromDevstar 从 devstar.cn 拉取应用列表
|
||||
func (m *Manager) ListAppsFromDevstar() ([]App, error) {
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
@@ -350,27 +379,18 @@ func (m *Manager) UpdateApp(app *App) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveApp removes an application from the database
|
||||
// RemoveApp removes a user's application instance from the database
|
||||
func (m *Manager) RemoveApp(appID string) error {
|
||||
// Get existing app from database
|
||||
existingAppStore, err := appstore_model.GetAppStoreByAppID(m.ctx, appID)
|
||||
if err != nil {
|
||||
return &AppStoreError{
|
||||
Code: "APP_NOT_FOUND",
|
||||
Message: "App not found in database",
|
||||
Details: err.Error(),
|
||||
}
|
||||
}
|
||||
|
||||
// Delete from database
|
||||
if err := appstore_model.DeleteAppStore(m.ctx, existingAppStore.ID); err != nil {
|
||||
// 删除用户的应用实例
|
||||
if err := user_app_instance.DeleteUserAppInstanceByAppID(m.ctx, m.userID, appID); err != nil {
|
||||
return &AppStoreError{
|
||||
Code: "DATABASE_ERROR",
|
||||
Message: "Failed to delete app from database",
|
||||
Message: "删除应用实例失败",
|
||||
Details: err.Error(),
|
||||
}
|
||||
}
|
||||
|
||||
log.Info("Successfully removed app instance %s for user %d", appID, m.userID)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -427,6 +447,17 @@ 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" {
|
||||
deployType = "kubernetes"
|
||||
} else if mergedApp.DeploymentType == "docker" {
|
||||
deployType = "docker"
|
||||
}
|
||||
}
|
||||
|
||||
// 根据安装目标和应用实际部署类型决定安装方式
|
||||
if installTarget == "kubeconfig" && kubeconfig != "" {
|
||||
// 安装到外部 Kubernetes 集群
|
||||
@@ -440,7 +471,7 @@ func (m *Manager) InstallApp(appID string, configJSON string, installTarget stri
|
||||
} else {
|
||||
// 根据应用实际部署类型决定本地安装方式
|
||||
log.Info("InstallApp: mergedApp.Deploy.Type = %s", mergedApp.Deploy.Type)
|
||||
switch mergedApp.Deploy.Type {
|
||||
switch deployType {
|
||||
case "kubernetes":
|
||||
// 应用要部署到 Kubernetes,安装到本地 K8s 集群
|
||||
if err := m.InstallAppToKubernetes(mergedApp, nil, ""); err != nil {
|
||||
@@ -467,13 +498,61 @@ func (m *Manager) InstallApp(appID string, configJSON string, installTarget stri
|
||||
}
|
||||
}
|
||||
|
||||
// 安装成功后,保存用户应用实例到数据库
|
||||
mergedAppJSON, err := json.Marshal(mergedApp)
|
||||
if err != nil {
|
||||
log.Error("Failed to marshal merged app: %v", err)
|
||||
return &AppStoreError{
|
||||
Code: "JSON_MARSHAL_ERROR",
|
||||
Message: "保存应用配置失败",
|
||||
Details: err.Error(),
|
||||
}
|
||||
}
|
||||
|
||||
instance := &user_app_instance.UserAppInstance{
|
||||
UserID: m.userID,
|
||||
AppID: appID,
|
||||
InstanceName: mergedApp.Name, // 使用应用名称作为实例名称
|
||||
UserConfig: configJSON,
|
||||
MergedApp: string(mergedAppJSON),
|
||||
DeployType: deployType,
|
||||
Kubeconfig: kubeconfig,
|
||||
KubeconfigContext: kubeconfigContext,
|
||||
}
|
||||
|
||||
if err := user_app_instance.CreateUserAppInstance(m.ctx, instance); err != nil {
|
||||
log.Error("Failed to create user app instance: %v", err)
|
||||
return &AppStoreError{
|
||||
Code: "DATABASE_ERROR",
|
||||
Message: "保存应用实例失败",
|
||||
Details: err.Error(),
|
||||
}
|
||||
}
|
||||
|
||||
log.Info("Successfully installed app %s for user %d", appID, m.userID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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 {
|
||||
// 获取应用信息
|
||||
// 获取用户的应用实例
|
||||
instance, err := user_app_instance.GetUserAppInstanceByAppID(m.ctx, m.userID, appID)
|
||||
if err != nil {
|
||||
return &AppStoreError{
|
||||
Code: "DATABASE_ERROR",
|
||||
Message: "获取应用实例失败",
|
||||
Details: err.Error(),
|
||||
}
|
||||
}
|
||||
if instance == nil {
|
||||
return &AppStoreError{
|
||||
Code: "APP_NOT_INSTALLED",
|
||||
Message: "应用未安装",
|
||||
}
|
||||
}
|
||||
|
||||
// 获取应用模板信息
|
||||
app, err := m.GetApp(appID)
|
||||
if err != nil {
|
||||
return &AppStoreError{
|
||||
@@ -493,52 +572,87 @@ func (m *Manager) UpdateInstalledApp(appID string, configJSON string, installTar
|
||||
}
|
||||
}
|
||||
|
||||
// 确定部署类型
|
||||
deployType := mergedApp.Deploy.Type
|
||||
if deployType == "" {
|
||||
deployType = instance.DeployType // 使用实例中保存的部署类型
|
||||
}
|
||||
|
||||
// 根据安装目标和应用实际部署类型决定更新方式
|
||||
if installTarget == "kubeconfig" && kubeconfig != "" {
|
||||
// 更新外部 Kubernetes 集群中的应用实例
|
||||
switch mergedApp.Deploy.Type {
|
||||
case "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()}
|
||||
}
|
||||
return nil
|
||||
case "docker":
|
||||
} else {
|
||||
return &AppStoreError{Code: "NOT_IMPLEMENTED", Message: "外部环境 Docker 更新功能开发中"}
|
||||
default:
|
||||
return &AppStoreError{Code: "NOT_IMPLEMENTED", Message: "外部环境更新功能开发中"}
|
||||
}
|
||||
}
|
||||
|
||||
// 本地更新(依据应用部署类型)
|
||||
switch mergedApp.Deploy.Type {
|
||||
case "kubernetes":
|
||||
if err := m.UpdateAppInKubernetes(mergedApp, nil, ""); err != nil {
|
||||
return &AppStoreError{Code: "KUBERNETES_UPDATE_ERROR", Message: "本地 Kubernetes 更新失败", Details: err.Error()}
|
||||
}
|
||||
return nil
|
||||
case "docker":
|
||||
// TODO: 实现 Docker 更新逻辑
|
||||
return &AppStoreError{Code: "NOT_IMPLEMENTED", Message: "本地 Docker 更新功能开发中"}
|
||||
default:
|
||||
// 未知部署类型,优先尝试 K8s(若 deployment_type 支持)
|
||||
if mergedApp.DeploymentType == "kubernetes" || mergedApp.DeploymentType == "both" {
|
||||
} else {
|
||||
// 本地更新(依据应用部署类型)
|
||||
if deployType == "kubernetes" {
|
||||
if err := m.UpdateAppInKubernetes(mergedApp, nil, ""); err != nil {
|
||||
return &AppStoreError{Code: "KUBERNETES_UPDATE_ERROR", Message: "本地 Kubernetes 更新失败", Details: err.Error()}
|
||||
}
|
||||
return nil
|
||||
} else {
|
||||
return &AppStoreError{Code: "NOT_IMPLEMENTED", Message: "本地 Docker 更新功能开发中"}
|
||||
}
|
||||
return &AppStoreError{Code: "NOT_IMPLEMENTED", Message: "本地更新功能开发中"}
|
||||
}
|
||||
|
||||
// 更新成功后,更新用户应用实例
|
||||
mergedAppJSON, err := json.Marshal(mergedApp)
|
||||
if err != nil {
|
||||
log.Error("Failed to marshal merged app: %v", err)
|
||||
return &AppStoreError{
|
||||
Code: "JSON_MARSHAL_ERROR",
|
||||
Message: "保存应用配置失败",
|
||||
Details: err.Error(),
|
||||
}
|
||||
}
|
||||
|
||||
instance.UserConfig = configJSON
|
||||
instance.MergedApp = string(mergedAppJSON)
|
||||
instance.DeployType = deployType
|
||||
instance.Kubeconfig = kubeconfig
|
||||
instance.KubeconfigContext = kubeconfigContext
|
||||
|
||||
if err := user_app_instance.UpdateUserAppInstance(m.ctx, instance); err != nil {
|
||||
log.Error("Failed to update user app instance: %v", err)
|
||||
return &AppStoreError{
|
||||
Code: "DATABASE_ERROR",
|
||||
Message: "更新应用实例失败",
|
||||
Details: err.Error(),
|
||||
}
|
||||
}
|
||||
|
||||
log.Info("Successfully updated app %s for user %d", appID, m.userID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// UninstallApp uninstalls an application based on the provided parameters
|
||||
func (m *Manager) UninstallApp(appID string, installTarget string, kubeconfig string, kubeconfigContext string) error {
|
||||
// 获取应用信息
|
||||
app, err := m.GetApp(appID)
|
||||
// 获取用户的应用实例
|
||||
instance, err := user_app_instance.GetUserAppInstanceByAppID(m.ctx, m.userID, appID)
|
||||
if err != nil {
|
||||
return &AppStoreError{
|
||||
Code: "APP_NOT_FOUND",
|
||||
Message: "获取应用失败",
|
||||
Code: "DATABASE_ERROR",
|
||||
Message: "获取应用实例失败",
|
||||
Details: err.Error(),
|
||||
}
|
||||
}
|
||||
if instance == nil {
|
||||
return &AppStoreError{
|
||||
Code: "APP_NOT_INSTALLED",
|
||||
Message: "应用未安装",
|
||||
}
|
||||
}
|
||||
|
||||
// 从实例中解析应用信息
|
||||
var app App
|
||||
if err := json.Unmarshal([]byte(instance.MergedApp), &app); err != nil {
|
||||
return &AppStoreError{
|
||||
Code: "JSON_UNMARSHAL_ERROR",
|
||||
Message: "解析应用配置失败",
|
||||
Details: err.Error(),
|
||||
}
|
||||
}
|
||||
@@ -546,60 +660,47 @@ func (m *Manager) UninstallApp(appID string, installTarget string, kubeconfig st
|
||||
// 根据安装目标和应用实际部署类型决定卸载方式
|
||||
if installTarget == "kubeconfig" && kubeconfig != "" {
|
||||
// 从外部 Kubernetes 集群卸载
|
||||
if err := m.UninstallAppFromKubernetes(app, []byte(kubeconfig), kubeconfigContext); err != nil {
|
||||
if instance.DeployType == "kubernetes" {
|
||||
if err := m.UninstallAppFromKubernetes(&app, []byte(kubeconfig), kubeconfigContext); err != nil {
|
||||
return &AppStoreError{
|
||||
Code: "KUBERNETES_UNINSTALL_ERROR",
|
||||
Message: "Kubernetes 卸载失败",
|
||||
Details: err.Error(),
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return &AppStoreError{
|
||||
Code: "KUBERNETES_UNINSTALL_ERROR",
|
||||
Message: "Kubernetes 卸载失败",
|
||||
Details: err.Error(),
|
||||
Code: "NOT_IMPLEMENTED",
|
||||
Message: "外部环境 Docker 卸载功能开发中",
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 优先检查应用实际部署类型
|
||||
if app.Deploy.Type == "kubernetes" {
|
||||
// 应用实际部署在 Kubernetes,从本地 K8s 集群卸载
|
||||
if err := m.UninstallAppFromKubernetes(app, nil, ""); err != nil {
|
||||
// 本地卸载(依据实例中保存的部署类型)
|
||||
if instance.DeployType == "kubernetes" {
|
||||
if err := m.UninstallAppFromKubernetes(&app, nil, ""); err != nil {
|
||||
return &AppStoreError{
|
||||
Code: "KUBERNETES_UNINSTALL_ERROR",
|
||||
Message: "本地 Kubernetes 卸载失败",
|
||||
Details: err.Error(),
|
||||
}
|
||||
}
|
||||
return nil
|
||||
} else if app.Deploy.Type == "docker" {
|
||||
// 应用实际部署在 Docker,从本地 Docker 卸载
|
||||
// TODO: 实现 Docker 卸载逻辑
|
||||
} else {
|
||||
return &AppStoreError{
|
||||
Code: "NOT_IMPLEMENTED",
|
||||
Message: "本地 Docker 卸载功能开发中",
|
||||
}
|
||||
} else if app.Deploy.Type == "" {
|
||||
// 如果 Deploy.Type 为空,根据应用支持的部署类型尝试卸载
|
||||
if app.DeploymentType == "kubernetes" || app.DeploymentType == "both" {
|
||||
// 尝试从 Kubernetes 卸载
|
||||
if err := m.UninstallAppFromKubernetes(app, nil, ""); err != nil {
|
||||
return &AppStoreError{
|
||||
Code: "KUBERNETES_UNINSTALL_ERROR",
|
||||
Message: "本地 Kubernetes 卸载失败",
|
||||
Details: err.Error(),
|
||||
}
|
||||
}
|
||||
return nil
|
||||
} else if app.DeploymentType == "docker" {
|
||||
// TODO: 实现 Docker 卸载逻辑
|
||||
return &AppStoreError{
|
||||
Code: "NOT_IMPLEMENTED",
|
||||
Message: "本地 Docker 卸载功能开发中",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果以上都不匹配,返回未知部署类型错误
|
||||
return &AppStoreError{
|
||||
Code: "UNKNOWN_DEPLOYMENT_TYPE",
|
||||
Message: "未知的部署类型,无法确定卸载方式",
|
||||
}
|
||||
}
|
||||
|
||||
// 卸载成功后,删除用户应用实例
|
||||
if err := user_app_instance.DeleteUserAppInstanceByAppID(m.ctx, m.userID, appID); err != nil {
|
||||
log.Error("Failed to delete user app instance: %v", err)
|
||||
// 注意:这里不返回错误,因为 Kubernetes 资源已经成功卸载
|
||||
// 数据库记录删除失败不应该影响卸载操作的成功
|
||||
log.Warn("Kubernetes resources uninstalled successfully, but failed to delete database record for app %s, user %d", appID, m.userID)
|
||||
}
|
||||
|
||||
log.Info("Successfully uninstalled app %s for user %d", appID, m.userID)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -635,24 +736,51 @@ func (m *Manager) IsInstalledAppReady(status *applicationv1.ApplicationStatus) b
|
||||
|
||||
// GetAppStatus 获取应用的安装和运行状态
|
||||
func (m *Manager) GetAppStatus(appID string) (map[string]interface{}, error) {
|
||||
app, err := m.GetApp(appID)
|
||||
// 获取用户的应用实例
|
||||
instance, err := user_app_instance.GetUserAppInstanceByAppID(m.ctx, m.userID, appID)
|
||||
if err != nil {
|
||||
return nil, &AppStoreError{
|
||||
Code: "APP_NOT_FOUND",
|
||||
Message: "应用不存在",
|
||||
Code: "DATABASE_ERROR",
|
||||
Message: "获取应用实例失败",
|
||||
Details: err.Error(),
|
||||
}
|
||||
}
|
||||
if instance == nil {
|
||||
// 应用未安装
|
||||
return map[string]interface{}{
|
||||
"phase": "Not Installed",
|
||||
"replicas": 0,
|
||||
"readyReplicas": 0,
|
||||
"ready": false,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 从实例中解析应用信息
|
||||
var app App
|
||||
if err := json.Unmarshal([]byte(instance.MergedApp), &app); err != nil {
|
||||
return nil, &AppStoreError{
|
||||
Code: "JSON_UNMARSHAL_ERROR",
|
||||
Message: "解析应用配置失败",
|
||||
Details: err.Error(),
|
||||
}
|
||||
}
|
||||
|
||||
// 检查应用是否已安装
|
||||
// 优先检查实际部署类型,如果为空则检查支持的部署类型
|
||||
if app.Deploy.Type == "kubernetes" || (app.Deploy.Type == "" && (app.DeploymentType == "kubernetes" || app.DeploymentType == "both")) {
|
||||
// 检查应用是否支持 Kubernetes 部署
|
||||
if instance.DeployType == "kubernetes" {
|
||||
// 尝试从 Kubernetes 获取状态
|
||||
k8sApp, err := m.GetAppFromKubernetes(app, nil, "")
|
||||
var kubeconfig []byte
|
||||
var contextName string
|
||||
if instance.Kubeconfig != "" {
|
||||
kubeconfig = []byte(instance.Kubeconfig)
|
||||
contextName = instance.KubeconfigContext
|
||||
}
|
||||
|
||||
k8sApp, err := m.GetAppFromKubernetes(&app, kubeconfig, contextName)
|
||||
if err != nil {
|
||||
// 应用未安装或获取状态失败
|
||||
// 如果应用实例存在于数据库中,但 Kubernetes 中找不到,可能是被暂停了
|
||||
// 返回暂停状态而不是未安装状态
|
||||
return map[string]interface{}{
|
||||
"phase": "Not Installed",
|
||||
"phase": "Paused",
|
||||
"replicas": 0,
|
||||
"readyReplicas": 0,
|
||||
"ready": false,
|
||||
@@ -698,20 +826,36 @@ 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 {
|
||||
// 获取应用信息
|
||||
app, err := m.GetApp(appID)
|
||||
// 获取用户的应用实例
|
||||
instance, err := user_app_instance.GetUserAppInstanceByAppID(m.ctx, m.userID, appID)
|
||||
if err != nil {
|
||||
return &AppStoreError{
|
||||
Code: "APP_NOT_FOUND",
|
||||
Message: "获取应用失败",
|
||||
Code: "DATABASE_ERROR",
|
||||
Message: "获取应用实例失败",
|
||||
Details: err.Error(),
|
||||
}
|
||||
}
|
||||
if instance == nil {
|
||||
return &AppStoreError{
|
||||
Code: "APP_NOT_INSTALLED",
|
||||
Message: "应用未安装",
|
||||
}
|
||||
}
|
||||
|
||||
// 从实例中解析应用信息
|
||||
var app App
|
||||
if err := json.Unmarshal([]byte(instance.MergedApp), &app); err != nil {
|
||||
return &AppStoreError{
|
||||
Code: "JSON_UNMARSHAL_ERROR",
|
||||
Message: "解析应用配置失败",
|
||||
Details: err.Error(),
|
||||
}
|
||||
}
|
||||
|
||||
// 检查应用是否支持 Kubernetes 部署
|
||||
if app.Deploy.Type == "kubernetes" || (app.Deploy.Type == "" && (app.DeploymentType == "kubernetes" || app.DeploymentType == "both")) {
|
||||
if instance.DeployType == "kubernetes" {
|
||||
// 创建应用的副本并设置副本数为 0
|
||||
stoppedApp := *app
|
||||
stoppedApp := app
|
||||
if stoppedApp.Deploy.Kubernetes != nil {
|
||||
stoppedApp.Deploy.Kubernetes.Replicas = 0
|
||||
}
|
||||
@@ -730,6 +874,63 @@ func (m *Manager) StopApp(appID string, installTarget string, kubeconfig []byte,
|
||||
// 如果应用不支持 Kubernetes 部署,返回错误
|
||||
return &AppStoreError{
|
||||
Code: "UNSUPPORTED_DEPLOYMENT_TYPE",
|
||||
Message: "应用不支持 Kubernetes 部署,无法停止",
|
||||
Message: fmt.Sprintf("应用部署类型 '%s' 不支持暂停功能,目前仅支持 Kubernetes 部署的应用", instance.DeployType),
|
||||
}
|
||||
}
|
||||
|
||||
// ResumeApp resumes an application by restoring its original replica count
|
||||
func (m *Manager) ResumeApp(appID string, installTarget string, kubeconfig []byte, kubeconfigContext string) error {
|
||||
// 获取用户的应用实例
|
||||
instance, err := user_app_instance.GetUserAppInstanceByAppID(m.ctx, m.userID, appID)
|
||||
if err != nil {
|
||||
return &AppStoreError{
|
||||
Code: "DATABASE_ERROR",
|
||||
Message: "获取应用实例失败",
|
||||
Details: err.Error(),
|
||||
}
|
||||
}
|
||||
if instance == nil {
|
||||
return &AppStoreError{
|
||||
Code: "APP_NOT_INSTALLED",
|
||||
Message: "应用未安装",
|
||||
}
|
||||
}
|
||||
|
||||
// 从实例中解析应用信息
|
||||
var app App
|
||||
if err := json.Unmarshal([]byte(instance.MergedApp), &app); err != nil {
|
||||
return &AppStoreError{
|
||||
Code: "JSON_UNMARSHAL_ERROR",
|
||||
Message: "解析应用配置失败",
|
||||
Details: err.Error(),
|
||||
}
|
||||
}
|
||||
|
||||
// 检查应用是否支持 Kubernetes 部署
|
||||
if instance.DeployType == "kubernetes" {
|
||||
// 恢复应用的原始副本数(从 MergedApp 中获取)
|
||||
resumedApp := app
|
||||
if resumedApp.Deploy.Kubernetes != nil {
|
||||
// 如果 MergedApp 中没有指定副本数,默认恢复为 1
|
||||
if resumedApp.Deploy.Kubernetes.Replicas == 0 {
|
||||
resumedApp.Deploy.Kubernetes.Replicas = 1
|
||||
}
|
||||
}
|
||||
|
||||
// 调用 k8s 层的 UpdateAppInKubernetes 函数来恢复应用
|
||||
if err := m.k8s.UpdateAppInKubernetes(&resumedApp, kubeconfig, kubeconfigContext); err != nil {
|
||||
return &AppStoreError{
|
||||
Code: "KUBERNETES_RESUME_ERROR",
|
||||
Message: "恢复应用失败",
|
||||
Details: err.Error(),
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 如果应用不支持 Kubernetes 部署,返回错误
|
||||
return &AppStoreError{
|
||||
Code: "UNSUPPORTED_DEPLOYMENT_TYPE",
|
||||
Message: fmt.Sprintf("应用部署类型 '%s' 不支持恢复功能,目前仅支持 Kubernetes 部署的应用", instance.DeployType),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,6 +128,25 @@
|
||||
font-size: 1em !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* 修复应用详情页面中网站和仓库按钮的文字对齐问题 */
|
||||
#app-details-modal .ui.buttons .ui.button {
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
height: 2.5em !important;
|
||||
line-height: 1 !important;
|
||||
}
|
||||
|
||||
#app-details-modal .ui.buttons .ui.button i.icon {
|
||||
margin-right: 0.5em !important;
|
||||
line-height: 1 !important;
|
||||
}
|
||||
|
||||
#app-details-modal .ui.buttons .ui.button:not(.icon) {
|
||||
padding-left: 1em !important;
|
||||
padding-right: 1em !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="ui stackable grid">
|
||||
@@ -331,7 +350,7 @@
|
||||
<div class="actions">
|
||||
<div class="ui button" onclick="closeDetailsModal()">{{ctx.Locale.Tr "cancel"}}</div>
|
||||
<div class="ui primary button" onclick="updateInstalledAppFromDetails()">更新应用</div>
|
||||
<div class="ui orange button" onclick="stopCurrentAppFromDetails()">暂停应用</div>
|
||||
<div class="ui orange button" id="pause-resume-btn" onclick="togglePauseResumeFromDetails()">暂停应用</div>
|
||||
<div class="ui red button" onclick="uninstallCurrentAppFromDetails()">卸载应用</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1046,8 +1065,8 @@ async function checkAppInstallStatus(app) {
|
||||
const replicas = data.replicas ?? data.status?.replicas ?? 0;
|
||||
const readyReplicas = data.readyReplicas ?? data.status?.readyReplicas ?? 0;
|
||||
|
||||
// 判断是否已安装且就绪
|
||||
const isInstalled = phase !== 'Not Installed' && phase !== 'Unknown' && (replicas === 0 || readyReplicas > 0);
|
||||
// 判断是否已安装(包括暂停状态)
|
||||
const isInstalled = phase !== 'Not Installed' && phase !== 'Unknown';
|
||||
|
||||
if (isInstalled) {
|
||||
installBtn.className = 'ui green compact button disabled';
|
||||
@@ -1087,41 +1106,54 @@ function uninstallCurrentAppFromDetails() {
|
||||
uninstallForm.submit();
|
||||
}
|
||||
|
||||
// 暂停应用(从详情弹窗触发)
|
||||
function stopCurrentAppFromDetails() {
|
||||
// 暂停/恢复应用(从详情弹窗触发)
|
||||
function togglePauseResumeFromDetails() {
|
||||
if (!currentApp) return;
|
||||
const appId = currentApp.id || currentApp.app_id;
|
||||
|
||||
// 根据当前状态决定操作
|
||||
const isRunning = currentAppStatus && currentAppStatus.replicas > 0;
|
||||
const action = isRunning ? 'stop' : 'resume';
|
||||
const actionText = isRunning ? '暂停' : '恢复';
|
||||
const confirmText = isRunning ?
|
||||
'确定要暂停应用吗?暂停后应用将停止运行,但数据会保留。' :
|
||||
'确定要恢复应用吗?应用将重新开始运行。';
|
||||
|
||||
// 显示确认对话框
|
||||
if (!confirm('确定要暂停应用吗?暂停后应用将停止运行,但数据会保留。')) {
|
||||
if (!confirm(confirmText)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建暂停应用的请求
|
||||
const stopForm = document.createElement('form');
|
||||
stopForm.method = 'POST';
|
||||
stopForm.action = `/user/settings/appstore/stop/${appId}`;
|
||||
// 创建暂停/恢复应用的请求
|
||||
const form = document.createElement('form');
|
||||
form.method = 'POST';
|
||||
form.action = `/user/settings/appstore/${action}/${appId}`;
|
||||
|
||||
const csrfInput = document.createElement('input');
|
||||
csrfInput.type = 'hidden';
|
||||
csrfInput.name = '_csrf';
|
||||
csrfInput.value = document.querySelector('meta[name="_csrf"]')?.content || '';
|
||||
stopForm.appendChild(csrfInput);
|
||||
form.appendChild(csrfInput);
|
||||
|
||||
const appIdInput = document.createElement('input');
|
||||
appIdInput.type = 'hidden';
|
||||
appIdInput.name = 'app_id';
|
||||
appIdInput.value = appId;
|
||||
stopForm.appendChild(appIdInput);
|
||||
form.appendChild(appIdInput);
|
||||
|
||||
const redirectInput = document.createElement('input');
|
||||
redirectInput.type = 'hidden';
|
||||
redirectInput.name = 'redirect_to';
|
||||
redirectInput.value = window.location.pathname;
|
||||
stopForm.appendChild(redirectInput);
|
||||
form.appendChild(redirectInput);
|
||||
|
||||
document.body.appendChild(stopForm);
|
||||
stopForm.submit();
|
||||
document.body.appendChild(form);
|
||||
form.submit();
|
||||
}
|
||||
|
||||
// 暂停应用(从详情弹窗触发)- 保留向后兼容
|
||||
function stopCurrentAppFromDetails() {
|
||||
togglePauseResumeFromDetails();
|
||||
}
|
||||
|
||||
// 加载并渲染运行状态(基于 K8s Application CRD)
|
||||
@@ -1169,13 +1201,13 @@ async function loadAndRenderAppStatus(app) {
|
||||
// 根据应用状态动态控制操作按钮的显示
|
||||
function updateActionButtonsVisibility() {
|
||||
const updateBtn = document.querySelector('#app-details-modal .ui.primary.button[onclick="updateInstalledAppFromDetails()"]');
|
||||
const stopBtn = document.querySelector('#app-details-modal .ui.orange.button[onclick="stopCurrentAppFromDetails()"]');
|
||||
const pauseResumeBtn = document.getElementById('pause-resume-btn');
|
||||
const uninstallBtn = document.querySelector('#app-details-modal .ui.red.button[onclick="uninstallCurrentAppFromDetails()"]');
|
||||
|
||||
if (!currentAppStatus) {
|
||||
// 状态未知,隐藏所有操作按钮
|
||||
if (updateBtn) updateBtn.style.display = 'none';
|
||||
if (stopBtn) stopBtn.style.display = 'none';
|
||||
if (pauseResumeBtn) pauseResumeBtn.style.display = 'none';
|
||||
if (uninstallBtn) uninstallBtn.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
@@ -1189,9 +1221,21 @@ function updateActionButtonsVisibility() {
|
||||
updateBtn.style.display = isInstalled ? 'inline-block' : 'none';
|
||||
}
|
||||
|
||||
// 暂停按钮:只有正在运行的应用才能暂停
|
||||
if (stopBtn) {
|
||||
stopBtn.style.display = isRunning ? 'inline-block' : 'none';
|
||||
// 暂停/恢复按钮:已安装的应用可以暂停或恢复
|
||||
if (pauseResumeBtn) {
|
||||
if (isInstalled) {
|
||||
pauseResumeBtn.style.display = 'inline-block';
|
||||
// 根据应用状态更新按钮文本和样式
|
||||
if (isRunning) {
|
||||
pauseResumeBtn.textContent = '暂停应用';
|
||||
pauseResumeBtn.className = 'ui orange button';
|
||||
} else if (isStopped) {
|
||||
pauseResumeBtn.textContent = '恢复应用';
|
||||
pauseResumeBtn.className = 'ui green button';
|
||||
}
|
||||
} else {
|
||||
pauseResumeBtn.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// 卸载按钮:只有已安装的应用才能卸载
|
||||
@@ -1295,49 +1339,9 @@ async function installApp() {
|
||||
installBtn.disabled = true;
|
||||
}
|
||||
|
||||
// 异步提交,成功后更新按钮状态
|
||||
try {
|
||||
const formData = new FormData(installForm);
|
||||
const response = await fetch(installForm.action, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
// 安装成功,更新按钮状态
|
||||
const appId = currentApp.id || currentApp.app_id;
|
||||
const cardInstallBtn = document.getElementById(`install-btn-${appId}`);
|
||||
if (cardInstallBtn) {
|
||||
cardInstallBtn.className = 'ui green compact button disabled';
|
||||
cardInstallBtn.innerHTML = '<i class="check icon"></i>已安装';
|
||||
cardInstallBtn.onclick = null;
|
||||
}
|
||||
|
||||
closeInstallModal();
|
||||
// 显示成功消息
|
||||
alert('应用安装成功!');
|
||||
} else {
|
||||
// 安装失败,恢复按钮状态
|
||||
if (installBtn) {
|
||||
installBtn.className = 'ui primary button';
|
||||
installBtn.textContent = '安装应用';
|
||||
installBtn.disabled = false;
|
||||
}
|
||||
const errorData = await response.json();
|
||||
alert('安装失败: ' + (errorData.error || '未知错误'));
|
||||
}
|
||||
} catch (error) {
|
||||
// 网络错误,恢复按钮状态
|
||||
if (installBtn) {
|
||||
installBtn.className = 'ui primary button';
|
||||
installBtn.textContent = '安装应用';
|
||||
installBtn.disabled = false;
|
||||
}
|
||||
alert('安装失败: ' + error.message);
|
||||
}
|
||||
|
||||
// 清理表单
|
||||
document.body.removeChild(installForm);
|
||||
// 与卸载一致:使用表单直接提交,后端重定向以显示绿色提示
|
||||
installForm.submit();
|
||||
return;
|
||||
}
|
||||
|
||||
function showAddAppModal() {
|
||||
|
||||
Reference in New Issue
Block a user