将应用商店的用户实例保存到数据库

This commit is contained in:
panshuxiao
2025-09-07 10:34:06 +08:00
repo.diff.parent d86772249c
repo.diff.commit c0e4044dab
repo.diff.stats_desc%!(EXTRA int=9, int=651, int=173)

repo.diff.view_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))
}

repo.diff.view_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
}

repo.diff.view_file

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

repo.diff.view_file

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

repo.diff.view_file

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

repo.diff.view_file

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

repo.diff.view_file

@@ -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(),
}
}

repo.diff.view_file

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

repo.diff.view_file

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