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