986 lines
27 KiB
Go
986 lines
27 KiB
Go
/*
|
||
* Copyright (c) Mengning Software. 2025. All rights reserved.
|
||
* Authors: DevStar Team, panshuxiao
|
||
* Create: 2025-11-19
|
||
* Description: Core business logic for AppStore lifecycle.
|
||
*/
|
||
|
||
package appstore
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"fmt"
|
||
"net/http"
|
||
"strings"
|
||
"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"
|
||
)
|
||
|
||
// Manager handles app store operations with database
|
||
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, userID int64) *Manager {
|
||
return &Manager{
|
||
parser: NewParser(),
|
||
ctx: *ctx,
|
||
k8s: NewK8sManager(*ctx),
|
||
userID: userID,
|
||
}
|
||
}
|
||
|
||
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)
|
||
if err != nil {
|
||
return nil, &AppStoreError{
|
||
Code: "DATABASE_ERROR",
|
||
Message: "Failed to list apps from database",
|
||
Details: err.Error(),
|
||
}
|
||
}
|
||
|
||
var apps []App
|
||
for _, appStore := range appStores {
|
||
if app, err := m.convertAppStoreToApp(appStore); err == nil {
|
||
apps = append(apps, *app)
|
||
}
|
||
}
|
||
|
||
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}
|
||
url := "https://devstar.cn/api/v1/appstore/apps"
|
||
resp, err := client.Get(url)
|
||
if err != nil {
|
||
return nil, &AppStoreError{Code: "REMOTE_ERROR", Message: "Failed to fetch apps from devstar", Details: err.Error()}
|
||
}
|
||
defer resp.Body.Close()
|
||
if resp.StatusCode != http.StatusOK {
|
||
return nil, &AppStoreError{Code: "REMOTE_ERROR", Message: "Invalid status from devstar", Details: resp.Status}
|
||
}
|
||
var payload struct {
|
||
Apps []App `json:"apps"`
|
||
}
|
||
if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil {
|
||
return nil, &AppStoreError{Code: "REMOTE_ERROR", Message: "Failed to decode devstar response", Details: err.Error()}
|
||
}
|
||
return payload.Apps, nil
|
||
}
|
||
|
||
// GetApp returns a specific application from database
|
||
func (m *Manager) GetApp(appID string) (*App, error) {
|
||
appStore, err := appstore_model.GetAppStoreByAppID(m.ctx, appID)
|
||
if err != nil {
|
||
return nil, &AppStoreError{
|
||
Code: "APP_NOT_FOUND",
|
||
Message: "App not found in database",
|
||
Details: err.Error(),
|
||
}
|
||
}
|
||
|
||
return m.convertAppStoreToApp(appStore)
|
||
}
|
||
|
||
// convertAppStoreToApp convert database AppStore model to App struct
|
||
func (m *Manager) convertAppStoreToApp(appStore *appstore_model.AppStore) (*App, error) {
|
||
// 基本信息直接从数据库字段获取
|
||
app := App{
|
||
ID: appStore.AppID,
|
||
Name: appStore.Name,
|
||
Description: appStore.Description,
|
||
Category: appStore.Category,
|
||
Tags: appStore.GetTagsList(),
|
||
Icon: appStore.Icon,
|
||
Author: appStore.Author,
|
||
Website: appStore.Website,
|
||
Repository: appStore.Repository,
|
||
License: appStore.License,
|
||
Version: appStore.Version,
|
||
DeploymentType: appStore.DeploymentType,
|
||
}
|
||
|
||
// 如果JSONData不为空,交由 parser 解析并合并(DB 字段优先)
|
||
if appStore.JSONData != "" {
|
||
merged, err := m.parser.ParseAndMergeApp([]byte(appStore.JSONData), &app)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
app = *merged
|
||
}
|
||
|
||
return &app, nil
|
||
}
|
||
|
||
// convertAppToAppStore converts App struct to database AppStore model
|
||
func (m *Manager) convertAppToAppStore(app *App) (*appstore_model.AppStore, error) {
|
||
jsonData, err := json.Marshal(app)
|
||
if err != nil {
|
||
return nil, &AppStoreError{
|
||
Code: "JSON_MARSHAL_ERROR",
|
||
Message: "Failed to marshal app to JSON",
|
||
Details: err.Error(),
|
||
}
|
||
}
|
||
|
||
appStore := &appstore_model.AppStore{
|
||
AppID: app.ID,
|
||
Name: app.Name,
|
||
Description: app.Description,
|
||
Category: app.Category,
|
||
Icon: app.Icon,
|
||
Author: app.Author,
|
||
Website: app.Website,
|
||
Repository: app.Repository,
|
||
License: app.License,
|
||
Version: app.Version,
|
||
DeploymentType: app.DeploymentType,
|
||
JSONData: string(jsonData),
|
||
IsActive: true,
|
||
IsOfficial: false,
|
||
IsVerified: true,
|
||
}
|
||
|
||
appStore.SetTagsList(app.Tags)
|
||
return appStore, nil
|
||
}
|
||
|
||
// SearchApps searches for applications by various criteria
|
||
func (m *Manager) SearchApps(query string, category string, tags []string) ([]App, error) {
|
||
apps, err := m.ListApps()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
var results []App
|
||
query = strings.ToLower(query)
|
||
|
||
for _, app := range apps {
|
||
// Check if app matches search criteria
|
||
matches := false
|
||
|
||
// Text search
|
||
if query != "" {
|
||
if strings.Contains(strings.ToLower(app.Name), query) ||
|
||
strings.Contains(strings.ToLower(app.Description), query) ||
|
||
strings.Contains(strings.ToLower(app.Author), query) {
|
||
matches = true
|
||
}
|
||
} else {
|
||
matches = true
|
||
}
|
||
|
||
// Category filter
|
||
if category != "" && app.Category != category {
|
||
matches = false
|
||
}
|
||
|
||
// Tags filter
|
||
if len(tags) > 0 {
|
||
tagMatch := false
|
||
for _, searchTag := range tags {
|
||
for _, appTag := range app.Tags {
|
||
if strings.EqualFold(appTag, searchTag) {
|
||
tagMatch = true
|
||
break
|
||
}
|
||
}
|
||
if tagMatch {
|
||
break
|
||
}
|
||
}
|
||
if !tagMatch {
|
||
matches = false
|
||
}
|
||
}
|
||
|
||
if matches {
|
||
results = append(results, app)
|
||
}
|
||
}
|
||
|
||
return results, nil
|
||
}
|
||
|
||
// GetCategories returns all available categories
|
||
func (m *Manager) GetCategories() ([]string, error) {
|
||
apps, err := m.ListApps()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
categories := make(map[string]bool)
|
||
for _, app := range apps {
|
||
categories[app.Category] = true
|
||
}
|
||
|
||
var result []string
|
||
for category := range categories {
|
||
result = append(result, category)
|
||
}
|
||
|
||
return result, nil
|
||
}
|
||
|
||
// GetTags returns all available tags
|
||
func (m *Manager) GetTags() ([]string, error) {
|
||
apps, err := m.ListApps()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
tags := make(map[string]bool)
|
||
for _, app := range apps {
|
||
for _, tag := range app.Tags {
|
||
tags[tag] = true
|
||
}
|
||
}
|
||
|
||
var result []string
|
||
for tag := range tags {
|
||
result = append(result, tag)
|
||
}
|
||
|
||
return result, nil
|
||
}
|
||
|
||
// PrepareInstallation prepares an application for installation by merging user config
|
||
// Returns a complete App structure with merged configuration
|
||
// func (m *Manager) PrepareInstallation(appID string, userConfig UserConfig) (*App, error) {
|
||
// // Load the application
|
||
// app, err := m.GetApp(appID)
|
||
// if err != nil {
|
||
// return nil, err
|
||
// }
|
||
|
||
// // Set the app ID and version in user config if not provided
|
||
// if userConfig.AppID == "" {
|
||
// userConfig.AppID = appID
|
||
// }
|
||
// if userConfig.Version == "" {
|
||
// userConfig.Version = app.Version
|
||
// }
|
||
|
||
// // Merge user configuration with app's default configuration
|
||
// mergedApp, err := m.parser.MergeUserConfig(app, userConfig)
|
||
// if err != nil {
|
||
// return nil, err
|
||
// }
|
||
|
||
// return mergedApp, nil
|
||
// }
|
||
|
||
// AddApp adds a new application to the database
|
||
func (m *Manager) AddApp(app *App) error {
|
||
// Validate the app
|
||
if err := m.parser.validateAppTemplate(app); err != nil {
|
||
return err
|
||
}
|
||
|
||
// Convert to database model
|
||
appStore, err := m.convertAppToAppStore(app)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
// Save to database
|
||
if err := appstore_model.CreateAppStore(m.ctx, appStore); err != nil {
|
||
return &AppStoreError{
|
||
Code: "DATABASE_ERROR",
|
||
Message: "Failed to create app in database",
|
||
Details: err.Error(),
|
||
}
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// AddAppFromJSON 通过 parser 解析原始 JSON 并添加应用
|
||
func (m *Manager) AddAppFromJSON(jsonBytes []byte) error {
|
||
app, err := m.parser.ParseAppTemplate(jsonBytes)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return m.AddApp(app)
|
||
}
|
||
|
||
// UpdateApp updates an existing application in database
|
||
func (m *Manager) UpdateApp(app *App) error {
|
||
// Validate the app
|
||
if err := m.parser.validateApp(app); err != nil {
|
||
return err
|
||
}
|
||
|
||
// Get existing app from database
|
||
existingAppStore, err := appstore_model.GetAppStoreByAppID(m.ctx, app.ID)
|
||
if err != nil {
|
||
return &AppStoreError{
|
||
Code: "APP_NOT_FOUND",
|
||
Message: "App not found in database",
|
||
Details: err.Error(),
|
||
}
|
||
}
|
||
|
||
// Convert to database model
|
||
appStore, err := m.convertAppToAppStore(app)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
// Preserve database ID and timestamps
|
||
appStore.ID = existingAppStore.ID
|
||
appStore.CreatedUnix = existingAppStore.CreatedUnix
|
||
|
||
// Update in database
|
||
if err := appstore_model.UpdateAppStore(m.ctx, appStore); err != nil {
|
||
return &AppStoreError{
|
||
Code: "DATABASE_ERROR",
|
||
Message: "Failed to update app in database",
|
||
Details: err.Error(),
|
||
}
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// RemoveApp removes an application template from the database
|
||
func (m *Manager) RemoveApp(appID string) error {
|
||
// 获取应用模板记录
|
||
appStore, err := appstore_model.GetAppStoreByAppID(m.ctx, appID)
|
||
if err != nil {
|
||
return &AppStoreError{
|
||
Code: "APP_NOT_FOUND",
|
||
Message: "应用模板不存在",
|
||
Details: err.Error(),
|
||
}
|
||
}
|
||
|
||
// 删除应用模板
|
||
if err := appstore_model.DeleteAppStore(m.ctx, appStore.ID); err != nil {
|
||
return &AppStoreError{
|
||
Code: "DATABASE_ERROR",
|
||
Message: "删除应用模板失败",
|
||
Details: err.Error(),
|
||
}
|
||
}
|
||
|
||
log.Info("Successfully removed app template %s", appID)
|
||
return nil
|
||
}
|
||
|
||
// GetAppConfigSchema returns the configuration schema for an application
|
||
func (m *Manager) GetAppConfigSchema(appID string) (*AppConfig, error) {
|
||
app, err := m.GetApp(appID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
return &app.Config, nil
|
||
}
|
||
|
||
// ValidateUserConfig validates user configuration against app schema
|
||
func (m *Manager) ValidateUserConfig(appID string, userConfig UserConfig) error {
|
||
app, err := m.GetApp(appID)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
// Set the app ID and version in user config if not provided
|
||
if userConfig.AppID == "" {
|
||
userConfig.AppID = appID
|
||
}
|
||
if userConfig.Version == "" {
|
||
userConfig.Version = app.Version
|
||
}
|
||
|
||
// Try to merge config (this will validate)
|
||
_, err = m.parser.MergeUserConfig(app, userConfig)
|
||
return err
|
||
}
|
||
|
||
// InstallApp installs an application based on the provided parameters
|
||
func (m *Manager) InstallApp(appID string, configJSON string, installTarget string, k8sURL string, token string) error {
|
||
// 获取应用信息
|
||
app, err := m.GetApp(appID)
|
||
if err != nil {
|
||
return &AppStoreError{
|
||
Code: "APP_NOT_FOUND",
|
||
Message: "获取应用失败",
|
||
Details: err.Error(),
|
||
}
|
||
}
|
||
|
||
// 使用 parser 解析和合并用户配置
|
||
log.Info("InstallApp: configJSON = %s", configJSON)
|
||
mergedApp, err := m.parser.ParseAndMergeUserConfig(app, configJSON)
|
||
if err != nil {
|
||
return &AppStoreError{
|
||
Code: "CONFIG_MERGE_ERROR",
|
||
Message: "配置合并失败",
|
||
Details: err.Error(),
|
||
}
|
||
}
|
||
|
||
// 确定部署类型
|
||
deployType := mergedApp.Deploy.Type
|
||
if deployType == "" {
|
||
// 如果 Deploy.Type 为空,根据 DeploymentType 推断
|
||
switch mergedApp.DeploymentType {
|
||
case "kubernetes", "both":
|
||
deployType = "kubernetes"
|
||
case "docker":
|
||
deployType = "docker"
|
||
}
|
||
}
|
||
|
||
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(),
|
||
}
|
||
}
|
||
case "docker":
|
||
// TODO: 实现 Docker 安装逻辑
|
||
return &AppStoreError{
|
||
Code: "NOT_IMPLEMENTED",
|
||
Message: "本地 Docker 安装功能开发中",
|
||
}
|
||
default:
|
||
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 := &user_app_instance.UserAppInstance{
|
||
UserID: m.userID,
|
||
AppID: appID,
|
||
InstanceName: mergedApp.Name,
|
||
UserConfig: configJSON,
|
||
MergedApp: string(mergedAppJSON),
|
||
DeployType: deployType,
|
||
}
|
||
if inputCred != nil && deployType == "kubernetes" {
|
||
instance.K8sURL = inputCred.K8sURL
|
||
instance.K8sToken = inputCred.Token
|
||
}
|
||
|
||
if err := user_app_instance.CreateUserAppInstance(m.ctx, instance); err != nil {
|
||
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, k8sURL string, token 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{
|
||
Code: "APP_NOT_FOUND",
|
||
Message: "获取应用失败",
|
||
Details: err.Error(),
|
||
}
|
||
}
|
||
|
||
// 使用 parser 解析和合并用户配置
|
||
mergedApp, err := m.parser.ParseAndMergeUserConfig(app, configJSON)
|
||
if err != nil {
|
||
return &AppStoreError{
|
||
Code: "CONFIG_MERGE_ERROR",
|
||
Message: "配置合并失败",
|
||
Details: err.Error(),
|
||
}
|
||
}
|
||
|
||
// 确定部署类型
|
||
deployType := mergedApp.Deploy.Type
|
||
if deployType == "" {
|
||
deployType = instance.DeployType
|
||
}
|
||
|
||
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()}
|
||
}
|
||
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)
|
||
return &AppStoreError{
|
||
Code: "JSON_MARSHAL_ERROR",
|
||
Message: "保存应用配置失败",
|
||
Details: err.Error(),
|
||
}
|
||
}
|
||
|
||
instance.UserConfig = configJSON
|
||
instance.MergedApp = string(mergedAppJSON)
|
||
instance.DeployType = deployType
|
||
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)
|
||
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
|
||
// It automatically determines whether to uninstall from external cluster or local cluster
|
||
// 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)
|
||
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(),
|
||
}
|
||
}
|
||
|
||
// 根据数据库中保存的凭据判断卸载方式:存在 URL+Token 则视为外部集群,否则使用默认集群
|
||
cred := credentialFromInstance(instance)
|
||
if instance.DeployType == "kubernetes" {
|
||
if cred != nil {
|
||
log.Info("UninstallApp: Uninstalling from external cluster, appID=%s, url=%s", appID, cred.K8sURL)
|
||
} else {
|
||
log.Info("UninstallApp: Uninstalling from default cluster, appID=%s", appID)
|
||
}
|
||
|
||
if err := m.UninstallAppFromKubernetes(&app, cred); err != nil {
|
||
return &AppStoreError{
|
||
Code: "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)
|
||
// 注意:这里不返回错误,因为 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
|
||
}
|
||
|
||
// InstallAppToKubernetes installs an application to a Kubernetes cluster
|
||
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, cred *K8sCredential) error {
|
||
return m.k8s.UninstallAppFromKubernetes(app, cred)
|
||
}
|
||
|
||
// GetAppFromKubernetes gets an application from a Kubernetes cluster
|
||
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, 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, cred *K8sCredential) error {
|
||
return m.k8s.UpdateAppInKubernetes(app, cred)
|
||
}
|
||
|
||
// IsInstalledAppReady 提供给上层的就绪判断入口,避免直接依赖 k8s 层实现
|
||
func (m *Manager) IsInstalledAppReady(status *applicationv1.ApplicationStatus) bool {
|
||
return m.k8s.IsApplicationReady(status)
|
||
}
|
||
|
||
// GetAppStatus 获取应用的安装和运行状态
|
||
func (m *Manager) GetAppStatus(appID string) (map[string]interface{}, error) {
|
||
// 获取用户的应用实例
|
||
instance, err := user_app_instance.GetUserAppInstanceByAppID(m.ctx, m.userID, appID)
|
||
if err != nil {
|
||
return nil, &AppStoreError{
|
||
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(),
|
||
}
|
||
}
|
||
|
||
// 检查应用是否支持 Kubernetes 部署
|
||
if instance.DeployType == "kubernetes" {
|
||
// 尝试从 Kubernetes 获取状态
|
||
cred := credentialFromInstance(instance)
|
||
|
||
k8sApp, err := m.GetAppFromKubernetes(&app, cred)
|
||
if err != nil {
|
||
// 如果应用实例存在于数据库中,但 Kubernetes 中找不到,可能是被暂停了
|
||
// 返回暂停状态而不是未安装状态
|
||
return map[string]interface{}{
|
||
"phase": "Paused",
|
||
"replicas": 0,
|
||
"readyReplicas": 0,
|
||
"ready": false,
|
||
}, nil
|
||
}
|
||
|
||
// 应用已安装,返回状态信息
|
||
if k8sApp != nil {
|
||
// 尝试解析 K8s Application 状态
|
||
if appData, ok := k8sApp.(*applicationv1.Application); ok {
|
||
// 使用 Application CRD 的实际状态
|
||
phase := string(appData.Status.Phase)
|
||
replicas := int(appData.Status.Replicas)
|
||
readyReplicas := int(appData.Status.ReadyReplicas)
|
||
|
||
// 判断是否就绪
|
||
ready := m.IsInstalledAppReady(&appData.Status)
|
||
|
||
return map[string]interface{}{
|
||
"phase": phase,
|
||
"replicas": replicas,
|
||
"readyReplicas": readyReplicas,
|
||
"ready": ready,
|
||
}, nil
|
||
}
|
||
// 如果无法解析,返回错误
|
||
return nil, &AppStoreError{
|
||
Code: "STATUS_PARSE_ERROR",
|
||
Message: "无法解析应用状态数据",
|
||
Details: fmt.Sprintf("期望 *applicationv1.Application 类型,实际类型: %T", k8sApp),
|
||
}
|
||
}
|
||
}
|
||
|
||
// 默认返回未安装状态
|
||
return map[string]interface{}{
|
||
"phase": "Not Installed",
|
||
"replicas": 0,
|
||
"readyReplicas": 0,
|
||
"ready": false,
|
||
}, nil
|
||
}
|
||
|
||
// StopApp stops an application by setting replicas to 0
|
||
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 {
|
||
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" {
|
||
// 创建应用的副本并设置副本数为 0
|
||
stoppedApp := app
|
||
if stoppedApp.Deploy.Kubernetes != nil {
|
||
stoppedApp.Deploy.Kubernetes.Replicas = 0
|
||
}
|
||
|
||
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: "停止应用失败",
|
||
Details: err.Error(),
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// 如果应用不支持 Kubernetes 部署,返回错误
|
||
return &AppStoreError{
|
||
Code: "UNSUPPORTED_DEPLOYMENT_TYPE",
|
||
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, k8sURL string, token 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
|
||
}
|
||
}
|
||
|
||
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: "恢复应用失败",
|
||
Details: err.Error(),
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// 如果应用不支持 Kubernetes 部署,返回错误
|
||
return &AppStoreError{
|
||
Code: "UNSUPPORTED_DEPLOYMENT_TYPE",
|
||
Message: fmt.Sprintf("应用部署类型 '%s' 不支持恢复功能,目前仅支持 Kubernetes 部署的应用", instance.DeployType),
|
||
}
|
||
}
|