first-commit
This commit is contained in:
134
services/appstore/examples/nginx.json
Normal file
134
services/appstore/examples/nginx.json
Normal file
@@ -0,0 +1,134 @@
|
||||
{
|
||||
"id": "nginx",
|
||||
"name": "Nginx Web Server",
|
||||
"description": "High-performance HTTP server and reverse proxy",
|
||||
"category": "web-server",
|
||||
"tags": ["web", "proxy", "http", "server"],
|
||||
"icon": "https://nginx.org/favicon.ico",
|
||||
"author": "Nginx Inc.",
|
||||
"website": "https://nginx.org",
|
||||
"repository": "https://github.com/nginx/nginx",
|
||||
"license": "BSD-2-Clause",
|
||||
"version": "1.24.0",
|
||||
"deployment_type": "both",
|
||||
"config": {
|
||||
"schema": {
|
||||
"port": {
|
||||
"type": "int",
|
||||
"description": "Port number for the web server",
|
||||
"required": true,
|
||||
"min": 1,
|
||||
"max": 65535,
|
||||
"placeholder": "Enter port number"
|
||||
},
|
||||
"server_name": {
|
||||
"type": "string",
|
||||
"description": "Server name for virtual host",
|
||||
"required": false,
|
||||
"pattern": "^[a-zA-Z0-9.-]+$",
|
||||
"placeholder": "Enter server name"
|
||||
},
|
||||
"ssl_enabled": {
|
||||
"type": "bool",
|
||||
"description": "Enable SSL/TLS support",
|
||||
"required": false,
|
||||
"help": "Enable HTTPS support"
|
||||
},
|
||||
"log_level": {
|
||||
"type": "select",
|
||||
"description": "Logging level",
|
||||
"required": false,
|
||||
"options": ["debug", "info", "warn", "error"]
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"port": 80,
|
||||
"server_name": "localhost",
|
||||
"ssl_enabled": false,
|
||||
"log_level": "info"
|
||||
}
|
||||
},
|
||||
"requirements": {
|
||||
"min_memory": "128MB",
|
||||
"min_cpu": "1 core",
|
||||
"min_storage": "100MB",
|
||||
"os": "linux",
|
||||
"arch": "x86_64"
|
||||
},
|
||||
"deploy": {
|
||||
"type": "docker",
|
||||
"docker": {
|
||||
"image": "nginx",
|
||||
"tag": "1.24.0",
|
||||
"ports": [
|
||||
{
|
||||
"host_port": 80,
|
||||
"container_port": 80,
|
||||
"protocol": "tcp",
|
||||
"name": "http"
|
||||
},
|
||||
{
|
||||
"host_port": 443,
|
||||
"container_port": 443,
|
||||
"protocol": "tcp",
|
||||
"name": "https"
|
||||
}
|
||||
],
|
||||
"volumes": [
|
||||
{
|
||||
"host_path": "/var/log/nginx",
|
||||
"container_path": "/var/log/nginx",
|
||||
"type": "bind"
|
||||
},
|
||||
{
|
||||
"host_path": "/etc/nginx/conf.d",
|
||||
"container_path": "/etc/nginx/conf.d",
|
||||
"type": "bind"
|
||||
}
|
||||
],
|
||||
"environment": {
|
||||
"NGINX_VERSION": "1.24.0"
|
||||
},
|
||||
"resources": {
|
||||
"cpu": "500m",
|
||||
"memory": "512Mi"
|
||||
}
|
||||
},
|
||||
"kubernetes": {
|
||||
"namespace": "web-servers",
|
||||
"replicas": 2,
|
||||
"image": "nginx",
|
||||
"tag": "1.24.0",
|
||||
"ports": [
|
||||
{
|
||||
"container_port": 80,
|
||||
"protocol": "tcp",
|
||||
"name": "http"
|
||||
}
|
||||
],
|
||||
"volumes": [
|
||||
{
|
||||
"container_path": "/var/log/nginx",
|
||||
"type": "emptyDir"
|
||||
}
|
||||
],
|
||||
"environment": {
|
||||
"NGINX_VERSION": "1.24.0"
|
||||
},
|
||||
"resources": {
|
||||
"cpu": "500m",
|
||||
"memory": "512Mi"
|
||||
},
|
||||
"service": {
|
||||
"type": "ClusterIP",
|
||||
"ports": [
|
||||
{
|
||||
"container_port": 80,
|
||||
"protocol": "tcp",
|
||||
"name": "http"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
306
services/appstore/k8s_application_mapper.go
Normal file
306
services/appstore/k8s_application_mapper.go
Normal file
@@ -0,0 +1,306 @@
|
||||
package appstore
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
applicationv1 "code.gitea.io/gitea/modules/k8s/api/application/v1"
|
||||
application "code.gitea.io/gitea/modules/k8s/application"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// BuildK8sCreateOptions converts an AppStore App into CreateApplicationOptions for the Application CRD.
|
||||
// Returns error if the app doesn't contain a valid Kubernetes deployment definition.
|
||||
func BuildK8sCreateOptions(app *App) (*application.CreateApplicationOptions, error) {
|
||||
if app == nil {
|
||||
return nil, fmt.Errorf("nil app")
|
||||
}
|
||||
|
||||
// Require actual Deploy.Type to be kubernetes, parameters are in Deploy.Kubernetes
|
||||
if strings.ToLower(strings.TrimSpace(app.Deploy.Type)) != "kubernetes" {
|
||||
return nil, fmt.Errorf("deploy.type must be 'kubernetes'")
|
||||
}
|
||||
if app.Deploy.Kubernetes == nil {
|
||||
return nil, fmt.Errorf("deploy.kubernetes is required for kubernetes deployment")
|
||||
}
|
||||
|
||||
k := app.Deploy.Kubernetes
|
||||
|
||||
// Name & Namespace
|
||||
name := sanitizeName(app.ID)
|
||||
namespace := k.Namespace
|
||||
if namespace == "" {
|
||||
namespace = "default"
|
||||
}
|
||||
|
||||
// Image with optional tag
|
||||
image := k.Image
|
||||
if k.Tag != "" {
|
||||
image = fmt.Sprintf("%s:%s", k.Image, k.Tag)
|
||||
}
|
||||
|
||||
// Template Ports
|
||||
var tplPorts []applicationv1.Port
|
||||
for _, p := range k.Ports {
|
||||
portName := p.Name
|
||||
if portName == "" {
|
||||
portName = fmt.Sprintf("port-%d", p.ContainerPort)
|
||||
}
|
||||
proto := strings.ToUpper(p.Protocol)
|
||||
if proto == "" {
|
||||
proto = "TCP"
|
||||
}
|
||||
tplPorts = append(tplPorts, applicationv1.Port{
|
||||
Name: portName,
|
||||
Port: int32(p.ContainerPort),
|
||||
Protocol: proto,
|
||||
})
|
||||
}
|
||||
|
||||
// Resources
|
||||
res := applicationv1.ResourceRequirements{}
|
||||
if k.Resources != nil {
|
||||
res.CPU = k.Resources.CPU
|
||||
res.Memory = k.Resources.Memory
|
||||
}
|
||||
|
||||
// Service config (optional)
|
||||
var svc *applicationv1.ServiceConfig
|
||||
if k.Service != nil {
|
||||
svc = &applicationv1.ServiceConfig{
|
||||
Enabled: true,
|
||||
Type: k.Service.Type, // ClusterIP/NodePort/LoadBalancer/ExternalName
|
||||
}
|
||||
if len(k.Service.Ports) > 0 {
|
||||
for _, sp := range k.Service.Ports {
|
||||
portName := sp.Name
|
||||
if portName == "" {
|
||||
portName = fmt.Sprintf("svc-%d", sp.ContainerPort)
|
||||
}
|
||||
proto := strings.ToUpper(sp.Protocol)
|
||||
if proto == "" {
|
||||
proto = "TCP"
|
||||
}
|
||||
svc.Ports = append(svc.Ports, applicationv1.ServicePort{
|
||||
Name: portName,
|
||||
Port: int32(sp.ContainerPort),
|
||||
TargetPort: portName, // name-based targetPort for stability
|
||||
Protocol: proto,
|
||||
})
|
||||
}
|
||||
} else if len(tplPorts) > 0 {
|
||||
// Fallback: expose template ports
|
||||
for _, p := range tplPorts {
|
||||
svc.Ports = append(svc.Ports, applicationv1.ServicePort{
|
||||
Name: p.Name,
|
||||
Port: p.Port,
|
||||
TargetPort: p.Name,
|
||||
Protocol: p.Protocol,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Replicas
|
||||
var replicasPtr *int32
|
||||
if k.Replicas > 0 {
|
||||
r := int32(k.Replicas)
|
||||
replicasPtr = &r
|
||||
}
|
||||
|
||||
opts := &application.CreateApplicationOptions{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
Component: app.Category,
|
||||
Template: applicationv1.ApplicationTemplate{
|
||||
Image: image,
|
||||
Type: "stateless",
|
||||
Ports: tplPorts,
|
||||
},
|
||||
Replicas: replicasPtr,
|
||||
Environment: k.Environment,
|
||||
Resources: &res,
|
||||
Expose: svc != nil, // deprecated in CRD but kept for compatibility
|
||||
Service: svc,
|
||||
}
|
||||
|
||||
return opts, nil
|
||||
}
|
||||
|
||||
func sanitizeName(s string) string {
|
||||
s = strings.ToLower(strings.TrimSpace(s))
|
||||
s = strings.ReplaceAll(s, "_", "-")
|
||||
s = strings.ReplaceAll(s, " ", "-")
|
||||
return s
|
||||
}
|
||||
|
||||
// BuildK8sGetOptions builds GetApplicationOptions from an App for querying Application CRD
|
||||
func BuildK8sGetOptions(app *App, namespaceOverride string, wait bool) (*application.GetApplicationOptions, error) {
|
||||
if app == nil {
|
||||
return nil, fmt.Errorf("nil app")
|
||||
}
|
||||
if strings.ToLower(strings.TrimSpace(app.Deploy.Type)) != "kubernetes" {
|
||||
return nil, fmt.Errorf("deploy.type must be 'kubernetes'")
|
||||
}
|
||||
if app.Deploy.Kubernetes == nil {
|
||||
return nil, fmt.Errorf("deploy.kubernetes is required")
|
||||
}
|
||||
name := sanitizeName(app.ID)
|
||||
ns := firstNonEmpty(namespaceOverride, app.Deploy.Kubernetes.Namespace, "default")
|
||||
return &application.GetApplicationOptions{
|
||||
Name: name,
|
||||
Namespace: ns,
|
||||
Wait: wait,
|
||||
GetOptions: metav1.GetOptions{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// BuildK8sDeleteOptions builds DeleteApplicationOptions to remove an Application CRD
|
||||
func BuildK8sDeleteOptions(app *App, namespaceOverride string) (*application.DeleteApplicationOptions, error) {
|
||||
if app == nil {
|
||||
return nil, fmt.Errorf("nil app")
|
||||
}
|
||||
if strings.ToLower(strings.TrimSpace(app.Deploy.Type)) != "kubernetes" {
|
||||
return nil, fmt.Errorf("deploy.type must be 'kubernetes'")
|
||||
}
|
||||
if app.Deploy.Kubernetes == nil {
|
||||
return nil, fmt.Errorf("deploy.kubernetes is required")
|
||||
}
|
||||
name := sanitizeName(app.ID)
|
||||
ns := firstNonEmpty(namespaceOverride, app.Deploy.Kubernetes.Namespace, "default")
|
||||
return &application.DeleteApplicationOptions{
|
||||
Name: name,
|
||||
Namespace: ns,
|
||||
DeleteOptions: metav1.DeleteOptions{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// BuildK8sListOptions builds ListApplicationsOptions to list Applications in a namespace
|
||||
func BuildK8sListOptions(namespace string, listOpts metav1.ListOptions) (*application.ListApplicationsOptions, error) {
|
||||
ns := namespace
|
||||
if strings.TrimSpace(ns) == "" {
|
||||
ns = "default"
|
||||
}
|
||||
return &application.ListApplicationsOptions{
|
||||
Namespace: ns,
|
||||
ListOptions: listOpts,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// BuildK8sUpdateOptions builds UpdateApplicationOptions from an App and optional existing Application object
|
||||
func BuildK8sUpdateOptions(app *App, namespaceOverride string, existing *applicationv1.Application) (*application.UpdateApplicationOptions, error) {
|
||||
if app == nil {
|
||||
return nil, fmt.Errorf("nil app")
|
||||
}
|
||||
if strings.ToLower(strings.TrimSpace(app.Deploy.Type)) != "kubernetes" {
|
||||
return nil, fmt.Errorf("deploy.type must be 'kubernetes'")
|
||||
}
|
||||
if app.Deploy.Kubernetes == nil {
|
||||
return nil, fmt.Errorf("deploy.kubernetes is required")
|
||||
}
|
||||
|
||||
name := sanitizeName(app.ID)
|
||||
ns := firstNonEmpty(namespaceOverride, app.Deploy.Kubernetes.Namespace, "default")
|
||||
|
||||
// Start from existing object when provided to preserve metadata/status
|
||||
var obj applicationv1.Application
|
||||
if existing != nil {
|
||||
obj = *existing.DeepCopy()
|
||||
} else {
|
||||
obj.TypeMeta = metav1.TypeMeta{Kind: "Application", APIVersion: "application.devstar.cn/v1"}
|
||||
obj.ObjectMeta = metav1.ObjectMeta{Name: name, Namespace: ns}
|
||||
}
|
||||
|
||||
// Map spec similar to BuildK8sCreateOptions
|
||||
k := app.Deploy.Kubernetes
|
||||
image := k.Image
|
||||
if k.Tag != "" {
|
||||
image = fmt.Sprintf("%s:%s", k.Image, k.Tag)
|
||||
}
|
||||
|
||||
var tplPorts []applicationv1.Port
|
||||
for _, p := range k.Ports {
|
||||
portName := p.Name
|
||||
if portName == "" {
|
||||
portName = fmt.Sprintf("port-%d", p.ContainerPort)
|
||||
}
|
||||
proto := strings.ToUpper(p.Protocol)
|
||||
if proto == "" {
|
||||
proto = "TCP"
|
||||
}
|
||||
tplPorts = append(tplPorts, applicationv1.Port{Name: portName, Port: int32(p.ContainerPort), Protocol: proto})
|
||||
}
|
||||
|
||||
res := applicationv1.ResourceRequirements{}
|
||||
if k.Resources != nil {
|
||||
res.CPU = k.Resources.CPU
|
||||
res.Memory = k.Resources.Memory
|
||||
}
|
||||
|
||||
var svc *applicationv1.ServiceConfig
|
||||
if k.Service != nil {
|
||||
svc = &applicationv1.ServiceConfig{Enabled: true, Type: k.Service.Type}
|
||||
if len(k.Service.Ports) > 0 {
|
||||
for _, sp := range k.Service.Ports {
|
||||
portName := sp.Name
|
||||
if portName == "" {
|
||||
portName = fmt.Sprintf("svc-%d", sp.ContainerPort)
|
||||
}
|
||||
proto := strings.ToUpper(sp.Protocol)
|
||||
if proto == "" {
|
||||
proto = "TCP"
|
||||
}
|
||||
svc.Ports = append(svc.Ports, applicationv1.ServicePort{
|
||||
Name: portName,
|
||||
Port: int32(sp.ContainerPort),
|
||||
TargetPort: portName,
|
||||
Protocol: proto,
|
||||
})
|
||||
}
|
||||
} else if len(tplPorts) > 0 {
|
||||
for _, p := range tplPorts {
|
||||
svc.Ports = append(svc.Ports, applicationv1.ServicePort{
|
||||
Name: p.Name,
|
||||
Port: p.Port,
|
||||
TargetPort: p.Name,
|
||||
Protocol: p.Protocol,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var replicasPtr *int32
|
||||
if k.Replicas > 0 {
|
||||
r := int32(k.Replicas)
|
||||
replicasPtr = &r
|
||||
}
|
||||
|
||||
obj.Spec = applicationv1.ApplicationSpec{
|
||||
Template: applicationv1.ApplicationTemplate{
|
||||
Image: image,
|
||||
Type: "stateless",
|
||||
Ports: tplPorts,
|
||||
},
|
||||
Replicas: replicasPtr,
|
||||
Environment: k.Environment,
|
||||
Resources: res,
|
||||
Expose: svc != nil,
|
||||
Service: svc,
|
||||
}
|
||||
|
||||
return &application.UpdateApplicationOptions{
|
||||
Name: name,
|
||||
Namespace: ns,
|
||||
Application: &obj,
|
||||
UpdateOptions: metav1.UpdateOptions{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func firstNonEmpty(vals ...string) string {
|
||||
for _, v := range vals {
|
||||
if strings.TrimSpace(v) != "" {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
222
services/appstore/k8s_manager.go
Normal file
222
services/appstore/k8s_manager.go
Normal file
@@ -0,0 +1,222 @@
|
||||
// Copyright 2024 The Devstar Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package appstore
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
k8s "code.gitea.io/gitea/modules/k8s"
|
||||
applicationv1 "code.gitea.io/gitea/modules/k8s/api/application/v1"
|
||||
"code.gitea.io/gitea/modules/k8s/application"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// K8sManager handles Kubernetes-specific application operations
|
||||
type K8sManager struct {
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
// NewK8sManager creates a new Kubernetes manager
|
||||
func NewK8sManager(ctx context.Context) *K8sManager {
|
||||
return &K8sManager{
|
||||
ctx: ctx,
|
||||
}
|
||||
}
|
||||
|
||||
// InstallAppToKubernetes installs an application to a Kubernetes cluster
|
||||
func (km *K8sManager) InstallAppToKubernetes(app *App, kubeconfig []byte, contextName string) error {
|
||||
// Validate that the app supports Kubernetes deployment
|
||||
if app.Deploy.Type != "kubernetes" && app.DeploymentType != "kubernetes" && app.DeploymentType != "both" {
|
||||
return &AppStoreError{
|
||||
Code: "DEPLOYMENT_TYPE_ERROR",
|
||||
Message: "Application does not support Kubernetes deployment",
|
||||
}
|
||||
}
|
||||
|
||||
// Get Kubernetes client
|
||||
k8sClient, err := k8s.GetKubernetesClient(km.ctx, kubeconfig, contextName)
|
||||
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
|
||||
func (km *K8sManager) UninstallAppFromKubernetes(app *App, kubeconfig []byte, contextName string) error {
|
||||
// Validate that the app supports Kubernetes deployment
|
||||
if app.Deploy.Type != "kubernetes" && app.DeploymentType != "kubernetes" && app.DeploymentType != "both" {
|
||||
return &AppStoreError{
|
||||
Code: "DEPLOYMENT_TYPE_ERROR",
|
||||
Message: "Application does not support Kubernetes deployment",
|
||||
}
|
||||
}
|
||||
|
||||
// Get Kubernetes client
|
||||
k8sClient, err := k8s.GetKubernetesClient(km.ctx, kubeconfig, contextName)
|
||||
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
|
||||
func (km *K8sManager) GetAppFromKubernetes(app *App, kubeconfig []byte, contextName string) (interface{}, error) {
|
||||
// Validate that the app supports Kubernetes deployment
|
||||
if app.Deploy.Type != "kubernetes" && app.DeploymentType != "kubernetes" && app.DeploymentType != "both" {
|
||||
return nil, &AppStoreError{
|
||||
Code: "DEPLOYMENT_TYPE_ERROR",
|
||||
Message: "Application does not support Kubernetes deployment",
|
||||
}
|
||||
}
|
||||
|
||||
// Get Kubernetes client
|
||||
k8sClient, err := k8s.GetKubernetesClient(km.ctx, kubeconfig, contextName)
|
||||
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
|
||||
func (km *K8sManager) ListAppsFromKubernetes(app *App, kubeconfig []byte, contextName string) (*applicationv1.ApplicationList, error) {
|
||||
// Validate that the app supports Kubernetes deployment
|
||||
if app.Deploy.Type != "kubernetes" && app.DeploymentType != "kubernetes" && app.DeploymentType != "both" {
|
||||
return nil, &AppStoreError{
|
||||
Code: "DEPLOYMENT_TYPE_ERROR",
|
||||
Message: "Application does not support Kubernetes deployment",
|
||||
}
|
||||
}
|
||||
|
||||
// Get Kubernetes client
|
||||
k8sClient, err := k8s.GetKubernetesClient(km.ctx, kubeconfig, contextName)
|
||||
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
|
||||
func (km *K8sManager) UpdateAppInKubernetes(app *App, kubeconfig []byte, contextName string) error {
|
||||
// Validate that the app supports Kubernetes deployment
|
||||
if app.Deploy.Type != "kubernetes" && app.DeploymentType != "kubernetes" && app.DeploymentType != "both" {
|
||||
return &AppStoreError{
|
||||
Code: "DEPLOYMENT_TYPE_ERROR",
|
||||
Message: "Application does not support Kubernetes deployment",
|
||||
}
|
||||
}
|
||||
|
||||
// Get Kubernetes client
|
||||
k8sClient, err := k8s.GetKubernetesClient(km.ctx, kubeconfig, contextName)
|
||||
if err != nil {
|
||||
return &AppStoreError{
|
||||
Code: "KUBERNETES_CLIENT_ERROR",
|
||||
Message: "Failed to create Kubernetes client",
|
||||
Details: err.Error(),
|
||||
}
|
||||
}
|
||||
|
||||
// Build Kubernetes update options
|
||||
updateOptions, err := BuildK8sUpdateOptions(app, "", nil)
|
||||
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
|
||||
}
|
549
services/appstore/manager.go
Normal file
549
services/appstore/manager.go
Normal file
@@ -0,0 +1,549 @@
|
||||
// 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)
|
||||
}
|
488
services/appstore/parser.go
Normal file
488
services/appstore/parser.go
Normal file
@@ -0,0 +1,488 @@
|
||||
// Copyright 2024 The Devstar Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package appstore
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
)
|
||||
|
||||
// Parser handles JSON parsing and validation for app store
|
||||
type Parser struct {
|
||||
}
|
||||
|
||||
// NewParser creates a new parser instance
|
||||
func NewParser() *Parser {
|
||||
return &Parser{}
|
||||
}
|
||||
|
||||
// MergeUserConfig merges user configuration with app's default configuration
|
||||
// Returns a complete App structure with merged configuration
|
||||
func (p *Parser) MergeUserConfig(app *App, userConfig UserConfig) (*App, error) {
|
||||
if userConfig.AppID != app.ID {
|
||||
return nil, &AppStoreError{
|
||||
Code: "CONFIG_ERROR",
|
||||
Message: "User config app ID does not match app ID",
|
||||
}
|
||||
}
|
||||
|
||||
if userConfig.Version != app.Version {
|
||||
return nil, &AppStoreError{
|
||||
Code: "CONFIG_ERROR",
|
||||
Message: "User config version does not match app version",
|
||||
}
|
||||
}
|
||||
|
||||
// 创建 App 的副本,避免修改原始数据
|
||||
mergedApp := *app
|
||||
|
||||
// 只用AppConfig.Default作为默认值来源
|
||||
mergedConfig := make(map[string]interface{})
|
||||
for fieldName, defaultValue := range app.Config.Default {
|
||||
mergedConfig[fieldName] = defaultValue
|
||||
}
|
||||
|
||||
// 用户输入覆盖默认值
|
||||
for fieldName, value := range userConfig.Values {
|
||||
if field, exists := app.Config.Schema[fieldName]; exists {
|
||||
if err := p.validateConfigValue(fieldName, field, value); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mergedConfig[fieldName] = value
|
||||
} else {
|
||||
log.Warn("Unknown configuration field: %s", fieldName)
|
||||
}
|
||||
}
|
||||
|
||||
// 检查必填字段
|
||||
for fieldName, field := range app.Config.Schema {
|
||||
if field.Required {
|
||||
if _, exists := mergedConfig[fieldName]; !exists {
|
||||
return nil, &AppStoreError{
|
||||
Code: "VALIDATION_ERROR",
|
||||
Message: fmt.Sprintf("Required field missing: %s", fieldName),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 更新 App 的 Config.Default 字段为合并后的配置
|
||||
mergedApp.Config.Default = mergedConfig
|
||||
|
||||
return &mergedApp, nil
|
||||
}
|
||||
|
||||
// ParseApp 从原始 JSON 字节流解析 App
|
||||
func (p *Parser) ParseApp(jsonBytes []byte) (*App, error) {
|
||||
var app App
|
||||
if err := json.Unmarshal(jsonBytes, &app); err != nil {
|
||||
return nil, &AppStoreError{
|
||||
Code: "INVALID_JSON",
|
||||
Message: "应用JSON格式错误",
|
||||
Details: err.Error(),
|
||||
}
|
||||
}
|
||||
if err := p.validateApp(&app); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &app, nil
|
||||
}
|
||||
|
||||
// ParseAndMergeApp 解析 JSON 并将结果按规则覆盖到 baseApp 上(DB 字段保持优先,仅覆盖功能性域)
|
||||
func (p *Parser) ParseAndMergeApp(jsonBytes []byte, baseApp *App) (*App, error) {
|
||||
if baseApp == nil {
|
||||
return nil, &AppStoreError{Code: "INTERNAL", Message: "baseApp is nil"}
|
||||
}
|
||||
parsed, err := p.ParseApp(jsonBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := *baseApp
|
||||
// 仅覆盖功能域,ID/Name/Category/Version/DeploymentType 等以 DB 为准
|
||||
out.Config = parsed.Config
|
||||
out.Deploy = parsed.Deploy
|
||||
out.Requirements = parsed.Requirements
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
// ParseAndMergeUserConfig 解析用户配置JSON字符串并与应用配置合并
|
||||
func (p *Parser) ParseAndMergeUserConfig(app *App, configJSON string) (*App, error) {
|
||||
if app == nil {
|
||||
return nil, &AppStoreError{
|
||||
Code: "INTERNAL_ERROR",
|
||||
Message: "应用对象不能为空",
|
||||
}
|
||||
}
|
||||
|
||||
// 如果用户没有提供配置,直接返回应用副本
|
||||
if configJSON == "" {
|
||||
mergedApp := *app
|
||||
return &mergedApp, nil
|
||||
}
|
||||
|
||||
// 解析用户配置JSON
|
||||
var userConfigValues map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(configJSON), &userConfigValues); err != nil {
|
||||
return nil, &AppStoreError{
|
||||
Code: "CONFIG_PARSE_ERROR",
|
||||
Message: "配置JSON格式错误",
|
||||
Details: err.Error(),
|
||||
}
|
||||
}
|
||||
|
||||
// 创建应用副本
|
||||
mergedApp := *app
|
||||
|
||||
// 处理 deploy 字段
|
||||
if deployValue, exists := userConfigValues["deploy"]; exists {
|
||||
log.Info("ParseAndMergeUserConfig: found deploy field: %v", deployValue)
|
||||
if deployMap, ok := deployValue.(map[string]interface{}); ok {
|
||||
if deployType, ok := deployMap["type"].(string); ok {
|
||||
log.Info("ParseAndMergeUserConfig: setting Deploy.Type to: %s", deployType)
|
||||
mergedApp.Deploy.Type = deployType
|
||||
|
||||
// 如果选择 Kubernetes 部署,确保 Deploy.Kubernetes 字段存在
|
||||
if deployType == "kubernetes" && mergedApp.Deploy.Kubernetes == nil {
|
||||
log.Info("ParseAndMergeUserConfig: creating default Kubernetes deployment config")
|
||||
|
||||
// 尝试从 Docker 配置中复制基本信息
|
||||
var image, tag string
|
||||
if mergedApp.Deploy.Docker != nil {
|
||||
image = mergedApp.Deploy.Docker.Image
|
||||
tag = mergedApp.Deploy.Docker.Tag
|
||||
}
|
||||
|
||||
// 如果 Docker 配置也没有,使用应用名称作为默认镜像
|
||||
if image == "" {
|
||||
image = strings.ToLower(mergedApp.Name)
|
||||
}
|
||||
if tag == "" {
|
||||
tag = mergedApp.Version
|
||||
}
|
||||
|
||||
mergedApp.Deploy.Kubernetes = &K8sDeploy{
|
||||
Namespace: "default",
|
||||
Replicas: 1,
|
||||
Image: image,
|
||||
Tag: tag,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 从 userConfigValues 中移除 deploy 字段,避免被当作普通配置值处理
|
||||
delete(userConfigValues, "deploy")
|
||||
} else {
|
||||
log.Info("ParseAndMergeUserConfig: no deploy field found in user config")
|
||||
}
|
||||
|
||||
// 创建用户配置对象(只包含普通配置值)
|
||||
userConfig := UserConfig{
|
||||
AppID: app.ID,
|
||||
Version: app.Version,
|
||||
Values: userConfigValues,
|
||||
}
|
||||
|
||||
// 使用现有的合并逻辑处理普通配置值
|
||||
mergedAppWithConfig, err := p.MergeUserConfig(&mergedApp, userConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return mergedAppWithConfig, nil
|
||||
}
|
||||
|
||||
// validateApp validates an application configuration
|
||||
func (p *Parser) validateApp(app *App) error {
|
||||
if app.ID == "" {
|
||||
return &AppStoreError{
|
||||
Code: "VALIDATION_ERROR",
|
||||
Message: "App ID is required",
|
||||
}
|
||||
}
|
||||
|
||||
if app.Name == "" {
|
||||
return &AppStoreError{
|
||||
Code: "VALIDATION_ERROR",
|
||||
Message: fmt.Sprintf("App name is required for app: %s", app.ID),
|
||||
}
|
||||
}
|
||||
|
||||
if app.Category == "" {
|
||||
return &AppStoreError{
|
||||
Code: "VALIDATION_ERROR",
|
||||
Message: fmt.Sprintf("App category is required for app: %s", app.ID),
|
||||
}
|
||||
}
|
||||
|
||||
if app.Version == "" {
|
||||
return &AppStoreError{
|
||||
Code: "VALIDATION_ERROR",
|
||||
Message: fmt.Sprintf("App version is required for app: %s", app.ID),
|
||||
}
|
||||
}
|
||||
|
||||
if err := p.validateVersionFormat(app.Version); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := p.validateAppConfig(&app.Config); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := p.validateAppDeploy(&app.Deploy); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Ensure deployment_type supports the selected deploy.type
|
||||
deployType := strings.ToLower(strings.TrimSpace(app.Deploy.Type))
|
||||
deploymentType := strings.ToLower(strings.TrimSpace(app.DeploymentType))
|
||||
|
||||
// Validate deployment_type value when provided
|
||||
if deploymentType != "" {
|
||||
switch deploymentType {
|
||||
case "docker", "kubernetes", "both":
|
||||
// allowed
|
||||
default:
|
||||
return &AppStoreError{
|
||||
Code: "VALIDATION_ERROR",
|
||||
Message: fmt.Sprintf("Invalid deployment_type: %s (must be docker|kubernetes|both)", app.DeploymentType),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Default deployment_type to deploy.type if empty (backward compatibility)
|
||||
if deploymentType == "" {
|
||||
if deployType == "docker" || deployType == "kubernetes" {
|
||||
app.DeploymentType = deployType
|
||||
deploymentType = deployType
|
||||
}
|
||||
}
|
||||
|
||||
// Cross-check: deploy.type must be included by deployment_type
|
||||
switch deployType {
|
||||
case "kubernetes":
|
||||
if deploymentType != "kubernetes" && deploymentType != "both" {
|
||||
return &AppStoreError{
|
||||
Code: "VALIDATION_ERROR",
|
||||
Message: "deployment_type must include 'kubernetes' when deploy.type is 'kubernetes' (use 'kubernetes' or 'both')",
|
||||
}
|
||||
}
|
||||
case "docker":
|
||||
if deploymentType != "docker" && deploymentType != "both" {
|
||||
return &AppStoreError{
|
||||
Code: "VALIDATION_ERROR",
|
||||
Message: "deployment_type must include 'docker' when deploy.type is 'docker' (use 'docker' or 'both')",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateVersionFormat validates version string format
|
||||
func (p *Parser) validateVersionFormat(version string) error {
|
||||
// Simple version format validation (semantic versioning)
|
||||
versionRegex := regexp.MustCompile(`^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$`)
|
||||
if !versionRegex.MatchString(version) {
|
||||
return &AppStoreError{
|
||||
Code: "VALIDATION_ERROR",
|
||||
Message: fmt.Sprintf("Invalid version format: %s", version),
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateAppConfig validates application configuration schema
|
||||
func (p *Parser) validateAppConfig(config *AppConfig) error {
|
||||
if config.Schema == nil {
|
||||
return &AppStoreError{
|
||||
Code: "VALIDATION_ERROR",
|
||||
Message: "Configuration schema is required",
|
||||
}
|
||||
}
|
||||
|
||||
for fieldName, field := range config.Schema {
|
||||
if err := p.validateConfigField(fieldName, field); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateConfigField validates a configuration field
|
||||
func (p *Parser) validateConfigField(fieldName string, field ConfigField) error {
|
||||
if field.Type == "" {
|
||||
return &AppStoreError{
|
||||
Code: "VALIDATION_ERROR",
|
||||
Message: fmt.Sprintf("Field type is required for field: %s", fieldName),
|
||||
}
|
||||
}
|
||||
|
||||
validTypes := []string{"string", "int", "bool", "select"}
|
||||
validType := false
|
||||
for _, t := range validTypes {
|
||||
if field.Type == t {
|
||||
validType = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !validType {
|
||||
return &AppStoreError{
|
||||
Code: "VALIDATION_ERROR",
|
||||
Message: fmt.Sprintf("Invalid field type '%s' for field: %s", field.Type, fieldName),
|
||||
}
|
||||
}
|
||||
|
||||
if field.Type == "select" && len(field.Options) == 0 {
|
||||
return &AppStoreError{
|
||||
Code: "VALIDATION_ERROR",
|
||||
Message: fmt.Sprintf("Options are required for select field: %s", fieldName),
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateAppDeploy validates application deployment configuration
|
||||
func (p *Parser) validateAppDeploy(deploy *AppDeploy) error {
|
||||
if deploy.Type == "" {
|
||||
return &AppStoreError{
|
||||
Code: "VALIDATION_ERROR",
|
||||
Message: "Deployment type is required",
|
||||
}
|
||||
}
|
||||
|
||||
validTypes := []string{"docker", "kubernetes"}
|
||||
validType := false
|
||||
for _, t := range validTypes {
|
||||
if deploy.Type == t {
|
||||
validType = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !validType {
|
||||
return &AppStoreError{
|
||||
Code: "VALIDATION_ERROR",
|
||||
Message: fmt.Sprintf("Invalid deployment type: %s", deploy.Type),
|
||||
}
|
||||
}
|
||||
|
||||
switch deploy.Type {
|
||||
case "docker":
|
||||
if deploy.Docker == nil {
|
||||
return &AppStoreError{
|
||||
Code: "VALIDATION_ERROR",
|
||||
Message: "Docker configuration is required for docker deployment type",
|
||||
}
|
||||
}
|
||||
case "kubernetes":
|
||||
if deploy.Kubernetes == nil {
|
||||
return &AppStoreError{
|
||||
Code: "VALIDATION_ERROR",
|
||||
Message: "Kubernetes configuration is required for kubernetes deployment type",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateConfigValue validates a single configuration value
|
||||
func (p *Parser) validateConfigValue(fieldName string, field ConfigField, value interface{}) error {
|
||||
switch field.Type {
|
||||
case "string":
|
||||
if str, ok := value.(string); ok {
|
||||
if field.Pattern != "" {
|
||||
matched, err := regexp.MatchString(field.Pattern, str)
|
||||
if err != nil {
|
||||
return &AppStoreError{
|
||||
Code: "VALIDATION_ERROR",
|
||||
Message: fmt.Sprintf("Invalid regex pattern for field %s: %s", fieldName, field.Pattern),
|
||||
Details: err.Error(),
|
||||
}
|
||||
}
|
||||
if !matched {
|
||||
return &AppStoreError{
|
||||
Code: "VALIDATION_ERROR",
|
||||
Message: fmt.Sprintf("Value does not match pattern for field %s", fieldName),
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return &AppStoreError{
|
||||
Code: "VALIDATION_ERROR",
|
||||
Message: fmt.Sprintf("Field %s must be a string", fieldName),
|
||||
}
|
||||
}
|
||||
case "int":
|
||||
var intVal int
|
||||
switch v := value.(type) {
|
||||
case float64:
|
||||
intVal = int(v)
|
||||
case int:
|
||||
intVal = v
|
||||
case int64:
|
||||
intVal = int(v)
|
||||
default:
|
||||
return &AppStoreError{
|
||||
Code: "VALIDATION_ERROR",
|
||||
Message: fmt.Sprintf("Field %s must be an integer", fieldName),
|
||||
}
|
||||
}
|
||||
|
||||
if field.Min != nil {
|
||||
if min, ok := field.Min.(float64); ok && float64(intVal) < min {
|
||||
return &AppStoreError{
|
||||
Code: "VALIDATION_ERROR",
|
||||
Message: fmt.Sprintf("Field %s must be at least %v", fieldName, field.Min),
|
||||
}
|
||||
}
|
||||
}
|
||||
if field.Max != nil {
|
||||
if max, ok := field.Max.(float64); ok && float64(intVal) > max {
|
||||
return &AppStoreError{
|
||||
Code: "VALIDATION_ERROR",
|
||||
Message: fmt.Sprintf("Field %s must be at most %v", fieldName, field.Max),
|
||||
}
|
||||
}
|
||||
}
|
||||
case "bool":
|
||||
if _, ok := value.(bool); !ok {
|
||||
return &AppStoreError{
|
||||
Code: "VALIDATION_ERROR",
|
||||
Message: fmt.Sprintf("Field %s must be a boolean", fieldName),
|
||||
}
|
||||
}
|
||||
case "select":
|
||||
if str, ok := value.(string); ok {
|
||||
validOption := false
|
||||
for _, option := range field.Options {
|
||||
if option == str {
|
||||
validOption = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !validOption {
|
||||
return &AppStoreError{
|
||||
Code: "VALIDATION_ERROR",
|
||||
Message: fmt.Sprintf("Field %s must be one of: %s", fieldName, strings.Join(field.Options, ", ")),
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return &AppStoreError{
|
||||
Code: "VALIDATION_ERROR",
|
||||
Message: fmt.Sprintf("Field %s must be a string", fieldName),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
144
services/appstore/types.go
Normal file
144
services/appstore/types.go
Normal file
@@ -0,0 +1,144 @@
|
||||
// Copyright 2024 The Devstar Authors. All rights reserved.
|
||||
|
||||
package appstore
|
||||
|
||||
// App represents a single application description
|
||||
type App struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Category string `json:"category"`
|
||||
Tags []string `json:"tags"`
|
||||
Icon string `json:"icon"`
|
||||
Author string `json:"author"`
|
||||
Website string `json:"website"`
|
||||
Repository string `json:"repository"`
|
||||
License string `json:"license"`
|
||||
Version string `json:"version"`
|
||||
DeploymentType string `json:"deployment_type"` // 部署类型:docker, kubernetes, both
|
||||
Config AppConfig `json:"config"`
|
||||
Requirements AppRequirements `json:"requirements,omitempty"`
|
||||
Deploy AppDeploy `json:"deploy"`
|
||||
}
|
||||
|
||||
// AppConfig represents the configuration schema for an application
|
||||
type AppConfig struct {
|
||||
Schema map[string]ConfigField `json:"schema"`
|
||||
Default map[string]any `json:"default"`
|
||||
Description string `json:"description,omitempty"`
|
||||
}
|
||||
|
||||
// ConfigField represents a configuration field definition
|
||||
type ConfigField struct {
|
||||
Type string `json:"type"` // string, int, bool, select
|
||||
Description string `json:"description"`
|
||||
Required bool `json:"required"`
|
||||
Options []string `json:"options,omitempty"` // for select type
|
||||
Min any `json:"min,omitempty"`
|
||||
Max any `json:"max,omitempty"`
|
||||
Pattern string `json:"pattern,omitempty"` // regex pattern
|
||||
Placeholder string `json:"placeholder,omitempty"`
|
||||
Help string `json:"help,omitempty"`
|
||||
}
|
||||
|
||||
// AppRequirements represents system requirements for an application
|
||||
type AppRequirements struct {
|
||||
MinMemory string `json:"min_memory,omitempty"`
|
||||
MinCPU string `json:"min_cpu,omitempty"`
|
||||
MinStorage string `json:"min_storage,omitempty"`
|
||||
OS string `json:"os,omitempty"`
|
||||
Arch string `json:"arch,omitempty"`
|
||||
}
|
||||
|
||||
// AppDeploy represents deployment configuration
|
||||
type AppDeploy struct {
|
||||
Type string `json:"type"` // docker, kubernetes
|
||||
Docker *DockerDeploy `json:"docker,omitempty"`
|
||||
Kubernetes *K8sDeploy `json:"kubernetes,omitempty"`
|
||||
}
|
||||
|
||||
// DockerDeploy represents Docker deployment configuration
|
||||
type DockerDeploy struct {
|
||||
Image string `json:"image"`
|
||||
Tag string `json:"tag,omitempty"`
|
||||
Ports []PortMapping `json:"ports,omitempty"`
|
||||
Volumes []VolumeMapping `json:"volumes,omitempty"`
|
||||
Environment map[string]string `json:"environment,omitempty"`
|
||||
Resources *ResourceLimits `json:"resources,omitempty"`
|
||||
}
|
||||
|
||||
// K8sDeploy represents Kubernetes deployment configuration
|
||||
type K8sDeploy struct {
|
||||
Namespace string `json:"namespace,omitempty"`
|
||||
Replicas int `json:"replicas,omitempty"`
|
||||
Image string `json:"image"`
|
||||
Tag string `json:"tag,omitempty"`
|
||||
Ports []PortMapping `json:"ports,omitempty"`
|
||||
Volumes []VolumeMapping `json:"volumes,omitempty"`
|
||||
Environment map[string]string `json:"environment,omitempty"`
|
||||
Resources *ResourceLimits `json:"resources,omitempty"`
|
||||
Service *K8sService `json:"service,omitempty"`
|
||||
}
|
||||
|
||||
// PortMapping represents port mapping configuration
|
||||
type PortMapping struct {
|
||||
HostPort int `json:"host_port,omitempty"`
|
||||
ContainerPort int `json:"container_port"`
|
||||
Protocol string `json:"protocol,omitempty"` // tcp, udp
|
||||
Name string `json:"name,omitempty"`
|
||||
}
|
||||
|
||||
// VolumeMapping represents volume mapping configuration
|
||||
type VolumeMapping struct {
|
||||
HostPath string `json:"host_path,omitempty"`
|
||||
ContainerPath string `json:"container_path"`
|
||||
Type string `json:"type,omitempty"` // bind, volume, tmpfs
|
||||
ReadOnly bool `json:"read_only,omitempty"`
|
||||
}
|
||||
|
||||
// ResourceLimits represents resource limits
|
||||
type ResourceLimits struct {
|
||||
CPU string `json:"cpu,omitempty"`
|
||||
Memory string `json:"memory,omitempty"`
|
||||
}
|
||||
|
||||
// K8sService represents Kubernetes service configuration
|
||||
type K8sService struct {
|
||||
Type string `json:"type"` // ClusterIP, NodePort, LoadBalancer
|
||||
Ports []PortMapping `json:"ports"`
|
||||
}
|
||||
|
||||
// AppStoreError represents an error in the app store operations
|
||||
type AppStoreError struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Details string `json:"details,omitempty"`
|
||||
}
|
||||
|
||||
func (e *AppStoreError) Error() string {
|
||||
return e.Message
|
||||
}
|
||||
|
||||
// UserConfig represents user-provided configuration values
|
||||
type UserConfig struct {
|
||||
AppID string `json:"app_id"`
|
||||
Version string `json:"version"`
|
||||
Values map[string]interface{} `json:"values"`
|
||||
Deploy *DeployConfig `json:"deploy,omitempty"`
|
||||
}
|
||||
|
||||
// DeployConfig represents deployment configuration
|
||||
type DeployConfig struct {
|
||||
Environment string `json:"environment,omitempty"`
|
||||
Namespace string `json:"namespace,omitempty"`
|
||||
Overrides map[string]interface{} `json:"overrides,omitempty"`
|
||||
}
|
||||
|
||||
// MergedConfig represents the merged configuration (default + user values)
|
||||
type MergedConfig struct {
|
||||
AppID string `json:"app_id"`
|
||||
Version string `json:"version"`
|
||||
Config map[string]interface{} `json:"config"`
|
||||
Schema map[string]ConfigField `json:"schema"`
|
||||
Deploy *DeployConfig `json:"deploy,omitempty"`
|
||||
}
|
Reference in New Issue
Block a user