first-commit

This commit is contained in:
2025-08-25 15:46:12 +08:00
commit f4d95dfff4
5665 changed files with 705359 additions and 0 deletions

View 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"
}
]
}
}
}
}

View 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 ""
}

View 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
}

View 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
View 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
View 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"`
}