Files
devstar/routers/web/user/setting/appstore.go
2025-11-25 14:24:05 +08:00

657 lines
19 KiB
Go

/*
* Copyright (c) Mengning Software. 2025. All rights reserved.
* Authors: DevStar Team, panshuxiao
* Create: 2025-11-19
* Description: User settings handlers for AppStore UI.
*/
// Copyright 2024 The Devstar Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package setting
import (
"io"
"net/http"
"strings"
appstore_model "code.gitea.io/gitea/models/appstore"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/services/appstore"
"code.gitea.io/gitea/services/context"
)
const (
tplSettingsAppStore templates.TplName = "user/settings/appstore"
)
// AppStore displays the app store page
func AppStore(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("settings.appstore")
ctx.Data["PageIsSettingsAppStore"] = true
ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
ctx.HTML(http.StatusOK, tplSettingsAppStore)
}
// AppStoreAPI handles API requests for app store data
func AppStoreAPI(ctx *context.Context) {
action := ctx.PathParam("action")
switch action {
case "apps":
handleGetApps(ctx)
case "categories":
handleGetCategories(ctx)
case "tags":
handleGetTags(ctx)
default:
ctx.JSON(http.StatusBadRequest, map[string]interface{}{
"error": "Invalid action",
})
}
}
// handleGetApps returns the list of apps from database
func handleGetApps(ctx *context.Context) {
// Get query parameters
category := ctx.FormString("category")
tag := ctx.FormString("tag")
search := ctx.FormString("search")
deployment := ctx.FormString("deployment") // docker, kubernetes
source := ctx.FormString("source") // local | devstar
manager := appstore.NewManager(ctx, ctx.Doer.ID)
var apps []appstore.App
var err error
if source == "devstar" {
apps, err = manager.ListAppsFromDevstar()
if err == nil {
// 与本地一致的过滤逻辑(先拿全量,再在服务端筛选)
if category != "" || tag != "" || search != "" || deployment != "" {
var filtered []appstore.App
for _, a := range apps {
if category != "" && a.Category != category {
continue
}
if tag != "" {
matched := false
for _, t := range a.Tags {
if t == tag {
matched = true
break
}
}
if !matched {
continue
}
}
if deployment != "" {
// 处理部署类型过滤,包括 'both' 类型
appDeployment := a.DeploymentType
if appDeployment == "" {
// 如果没有 deployment_type 字段,尝试从其他字段获取
if a.Deploy.Type != "" {
appDeployment = a.Deploy.Type
} else {
appDeployment = "docker" // 默认值
}
}
if appDeployment != "both" && appDeployment != deployment {
continue
}
}
if search != "" {
low := strings.ToLower(search)
nameOk := strings.Contains(strings.ToLower(a.Name), low)
descOk := strings.Contains(strings.ToLower(a.Description), low)
authorOk := strings.Contains(strings.ToLower(a.Author), low)
if !(nameOk || descOk || authorOk) {
continue
}
}
filtered = append(filtered, a)
}
apps = filtered
}
}
} else {
if search != "" {
// Convert tag parameter to tags slice
var tags []string
if tag != "" {
tags = strings.Split(tag, ",")
}
apps, err = manager.SearchApps(search, category, tags)
} else {
apps, err = manager.ListApps()
// Filter by category, tag and deployment if specified
if category != "" || tag != "" || deployment != "" {
var filteredApps []appstore.App
for _, app := range apps {
matchCategory := category == "" || app.Category == category
matchTag := tag == ""
if tag != "" {
for _, appTag := range app.Tags {
if appTag == tag {
matchTag = true
break
}
}
}
// 处理部署类型过滤,包括 'both' 类型
matchDeployment := deployment == ""
if deployment != "" {
appDeployment := app.DeploymentType
if appDeployment == "" {
// 如果没有 deployment_type 字段,尝试从其他字段获取
if app.Deploy.Type != "" {
appDeployment = app.Deploy.Type
} else {
appDeployment = "docker" // 默认值
}
}
matchDeployment = appDeployment == "both" || appDeployment == deployment
}
if matchCategory && matchTag && matchDeployment {
filteredApps = append(filteredApps, app)
}
}
apps = filteredApps
}
}
}
if err != nil {
ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
"error": err.Error(),
})
return
}
ctx.JSON(http.StatusOK, map[string]interface{}{
"apps": apps,
})
}
// handleGetCategories returns the list of categories
func handleGetCategories(ctx *context.Context) {
categories, err := appstore_model.GetCategories(ctx)
if err != nil {
ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
"error": err.Error(),
})
return
}
ctx.JSON(http.StatusOK, map[string]interface{}{
"categories": categories,
})
}
// handleGetTags returns the list of tags
func handleGetTags(ctx *context.Context) {
tags, err := appstore_model.GetTags(ctx)
if err != nil {
ctx.JSON(http.StatusBadRequest, map[string]interface{}{
"error": err.Error(),
})
return
}
ctx.JSON(http.StatusOK, map[string]interface{}{
"tags": tags,
})
}
// AppStoreStatus handles app instance status requests
func AppStoreStatus(ctx *context.Context) {
appID := ctx.PathParam("app_id")
if appID == "" {
ctx.JSON(http.StatusBadRequest, map[string]interface{}{
"error": "应用ID不能为空",
})
return
}
manager := appstore.NewManager(ctx, ctx.Doer.ID)
status, err := manager.GetAppStatus(appID)
if err != nil {
if appErr, ok := err.(*appstore.AppStoreError); ok {
ctx.JSON(http.StatusNotFound, map[string]interface{}{
"error": appErr.Message,
})
} else {
ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
"error": "获取应用状态失败",
})
}
return
}
ctx.JSON(http.StatusOK, status)
}
// AppStoreInstall handles app installation
func AppStoreInstall(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")
configJSON := ctx.FormString("config")
installTarget := ctx.FormString("install_target")
k8sURL := ctx.FormString("k8s_url")
token := ctx.FormString("k8s_token")
if appID == "" {
ctx.JSON(400, map[string]string{"error": "应用ID不能为空"})
return
}
// 权限校验:仅管理员可选择默认位置
if installTarget == "local" && (ctx.Doer == nil || !ctx.Doer.IsAdmin) {
ctx.JSON(403, map[string]string{"error": "仅管理员可选择默认位置"})
return
}
// 创建 manager 并执行安装
manager := appstore.NewManager(ctx, ctx.Doer.ID)
if err := manager.InstallApp(appID, configJSON, installTarget, k8sURL, token); err != nil {
// 根据错误类型返回相应的状态码和消息
if appErr, ok := err.(*appstore.AppStoreError); ok {
switch appErr.Code {
case "APP_NOT_FOUND", "CONFIG_MERGE_ERROR":
ctx.JSON(400, map[string]string{"error": appErr.Message + ": " + appErr.Details})
case "KUBERNETES_INSTALL_ERROR":
ctx.JSON(500, map[string]string{"error": appErr.Message + ": " + appErr.Details})
case "NOT_IMPLEMENTED":
ctx.JSON(501, 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" && k8sURL != "" && token != "" {
ctx.Flash.Success("应用已成功安装到自定义位置")
} else {
ctx.Flash.Success("应用已成功安装到默认位置")
}
// 优先根据表单的 redirect_to 回跳(前端会带上当前路径)
if redirectTo := ctx.FormString("redirect_to"); redirectTo != "" {
ctx.RedirectToCurrentSite(redirectTo)
} else {
ctx.Redirect(setting.AppSubURL + "/user/settings/appstore")
}
}
// AppStoreUpdate handles app update
func AppStoreUpdate(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")
configJSON := ctx.FormString("config")
installTarget := ctx.FormString("install_target")
k8sURL := ctx.FormString("k8s_url")
token := ctx.FormString("k8s_token")
if appID == "" {
ctx.JSON(400, map[string]string{"error": "应用ID不能为空"})
return
}
// 权限校验:仅管理员可选择默认位置
if installTarget == "local" && (ctx.Doer == nil || !ctx.Doer.IsAdmin) {
ctx.JSON(403, map[string]string{"error": "仅管理员可选择默认位置"})
return
}
// 创建 manager 并执行更新
manager := appstore.NewManager(ctx, ctx.Doer.ID)
if err := manager.UpdateInstalledApp(appID, configJSON, installTarget, k8sURL, token); err != nil {
// 根据错误类型返回相应的状态码和消息
if appErr, ok := err.(*appstore.AppStoreError); ok {
switch appErr.Code {
case "APP_NOT_FOUND", "CONFIG_MERGE_ERROR":
ctx.JSON(400, map[string]string{"error": appErr.Message + ": " + appErr.Details})
case "KUBERNETES_UPDATE_ERROR":
ctx.JSON(500, map[string]string{"error": appErr.Message + ": " + appErr.Details})
case "NOT_IMPLEMENTED":
ctx.JSON(501, 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" && k8sURL != "" && token != "" {
ctx.Flash.Success("应用已成功更新到自定义位置")
} else {
ctx.Flash.Success("应用已成功更新到默认位置")
}
// 优先根据表单的 redirect_to 回跳(前端会带上当前路径)
if redirectTo := ctx.FormString("redirect_to"); redirectTo != "" {
ctx.RedirectToCurrentSite(redirectTo)
} else {
ctx.Redirect(setting.AppSubURL + "/user/settings/appstore")
}
}
// AppStoreUninstall handles app uninstallation
func AppStoreUninstall(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 并执行卸载
// UninstallApp 会自动从数据库读取保存的 Kubernetes 凭据判断是外部集群还是本地集群
manager := appstore.NewManager(ctx, ctx.Doer.ID)
if err := manager.UninstallApp(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 "KUBERNETES_UNINSTALL_ERROR":
ctx.JSON(500, map[string]string{"error": appErr.Message + ": " + appErr.Details})
case "NOT_IMPLEMENTED":
ctx.JSON(501, 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
}
// 卸载成功
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")
k8sURL := ctx.FormString("k8s_url")
token := ctx.FormString("k8s_token")
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, k8sURL, token); 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" && k8sURL != "" && token != "" {
ctx.Flash.Success("应用已成功在指定位置恢复")
} else {
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")
}
}
// AppStoreStop handles app stopping (setting replicas to 0)
func AppStoreStop(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")
k8sURL := ctx.FormString("k8s_url")
token := ctx.FormString("k8s_token")
if appID == "" {
ctx.JSON(400, map[string]string{"error": "应用ID不能为空"})
return
}
// 创建 manager 并执行暂停
manager := appstore.NewManager(ctx, ctx.Doer.ID)
if err := manager.StopApp(appID, installTarget, k8sURL, token); 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_STOP_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" && k8sURL != "" && token != "" {
ctx.Flash.Success("应用已成功在指定位置暂停")
} else {
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")
}
}
// AppStoreConfigure handles app configuration
func AppStoreConfigure(ctx *context.Context) {
// TODO: Load app configuration form
ctx.Data["Title"] = "App Configuration"
ctx.Data["PageIsSettingsAppStore"] = true
ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
ctx.HTML(http.StatusOK, tplSettingsAppStore)
}
// AppStoreConfigurePost handles app configuration form submission
func AppStoreConfigurePost(ctx *context.Context) {
// 检查用户登录状态
if !ctx.IsSigned {
ctx.JSON(401, map[string]string{"error": "未登录"})
return
}
// TODO: Handle configuration form submission
ctx.Flash.Success("App configuration feature coming soon")
if redirectTo := ctx.FormString("redirect_to"); redirectTo != "" {
ctx.RedirectToCurrentSite(redirectTo)
} else {
ctx.Redirect(setting.AppSubURL + "/user/settings/appstore")
}
}
// 添加应用API
func AddAppAPI(ctx *context.Context) {
// 检查 CSRF token
if !ctx.IsSigned {
ctx.JSON(401, map[string]string{"error": "未登录"})
return
}
manager := appstore.NewManager(ctx, ctx.Doer.ID)
if ctx.Req.Method != "POST" {
ctx.JSON(405, map[string]string{"error": "Method Not Allowed"})
return
}
defer ctx.Req.Body.Close()
jsonBytes, err := io.ReadAll(ctx.Req.Body)
if err != nil {
ctx.JSON(400, map[string]string{"error": "读取请求体失败"})
return
}
if err := manager.AddAppFromJSON(jsonBytes); err != nil {
ctx.JSON(400, map[string]string{"error": err.Error()})
return
}
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
}
// 从路径参数获取应用ID
appID := ctx.PathParam("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")
}
}