550 lines
15 KiB
Go
550 lines
15 KiB
Go
|
// 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)
|
|||
|
}
|