// Copyright 2024 The Devstar Authors. All rights reserved. // SPDX-License-Identifier: MIT package appstore import ( "context" "encoding/json" "net/http" "strings" "time" appstore_model "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 } // NewManager creates a new app store manager for database operations func NewManager(ctx *gitea_context.Context) *Manager { return &Manager{ parser: NewParser(), ctx: *ctx, k8s: NewK8sManager(*ctx), } } // 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 } // 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.ToLower(appTag) == strings.ToLower(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.validateApp(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.ParseApp(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 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 { return &AppStoreError{ Code: "DATABASE_ERROR", Message: "Failed to delete app from database", Details: err.Error(), } } 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, kubeconfig string, kubeconfigContext 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(), } } // 根据安装目标和应用实际部署类型决定安装方式 if installTarget == "kubeconfig" && kubeconfig != "" { // 安装到外部 Kubernetes 集群 if err := m.InstallAppToKubernetes(mergedApp, []byte(kubeconfig), kubeconfigContext); err != nil { return &AppStoreError{ Code: "KUBERNETES_INSTALL_ERROR", Message: "Kubernetes 安装失败", Details: err.Error(), } } } else { // 根据应用实际部署类型决定本地安装方式 log.Info("InstallApp: mergedApp.Deploy.Type = %s", mergedApp.Deploy.Type) switch mergedApp.Deploy.Type { case "kubernetes": // 应用要部署到 Kubernetes,安装到本地 K8s 集群 if err := m.InstallAppToKubernetes(mergedApp, nil, ""); err != nil { return &AppStoreError{ Code: "KUBERNETES_INSTALL_ERROR", Message: "本地 Kubernetes 安装失败", Details: err.Error(), } } case "docker": // 应用要部署到 Docker,安装到本地 Docker // TODO: 实现 Docker 安装逻辑 return &AppStoreError{ Code: "NOT_IMPLEMENTED", Message: "本地 Docker 安装功能开发中", } default: // 未知部署类型,默认尝试 Docker // TODO: 实现 Docker 安装逻辑 return &AppStoreError{ Code: "NOT_IMPLEMENTED", Message: "本地安装功能开发中", } } } 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) if err != nil { return &AppStoreError{ Code: "APP_NOT_FOUND", Message: "获取应用失败", Details: err.Error(), } } // 根据安装目标和应用实际部署类型决定卸载方式 if installTarget == "kubeconfig" && kubeconfig != "" { // 从外部 Kubernetes 集群卸载 if err := m.UninstallAppFromKubernetes(app, []byte(kubeconfig), kubeconfigContext); err != nil { return &AppStoreError{ Code: "KUBERNETES_UNINSTALL_ERROR", Message: "Kubernetes 卸载失败", Details: err.Error(), } } } else { // 根据应用实际部署类型决定本地卸载方式 switch app.Deploy.Type { case "kubernetes": // 应用部署在 Kubernetes,从本地 K8s 集群卸载 if err := m.UninstallAppFromKubernetes(app, nil, ""); err != nil { return &AppStoreError{ Code: "KUBERNETES_UNINSTALL_ERROR", Message: "本地 Kubernetes 卸载失败", Details: err.Error(), } } case "docker": // 应用部署在 Docker,从本地 Docker 卸载 // TODO: 实现 Docker 卸载逻辑 return &AppStoreError{ Code: "NOT_IMPLEMENTED", Message: "本地 Docker 卸载功能开发中", } default: // 未知部署类型,默认尝试 Docker // TODO: 实现 Docker 卸载逻辑 return &AppStoreError{ Code: "NOT_IMPLEMENTED", Message: "本地卸载功能开发中", } } } return nil } // InstallAppToKubernetes installs an application to a Kubernetes cluster func (m *Manager) InstallAppToKubernetes(app *App, kubeconfig []byte, contextName string) error { return m.k8s.InstallAppToKubernetes(app, kubeconfig, contextName) } // UninstallAppFromKubernetes uninstalls an application from a Kubernetes cluster func (m *Manager) UninstallAppFromKubernetes(app *App, kubeconfig []byte, contextName string) error { return m.k8s.UninstallAppFromKubernetes(app, kubeconfig, contextName) } // GetAppFromKubernetes gets an application from a Kubernetes cluster func (m *Manager) GetAppFromKubernetes(app *App, kubeconfig []byte, contextName string) (interface{}, error) { return m.k8s.GetAppFromKubernetes(app, kubeconfig, contextName) } // ListAppsFromKubernetes lists applications from a Kubernetes cluster func (m *Manager) ListAppsFromKubernetes(app *App, kubeconfig []byte, contextName string) (*applicationv1.ApplicationList, error) { return m.k8s.ListAppsFromKubernetes(app, kubeconfig, contextName) } // UpdateAppInKubernetes updates an application in a Kubernetes cluster func (m *Manager) UpdateAppInKubernetes(app *App, kubeconfig []byte, contextName string) error { return m.k8s.UpdateAppInKubernetes(app, kubeconfig, contextName) }