343 lines
12 KiB
Go
343 lines
12 KiB
Go
// Package loader — загрузка YAML-спеков и построение GenResource/GenSubresource/GenAction.
|
|
//
|
|
// LoadSpecs — главная функция. Для каждого YAML:
|
|
// 1. Парсит операции (create/modify/delete/suspend/resume/...)
|
|
// 2. Классифицирует их на instance/subresource/action
|
|
// 3. Сливает параметры, вычисляет CreateOnly и ForceNew
|
|
// 4. Строит GenResource/GenSubresource/GenAction
|
|
// 5. Сортирует результат по имени сервиса
|
|
//
|
|
// ValidateSpec — fail-fast валидация (паника при неизвестном Kind).
|
|
package loader
|
|
|
|
import (
|
|
"fmt"
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
|
|
"gopkg.in/yaml.v3"
|
|
|
|
"resource-generator/internal/params"
|
|
"resource-generator/internal/types"
|
|
)
|
|
|
|
// LoadSpecs загружает все YAML-спеки из директории и строит GenResource/GenSubresource/GenAction.
|
|
func LoadSpecs(dir string) ([]types.GenResource, []types.GenSubresource, []types.GenAction, error) {
|
|
var services []types.GenResource
|
|
var subs []types.GenSubresource
|
|
var actions []types.GenAction
|
|
domainServiceIDsSet := map[int]struct{}{}
|
|
walkErr := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if d.IsDir() || !strings.HasSuffix(d.Name(), ".yaml") {
|
|
return nil
|
|
}
|
|
|
|
b, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var spec types.ServiceSpec
|
|
if err := yaml.Unmarshal(b, &spec); err != nil {
|
|
return err
|
|
}
|
|
|
|
// P1.4: fail-fast на неизвестных kind'ах и отсутствующих обязательных полях.
|
|
if err := ValidateSpec(path, &spec); err != nil {
|
|
return fmt.Errorf("%s: %w", path, err)
|
|
}
|
|
|
|
createParams := []types.Param{}
|
|
modifyParams := []types.Param{}
|
|
supportsSuspendDestroy := false
|
|
for _, op := range spec.Operations {
|
|
if op.Kind != "instance" {
|
|
continue
|
|
}
|
|
switch op.Action {
|
|
case "create":
|
|
createParams = ConvertParams(op.Params)
|
|
case "modify":
|
|
modifyParams = ConvertParams(op.Params)
|
|
case "suspend":
|
|
supportsSuspendDestroy = true
|
|
}
|
|
}
|
|
schemaParams := params.Merge(createParams, modifyParams)
|
|
createParams = params.AlignParamTypes(createParams, schemaParams)
|
|
modifyParams = params.AlignParamTypes(modifyParams, schemaParams)
|
|
createOnly := params.ComputeCreateOnly(createParams, modifyParams)
|
|
schemaParams = params.MarkCreateOnly(schemaParams, createOnly)
|
|
hasDomainParam := false
|
|
for _, param := range schemaParams {
|
|
if strings.EqualFold(strings.TrimSpace(param.Code), "domain") {
|
|
hasDomainParam = true
|
|
domainServiceIDsSet[spec.ServiceID] = struct{}{}
|
|
break
|
|
}
|
|
}
|
|
adoptExistingOnCreate := false
|
|
|
|
suspendOnDestroy := true
|
|
if spec.Lifecycle.SuspendOnDestroyDefault != nil {
|
|
suspendOnDestroy = *spec.Lifecycle.SuspendOnDestroyDefault
|
|
}
|
|
|
|
gr := types.GenResource{
|
|
Name: spec.Name,
|
|
ServiceID: spec.ServiceID,
|
|
CreateParams: createParams,
|
|
ModifyParams: modifyParams,
|
|
SchemaParams: schemaParams,
|
|
CreateOnlyParams: params.FilterCreateOnly(schemaParams),
|
|
CreateOnlyRequiredParams: params.FilterCreateOnlyRequired(schemaParams),
|
|
OutputParams: NormalizeOutputParams(spec.Outputs.Params),
|
|
HasRefSvcParams: HasRefSvcParams(schemaParams),
|
|
SupportsSuspendDestroy: supportsSuspendDestroy,
|
|
SuspendOnDestroy: suspendOnDestroy,
|
|
AdoptExistingOnCreate: adoptExistingOnCreate,
|
|
HasDomainParam: hasDomainParam,
|
|
}
|
|
params.Analyze(&gr.UsesBool, &gr.UsesInt64, &gr.UsesString, &gr.HasDefaults, &gr.NeedsBoolDefault, &gr.NeedsInt64Default, &gr.NeedsStringDefault, gr.SchemaParams)
|
|
// Анализируем nested sub-params для default-импортов
|
|
if params.AnalyzeNestedDefaults(&gr.NeedsBoolDefault, &gr.NeedsInt64Default, &gr.NeedsStringDefault, gr.SchemaParams) {
|
|
gr.NeedsFmtImport = true
|
|
}
|
|
gr.NeedsJsonPlanMod = params.AnalyzeJsonPlanMod(gr.SchemaParams)
|
|
gr.NeedsStringsImport = params.HasRestoreCasingParams(gr.SchemaParams) || params.AnalyzeNeedsStrings(gr.ModifyParams) || params.AnalyzeNeedsStrings(gr.CreateOnlyRequiredParams) || params.AnalyzeNeedsStrings(gr.CreateOnlyParams)
|
|
gr.NeedsFmtImport = params.HasNestedParams(gr.SchemaParams)
|
|
|
|
// --- Action processing (ДО append, чтобы HasRedeploy попал в слайс) ---
|
|
for _, op := range spec.Operations {
|
|
if op.Kind != "action" {
|
|
continue
|
|
}
|
|
switch op.Action {
|
|
case "redeploy":
|
|
gr.HasRedeploy = true
|
|
gr.RedeployParams = ConvertParams(op.Params)
|
|
case "restart", "recovery", "reconcile":
|
|
// Исключены
|
|
default:
|
|
act := types.GenAction{
|
|
ServiceName: spec.Name,
|
|
ServiceID: spec.ServiceID,
|
|
ActionName: op.Action,
|
|
OperationName: op.Name,
|
|
Params: ConvertParams(op.Params),
|
|
}
|
|
act.SchemaParams = act.Params
|
|
params.Analyze(&act.UsesBool, &act.UsesInt64, &act.UsesString, &act.HasDefaults, &act.NeedsBoolDefault, &act.NeedsInt64Default, &act.NeedsStringDefault, act.SchemaParams)
|
|
act.NeedsJsonPlanMod = params.AnalyzeJsonPlanMod(act.SchemaParams)
|
|
act.NeedsStringsImport = params.AnalyzeNeedsStrings(act.SchemaParams)
|
|
actions = append(actions, act)
|
|
}
|
|
}
|
|
|
|
services = append(services, gr)
|
|
|
|
srByName := map[string]*types.GenSubresource{}
|
|
for _, op := range spec.Operations {
|
|
if op.Kind != "subresource" {
|
|
continue
|
|
}
|
|
name := strings.TrimSpace(op.Subresource)
|
|
if name == "" {
|
|
continue
|
|
}
|
|
sr := srByName[name]
|
|
if sr == nil {
|
|
sr = &types.GenSubresource{ServiceName: spec.Name, ServiceID: spec.ServiceID, SubName: name}
|
|
srByName[name] = sr
|
|
}
|
|
switch op.Action {
|
|
case "create":
|
|
sr.CreateOpName = op.Name
|
|
sr.CreateParams = ConvertParams(op.Params)
|
|
case "modify":
|
|
sr.ModifyOpName = op.Name
|
|
sr.ModifyParams = ConvertParams(op.Params)
|
|
case "delete":
|
|
sr.DeleteOpName = op.Name
|
|
sr.DeleteParams = ConvertParams(op.Params)
|
|
}
|
|
}
|
|
for _, sr := range srByName {
|
|
sr.SchemaParams = params.Merge(sr.CreateParams, sr.ModifyParams, sr.DeleteParams)
|
|
sr.CreateParams = params.AlignParamTypes(sr.CreateParams, sr.SchemaParams)
|
|
sr.ModifyParams = params.AlignParamTypes(sr.ModifyParams, sr.SchemaParams)
|
|
sr.DeleteParams = params.AlignParamTypes(sr.DeleteParams, sr.SchemaParams)
|
|
createOnly := params.ComputeCreateOnly(sr.CreateParams, sr.ModifyParams)
|
|
sr.SchemaParams = params.MarkCreateOnly(sr.SchemaParams, createOnly)
|
|
forceNewCodes := params.BuildSubresourceForceNewCodes(sr, createOnly)
|
|
sr.SchemaParams = params.MarkForceNew(sr.SchemaParams, forceNewCodes)
|
|
sr.IdentityParams = params.FilterForceNew(sr.SchemaParams)
|
|
if len(sr.IdentityParams) == 0 {
|
|
sr.IdentityParams = sr.SchemaParams
|
|
}
|
|
sr.HasRefSvcParams = HasRefSvcParams(sr.SchemaParams)
|
|
params.Analyze(&sr.UsesBool, &sr.UsesInt64, &sr.UsesString, &sr.HasDefaults, &sr.NeedsBoolDefault, &sr.NeedsInt64Default, &sr.NeedsStringDefault, sr.SchemaParams)
|
|
params.AnalyzePlanModifiers(&sr.NeedsBoolPlanMod, &sr.NeedsInt64PlanMod, &sr.NeedsStringPlanMod, sr.SchemaParams)
|
|
sr.NeedsJsonPlanMod = params.AnalyzeJsonPlanMod(sr.SchemaParams)
|
|
sr.NeedsStringsImport = params.AnalyzeNeedsStrings(sr.ModifyParams)
|
|
subs = append(subs, *sr)
|
|
}
|
|
|
|
return nil
|
|
})
|
|
|
|
if walkErr != nil {
|
|
return nil, nil, nil, walkErr
|
|
}
|
|
|
|
domainServiceIDs := make([]int, 0, len(domainServiceIDsSet))
|
|
for serviceID := range domainServiceIDsSet {
|
|
domainServiceIDs = append(domainServiceIDs, serviceID)
|
|
}
|
|
sort.Ints(domainServiceIDs)
|
|
for idx := range services {
|
|
if len(domainServiceIDs) == 0 {
|
|
services[idx].DomainServiceIDs = nil
|
|
continue
|
|
}
|
|
services[idx].DomainServiceIDs = append([]int(nil), domainServiceIDs...)
|
|
}
|
|
|
|
sort.Slice(services, func(i, j int) bool { return services[i].Name < services[j].Name })
|
|
sort.Slice(subs, func(i, j int) bool {
|
|
if subs[i].ServiceName == subs[j].ServiceName {
|
|
return subs[i].SubName < subs[j].SubName
|
|
}
|
|
return subs[i].ServiceName < subs[j].ServiceName
|
|
})
|
|
sort.Slice(actions, func(i, j int) bool {
|
|
if actions[i].ServiceName == actions[j].ServiceName {
|
|
return actions[i].ActionName < actions[j].ActionName
|
|
}
|
|
return actions[i].ServiceName < actions[j].ServiceName
|
|
})
|
|
|
|
return services, subs, actions, nil
|
|
}
|
|
|
|
// ConvertParams конвертирует []ParamSpec → []Param с нормализацией типов.
|
|
func ConvertParams(params []types.ParamSpec) []types.Param {
|
|
out := make([]types.Param, 0, len(params))
|
|
for _, p := range params {
|
|
typeName := NormalizeParamType(p.DataType)
|
|
defVal := NormalizeDefault(p.Default)
|
|
ref := 0
|
|
if p.RefSvcID != nil {
|
|
ref = *p.RefSvcID
|
|
}
|
|
isNested := len(p.SubParams) > 0
|
|
// Рекурсивно конвертируем SubParams для map-fixed/array-map-fixed.
|
|
subParams := make([]types.Param, 0, len(p.SubParams))
|
|
for _, sp := range p.SubParams {
|
|
sub := ConvertParams([]types.ParamSpec{sp})
|
|
if len(sub) > 0 {
|
|
subParams = append(subParams, sub[0])
|
|
}
|
|
}
|
|
out = append(out, types.Param{
|
|
ID: p.ID,
|
|
Code: p.Code,
|
|
Type: typeName,
|
|
Required: p.Required,
|
|
Default: defVal,
|
|
RefSvcId: ref,
|
|
Descr: strings.TrimSpace(p.Descr),
|
|
Man: strings.TrimSpace(p.Man),
|
|
Sensitive: p.IsSensitive,
|
|
IsJson: strings.EqualFold(strings.TrimSpace(p.DataType), "json"),
|
|
HasSubParams: isNested,
|
|
SubParams: subParams,
|
|
})
|
|
}
|
|
return out
|
|
}
|
|
|
|
// NormalizeParamType нормализует строку data_type → bool | int64 | string | map-fixed | array-map-fixed.
|
|
func NormalizeParamType(value string) string {
|
|
v := strings.ToLower(strings.TrimSpace(value))
|
|
if v == "map-fixed" || v == "array-map-fixed" {
|
|
return v
|
|
}
|
|
if strings.Contains(v, "bool") {
|
|
return "bool"
|
|
}
|
|
if strings.Contains(v, "int") || strings.Contains(v, "number") {
|
|
return "int64"
|
|
}
|
|
return "string"
|
|
}
|
|
|
|
// NormalizeDefault нормализует значение по умолчанию в строку.
|
|
func NormalizeDefault(value interface{}) string {
|
|
if value == nil {
|
|
return ""
|
|
}
|
|
switch t := value.(type) {
|
|
case string:
|
|
return strings.TrimSpace(t)
|
|
default:
|
|
return fmt.Sprintf("%v", t)
|
|
}
|
|
}
|
|
|
|
// NormalizeOutputParams нормализует выходные параметры.
|
|
func NormalizeOutputParams(params []types.OutputParam) []types.OutputParam {
|
|
if len(params) == 0 {
|
|
return []types.OutputParam{}
|
|
}
|
|
return params
|
|
}
|
|
|
|
// HasRefSvcParams проверяет наличие ref_svc-параметров.
|
|
func HasRefSvcParams(params []types.Param) bool {
|
|
for _, p := range params {
|
|
if p.RefSvcId > 0 && strings.EqualFold(strings.TrimSpace(p.Type), "string") {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// KnownKinds — допустимые значения Kind операций.
|
|
var KnownKinds = map[string]bool{
|
|
"instance": true,
|
|
"subresource": true,
|
|
"action": true,
|
|
}
|
|
|
|
// ValidateSpec проверяет YAML-спек на обязательные поля и неизвестные kinds.
|
|
// P1.4: fail-fast — паника при неизвестном kind вместо тихого игнорирования.
|
|
func ValidateSpec(path string, spec *types.ServiceSpec) error {
|
|
if spec.Name == "" {
|
|
return fmt.Errorf("missing required field: name")
|
|
}
|
|
if spec.ServiceID <= 0 {
|
|
return fmt.Errorf("missing required field: service_id")
|
|
}
|
|
for i, op := range spec.Operations {
|
|
if op.Kind == "" {
|
|
return fmt.Errorf("operation[%d] %q: missing required field: kind", i, op.Name)
|
|
}
|
|
if !KnownKinds[op.Kind] {
|
|
return fmt.Errorf("operation[%d] %q: unknown kind %q (valid: instance, subresource, action)", i, op.Name, op.Kind)
|
|
}
|
|
if op.Action == "" {
|
|
return fmt.Errorf("operation[%d] %q (kind=%s): missing required field: action", i, op.Name, op.Kind)
|
|
}
|
|
if op.Kind == "subresource" && op.Subresource == "" {
|
|
return fmt.Errorf("operation[%d] %q (kind=subresource): missing required field: subresource", i, op.Name)
|
|
}
|
|
}
|
|
return nil
|
|
}
|