Files
devstar-create-from-template/services/appstore/parser.go
2025-08-25 15:46:12 +08:00

489 lines
13 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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
}