2025-11-19 21:40:10 +08:00
|
|
|
|
/*
|
|
|
|
|
|
* Copyright (c) Mengning Software. 2025. All rights reserved.
|
|
|
|
|
|
* Authors: DevStar Team, panshuxiao
|
|
|
|
|
|
* Create: 2025-11-19
|
|
|
|
|
|
* Description: Coordinates Kubernetes operations for installs.
|
|
|
|
|
|
*/
|
2025-08-15 18:07:41 +08:00
|
|
|
|
|
|
|
|
|
|
package appstore
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"context"
|
2025-09-02 11:45:50 +08:00
|
|
|
|
"fmt"
|
2025-08-15 18:07:41 +08:00
|
|
|
|
|
|
|
|
|
|
k8s "code.gitea.io/gitea/modules/k8s"
|
|
|
|
|
|
applicationv1 "code.gitea.io/gitea/modules/k8s/api/application/v1"
|
|
|
|
|
|
"code.gitea.io/gitea/modules/k8s/application"
|
2025-09-07 10:34:06 +08:00
|
|
|
|
"code.gitea.io/gitea/modules/log"
|
2025-08-15 18:07:41 +08:00
|
|
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
2025-11-25 14:24:05 +08:00
|
|
|
|
dynamicclient "k8s.io/client-go/dynamic"
|
2025-08-15 18:07:41 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// K8sManager handles Kubernetes-specific application operations
|
|
|
|
|
|
type K8sManager struct {
|
|
|
|
|
|
ctx context.Context
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-25 14:24:05 +08:00
|
|
|
|
// K8sCredential describes how to reach a Kubernetes cluster.
|
|
|
|
|
|
type K8sCredential struct {
|
|
|
|
|
|
K8sURL string
|
|
|
|
|
|
Token string
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-15 18:07:41 +08:00
|
|
|
|
// NewK8sManager creates a new Kubernetes manager
|
|
|
|
|
|
func NewK8sManager(ctx context.Context) *K8sManager {
|
|
|
|
|
|
return &K8sManager{
|
|
|
|
|
|
ctx: ctx,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-25 14:24:05 +08:00
|
|
|
|
// buildK8sClient creates a dynamic client from the provided credential.
|
|
|
|
|
|
func (km *K8sManager) buildK8sClient(cred *K8sCredential) (dynamicclient.Interface, error) {
|
|
|
|
|
|
if cred != nil && cred.K8sURL != "" && cred.Token != "" {
|
|
|
|
|
|
return k8s.GetKubernetesClientWithToken(km.ctx, cred.K8sURL, cred.Token)
|
|
|
|
|
|
}
|
|
|
|
|
|
return k8s.GetKubernetesClientWithToken(km.ctx, "", "")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-15 18:07:41 +08:00
|
|
|
|
// InstallAppToKubernetes installs an application to a Kubernetes cluster
|
2025-11-25 14:24:05 +08:00
|
|
|
|
func (km *K8sManager) InstallAppToKubernetes(app *App, cred *K8sCredential) error {
|
2025-08-15 18:07:41 +08:00
|
|
|
|
// Validate that the app supports Kubernetes deployment
|
2025-09-02 11:45:50 +08:00
|
|
|
|
// 优先检查实际部署类型,如果为空则检查支持的部署类型
|
|
|
|
|
|
if app.Deploy.Type != "" && app.Deploy.Type != "kubernetes" {
|
|
|
|
|
|
return &AppStoreError{
|
|
|
|
|
|
Code: "DEPLOYMENT_TYPE_ERROR",
|
|
|
|
|
|
Message: "Application is not configured for Kubernetes deployment",
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if app.Deploy.Type == "" && app.DeploymentType != "kubernetes" && app.DeploymentType != "both" {
|
2025-08-15 18:07:41 +08:00
|
|
|
|
return &AppStoreError{
|
|
|
|
|
|
Code: "DEPLOYMENT_TYPE_ERROR",
|
|
|
|
|
|
Message: "Application does not support Kubernetes deployment",
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Get Kubernetes client
|
2025-11-25 14:24:05 +08:00
|
|
|
|
k8sClient, err := km.buildK8sClient(cred)
|
2025-08-15 18:07:41 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return &AppStoreError{
|
|
|
|
|
|
Code: "KUBERNETES_CLIENT_ERROR",
|
|
|
|
|
|
Message: "Failed to create Kubernetes client",
|
|
|
|
|
|
Details: err.Error(),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Build Kubernetes create options
|
|
|
|
|
|
createOptions, err := BuildK8sCreateOptions(app)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return &AppStoreError{
|
|
|
|
|
|
Code: "KUBERNETES_OPTIONS_ERROR",
|
|
|
|
|
|
Message: "Failed to build Kubernetes create options",
|
|
|
|
|
|
Details: err.Error(),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Create the application in Kubernetes
|
|
|
|
|
|
_, err = application.CreateApplication(km.ctx, k8sClient, createOptions)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return &AppStoreError{
|
|
|
|
|
|
Code: "KUBERNETES_CREATE_ERROR",
|
|
|
|
|
|
Message: "Failed to create application in Kubernetes",
|
|
|
|
|
|
Details: err.Error(),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// UninstallAppFromKubernetes uninstalls an application from a Kubernetes cluster
|
2025-11-25 14:24:05 +08:00
|
|
|
|
func (km *K8sManager) UninstallAppFromKubernetes(app *App, cred *K8sCredential) error {
|
2025-08-15 18:07:41 +08:00
|
|
|
|
// Validate that the app supports Kubernetes deployment
|
2025-09-02 11:45:50 +08:00
|
|
|
|
// 优先检查实际部署类型,如果为空则检查支持的部署类型
|
|
|
|
|
|
if app.Deploy.Type != "" && app.Deploy.Type != "kubernetes" {
|
|
|
|
|
|
return &AppStoreError{
|
|
|
|
|
|
Code: "DEPLOYMENT_TYPE_ERROR",
|
|
|
|
|
|
Message: "Application is not configured for Kubernetes deployment",
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if app.Deploy.Type == "" && app.DeploymentType != "kubernetes" && app.DeploymentType != "both" {
|
2025-08-15 18:07:41 +08:00
|
|
|
|
return &AppStoreError{
|
|
|
|
|
|
Code: "DEPLOYMENT_TYPE_ERROR",
|
|
|
|
|
|
Message: "Application does not support Kubernetes deployment",
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Get Kubernetes client
|
2025-11-25 14:24:05 +08:00
|
|
|
|
k8sClient, err := km.buildK8sClient(cred)
|
2025-08-15 18:07:41 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return &AppStoreError{
|
|
|
|
|
|
Code: "KUBERNETES_CLIENT_ERROR",
|
|
|
|
|
|
Message: "Failed to create Kubernetes client",
|
|
|
|
|
|
Details: err.Error(),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Build Kubernetes delete options
|
|
|
|
|
|
deleteOptions, err := BuildK8sDeleteOptions(app, "")
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return &AppStoreError{
|
|
|
|
|
|
Code: "KUBERNETES_OPTIONS_ERROR",
|
|
|
|
|
|
Message: "Failed to build Kubernetes delete options",
|
|
|
|
|
|
Details: err.Error(),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Delete the application from Kubernetes
|
|
|
|
|
|
err = application.DeleteApplication(km.ctx, k8sClient, deleteOptions)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return &AppStoreError{
|
|
|
|
|
|
Code: "KUBERNETES_DELETE_ERROR",
|
|
|
|
|
|
Message: "Failed to delete application from Kubernetes",
|
|
|
|
|
|
Details: err.Error(),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// GetAppFromKubernetes gets an application from a Kubernetes cluster
|
2025-11-25 14:24:05 +08:00
|
|
|
|
func (km *K8sManager) GetAppFromKubernetes(app *App, cred *K8sCredential) (interface{}, error) {
|
2025-08-15 18:07:41 +08:00
|
|
|
|
// Validate that the app supports Kubernetes deployment
|
2025-09-02 11:45:50 +08:00
|
|
|
|
// 优先检查实际部署类型,如果为空则检查支持的部署类型
|
|
|
|
|
|
if app.Deploy.Type != "" && app.Deploy.Type != "kubernetes" {
|
|
|
|
|
|
return nil, &AppStoreError{
|
|
|
|
|
|
Code: "DEPLOYMENT_TYPE_ERROR",
|
|
|
|
|
|
Message: "Application is not configured for Kubernetes deployment",
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if app.Deploy.Type == "" && app.DeploymentType != "kubernetes" && app.DeploymentType != "both" {
|
2025-08-15 18:07:41 +08:00
|
|
|
|
return nil, &AppStoreError{
|
|
|
|
|
|
Code: "DEPLOYMENT_TYPE_ERROR",
|
|
|
|
|
|
Message: "Application does not support Kubernetes deployment",
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Get Kubernetes client
|
2025-11-25 14:24:05 +08:00
|
|
|
|
k8sClient, err := km.buildK8sClient(cred)
|
2025-08-15 18:07:41 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, &AppStoreError{
|
|
|
|
|
|
Code: "KUBERNETES_CLIENT_ERROR",
|
|
|
|
|
|
Message: "Failed to create Kubernetes client",
|
|
|
|
|
|
Details: err.Error(),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Build Kubernetes get options
|
|
|
|
|
|
getOptions, err := BuildK8sGetOptions(app, "", false)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, &AppStoreError{
|
|
|
|
|
|
Code: "KUBERNETES_OPTIONS_ERROR",
|
|
|
|
|
|
Message: "Failed to build Kubernetes get options",
|
|
|
|
|
|
Details: err.Error(),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Get the application from Kubernetes
|
|
|
|
|
|
return application.GetApplication(km.ctx, k8sClient, getOptions)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ListAppsFromKubernetes lists applications from a Kubernetes cluster
|
2025-11-25 14:24:05 +08:00
|
|
|
|
func (km *K8sManager) ListAppsFromKubernetes(app *App, cred *K8sCredential) (*applicationv1.ApplicationList, error) {
|
2025-08-15 18:07:41 +08:00
|
|
|
|
// Validate that the app supports Kubernetes deployment
|
2025-09-02 11:45:50 +08:00
|
|
|
|
// 优先检查实际部署类型,如果为空则检查支持的部署类型
|
|
|
|
|
|
if app.Deploy.Type != "" && app.Deploy.Type != "kubernetes" {
|
|
|
|
|
|
return nil, &AppStoreError{
|
|
|
|
|
|
Code: "DEPLOYMENT_TYPE_ERROR",
|
|
|
|
|
|
Message: "Application is not configured for Kubernetes deployment",
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if app.Deploy.Type == "" && app.DeploymentType != "kubernetes" && app.DeploymentType != "both" {
|
2025-08-15 18:07:41 +08:00
|
|
|
|
return nil, &AppStoreError{
|
|
|
|
|
|
Code: "DEPLOYMENT_TYPE_ERROR",
|
|
|
|
|
|
Message: "Application does not support Kubernetes deployment",
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Get Kubernetes client
|
2025-11-25 14:24:05 +08:00
|
|
|
|
k8sClient, err := km.buildK8sClient(cred)
|
2025-08-15 18:07:41 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, &AppStoreError{
|
|
|
|
|
|
Code: "KUBERNETES_CLIENT_ERROR",
|
|
|
|
|
|
Message: "Failed to create Kubernetes client",
|
|
|
|
|
|
Details: err.Error(),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Build Kubernetes list options
|
|
|
|
|
|
listOptions, err := BuildK8sListOptions("", metav1.ListOptions{})
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, &AppStoreError{
|
|
|
|
|
|
Code: "KUBERNETES_OPTIONS_ERROR",
|
|
|
|
|
|
Message: "Failed to build Kubernetes list options",
|
|
|
|
|
|
Details: err.Error(),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// List applications from Kubernetes
|
|
|
|
|
|
return application.ListApplications(km.ctx, k8sClient, listOptions)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// UpdateAppInKubernetes updates an application in a Kubernetes cluster
|
2025-11-25 14:24:05 +08:00
|
|
|
|
func (km *K8sManager) UpdateAppInKubernetes(app *App, cred *K8sCredential) error {
|
2025-08-15 18:07:41 +08:00
|
|
|
|
// Validate that the app supports Kubernetes deployment
|
2025-09-02 11:45:50 +08:00
|
|
|
|
// 优先检查实际部署类型,如果为空则检查支持的部署类型
|
|
|
|
|
|
if app.Deploy.Type != "" && app.Deploy.Type != "kubernetes" {
|
|
|
|
|
|
return &AppStoreError{
|
|
|
|
|
|
Code: "DEPLOYMENT_TYPE_ERROR",
|
|
|
|
|
|
Message: "Application is not configured for Kubernetes deployment",
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if app.Deploy.Type == "" && app.DeploymentType != "kubernetes" && app.DeploymentType != "both" {
|
2025-08-15 18:07:41 +08:00
|
|
|
|
return &AppStoreError{
|
|
|
|
|
|
Code: "DEPLOYMENT_TYPE_ERROR",
|
|
|
|
|
|
Message: "Application does not support Kubernetes deployment",
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Get Kubernetes client
|
2025-11-25 14:24:05 +08:00
|
|
|
|
k8sClient, err := km.buildK8sClient(cred)
|
2025-08-15 18:07:41 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return &AppStoreError{
|
|
|
|
|
|
Code: "KUBERNETES_CLIENT_ERROR",
|
|
|
|
|
|
Message: "Failed to create Kubernetes client",
|
|
|
|
|
|
Details: err.Error(),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-02 11:45:50 +08:00
|
|
|
|
// 先获取现有的应用,以保留元数据(特别是 resourceVersion)
|
2025-11-25 14:24:05 +08:00
|
|
|
|
existingApp, err := km.GetAppFromKubernetes(app, cred)
|
2025-09-02 11:45:50 +08:00
|
|
|
|
if err != nil {
|
2025-09-07 10:34:06 +08:00
|
|
|
|
// 如果应用不存在,尝试创建新应用
|
|
|
|
|
|
log.Warn("Application not found in Kubernetes, attempting to create new application: %v", err)
|
2025-09-02 11:45:50 +08:00
|
|
|
|
return &AppStoreError{
|
|
|
|
|
|
Code: "APP_NOT_FOUND",
|
2025-09-07 10:34:06 +08:00
|
|
|
|
Message: "应用在 Kubernetes 中不存在,请先安装应用",
|
2025-09-02 11:45:50 +08:00
|
|
|
|
Details: err.Error(),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 确保获取到的是 Application 类型
|
|
|
|
|
|
appData, ok := existingApp.(*applicationv1.Application)
|
|
|
|
|
|
if !ok {
|
|
|
|
|
|
return &AppStoreError{
|
|
|
|
|
|
Code: "TYPE_ERROR",
|
|
|
|
|
|
Message: "无法获取现有应用数据",
|
|
|
|
|
|
Details: fmt.Sprintf("期望 *applicationv1.Application 类型,实际类型: %T", existingApp),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Build Kubernetes update options,传入现有应用以保留元数据
|
|
|
|
|
|
updateOptions, err := BuildK8sUpdateOptions(app, "", appData)
|
2025-08-15 18:07:41 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return &AppStoreError{
|
|
|
|
|
|
Code: "KUBERNETES_OPTIONS_ERROR",
|
|
|
|
|
|
Message: "Failed to build Kubernetes update options",
|
|
|
|
|
|
Details: err.Error(),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Update the application in Kubernetes
|
|
|
|
|
|
_, err = application.UpdateApplication(km.ctx, k8sClient, updateOptions)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return &AppStoreError{
|
|
|
|
|
|
Code: "KUBERNETES_UPDATE_ERROR",
|
|
|
|
|
|
Message: "Failed to update application in Kubernetes",
|
|
|
|
|
|
Details: err.Error(),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
2025-09-02 11:45:50 +08:00
|
|
|
|
|
|
|
|
|
|
// IsApplicationReady 封装底层就绪判断,避免上层直接依赖 k8s/application 的实现
|
|
|
|
|
|
func (km *K8sManager) IsApplicationReady(status *applicationv1.ApplicationStatus) bool {
|
|
|
|
|
|
return application.IsApplicationStatusReady(status)
|
|
|
|
|
|
}
|