Files
tf_provider/universal_rebuild/tools/gen_v2/loader.go
T

283 lines
9.4 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.
// loader — загрузка YAML-спеков и построение ресурсов.
package main
import (
"fmt"
"io/fs"
"os"
"path/filepath"
"sort"
"strings"
"gopkg.in/yaml.v3"
)
func loadSpecs(dir string) ([]GenResource, []GenSubresource, []GenAction, error) {
var services []GenResource
var subs []GenSubresource
var actions []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 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 := []Param{}
modifyParams := []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 := mergeParams(createParams, modifyParams)
createParams = alignParamTypes(createParams, schemaParams)
modifyParams = alignParamTypes(modifyParams, schemaParams)
createOnly := computeCreateOnly(createParams, modifyParams)
schemaParams = 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 := GenResource{
Name: spec.Name,
ServiceID: spec.ServiceID,
CreateParams: createParams,
ModifyParams: modifyParams,
SchemaParams: schemaParams,
CreateOnlyParams: filterCreateOnly(schemaParams),
CreateOnlyRequiredParams: filterCreateOnlyRequired(schemaParams),
OutputParams: normalizeOutputParams(spec.Outputs.Params),
HasRefSvcParams: hasRefSvcParams(schemaParams),
SupportsSuspendDestroy: supportsSuspendDestroy,
SuspendOnDestroy: suspendOnDestroy,
AdoptExistingOnCreate: adoptExistingOnCreate,
HasDomainParam: hasDomainParam,
}
analyzeParams(&gr.UsesBool, &gr.UsesInt64, &gr.UsesString, &gr.HasDefaults, &gr.NeedsBoolDefault, &gr.NeedsInt64Default, &gr.NeedsStringDefault, gr.SchemaParams)
gr.NeedsJsonPlanMod = analyzeJsonPlanMod(gr.SchemaParams)
// NeedsStringsImport = true если:
// 1) есть ref_svc params (RefSvcId > 0, != 12) — шаблон генерирует restore-casing блоки с strings.EqualFold
// 2) есть строковые ModifyParams — шаблон генерирует EqualFold в hasServiceParamChanges (Update)
// 3) есть строковые CreateOnlyParams — шаблон генерирует EqualFold в create-only validation (ModifyPlan)
// RefSvcId=12 (S3 Object Storage) НЕ генерирует restore-блок — для S3 UUID обязателен точный формат
gr.NeedsStringsImport = hasRestoreCasingParams(gr.SchemaParams) || analyzeNeedsStrings(gr.ModifyParams) || analyzeNeedsStrings(gr.CreateOnlyRequiredParams) || analyzeNeedsStrings(gr.CreateOnlyParams)
// --- Action processing (ДО append, чтобы HasRedeploy попал в слайс) ---
// Правила из ARCHITECTURE.md:
// - redeploy → встроить как git_revision в основной ресурс
// - restart, recovery, reconcile → исключить (ручные операции, только UI)
// - всё остальное → стандартный action-ресурс (на будущее, если появятся новые)
for _, op := range spec.Operations {
if op.Kind != "action" {
continue
}
switch op.Action {
case "redeploy":
gr.HasRedeploy = true
case "restart", "recovery", "reconcile":
// Исключены
default:
act := GenAction{
ServiceName: spec.Name,
ServiceID: spec.ServiceID,
ActionName: op.Action,
OperationName: op.Name,
Params: convertParams(op.Params),
}
act.SchemaParams = act.Params
analyzeParams(&act.UsesBool, &act.UsesInt64, &act.UsesString, &act.HasDefaults, &act.NeedsBoolDefault, &act.NeedsInt64Default, &act.NeedsStringDefault, act.SchemaParams)
act.NeedsJsonPlanMod = analyzeJsonPlanMod(act.SchemaParams)
act.NeedsStringsImport = analyzeNeedsStrings(act.SchemaParams)
actions = append(actions, act)
}
}
services = append(services, gr)
srByName := map[string]*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 = &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 = mergeParams(sr.CreateParams, sr.ModifyParams, sr.DeleteParams)
sr.CreateParams = alignParamTypes(sr.CreateParams, sr.SchemaParams)
sr.ModifyParams = alignParamTypes(sr.ModifyParams, sr.SchemaParams)
sr.DeleteParams = alignParamTypes(sr.DeleteParams, sr.SchemaParams)
createOnly := computeCreateOnly(sr.CreateParams, sr.ModifyParams)
sr.SchemaParams = markCreateOnly(sr.SchemaParams, createOnly)
forceNewCodes := buildSubresourceForceNewCodes(sr, createOnly)
sr.SchemaParams = markForceNew(sr.SchemaParams, forceNewCodes)
sr.IdentityParams = filterForceNew(sr.SchemaParams)
if len(sr.IdentityParams) == 0 {
sr.IdentityParams = sr.SchemaParams
}
sr.HasRefSvcParams = hasRefSvcParams(sr.SchemaParams)
analyzeParams(&sr.UsesBool, &sr.UsesInt64, &sr.UsesString, &sr.HasDefaults, &sr.NeedsBoolDefault, &sr.NeedsInt64Default, &sr.NeedsStringDefault, sr.SchemaParams)
analyzePlanModifiers(&sr.NeedsBoolPlanMod, &sr.NeedsInt64PlanMod, &sr.NeedsStringPlanMod, sr.SchemaParams)
sr.NeedsJsonPlanMod = analyzeJsonPlanMod(sr.SchemaParams)
sr.NeedsStringsImport = 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
}
func convertParams(params []ParamSpec) []Param {
out := make([]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
}
out = append(out, 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.Sensitive,
// IsJson: флаг для добавления JsonNormalize план-модификатора в схему.
IsJson: strings.EqualFold(strings.TrimSpace(p.DataType), "json"),
})
}
return out
}
func normalizeParamType(value string) string {
v := strings.ToLower(strings.TrimSpace(value))
if strings.Contains(v, "bool") {
return "bool"
}
if strings.Contains(v, "int") || strings.Contains(v, "number") {
return "int64"
}
return "string"
}
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)
}
}
func normalizeOutputParams(params []OutputParam) []OutputParam {
if len(params) == 0 {
return []OutputParam{}
}
return params
}
func hasRefSvcParams(params []Param) bool {
for _, p := range params {
if p.RefSvcId > 0 && strings.EqualFold(strings.TrimSpace(p.Type), "string") {
return true
}
}
return false
}