Files
devstar-create-from-template/services/appstore/parser.go

489 lines
13 KiB
Go
Raw Normal View History

2025-08-25 15:46:12 +08:00
// 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
}