Introduce the modifier YAML kind for delayed parent-level modify operations and generate dedicated Terraform resources with typed parameters. Keep modifier parameters out of the ordinary instance CRUD resource, register modifiers separately, and leave delete as a no-op until an inverse API payload is confirmed. Mark vcOrg and vcNsxt modify operations during YAML generation so the contract survives regeneration.
372 lines
13 KiB
Go
372 lines
13 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-спеки из директории и строит модели всех ресурсов.
|
|
func LoadSpecs(dir string) ([]types.GenResource, []types.GenSubresource, []types.GenAction, []types.GenModifier, error) {
|
|
var services []types.GenResource
|
|
var subs []types.GenSubresource
|
|
var actions []types.GenAction
|
|
var modifiers []types.GenModifier
|
|
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 == "modifier" {
|
|
name := strings.TrimSpace(op.Modifier)
|
|
if name == "" {
|
|
name = strings.TrimSpace(op.Action)
|
|
}
|
|
modifier := types.GenModifier{
|
|
ServiceName: spec.Name,
|
|
ServiceID: spec.ServiceID,
|
|
ModifierName: name,
|
|
OperationName: op.Action,
|
|
Params: ConvertParams(op.Params),
|
|
}
|
|
modifier.SchemaParams = modifier.Params
|
|
params.Analyze(&modifier.UsesBool, &modifier.UsesInt64, &modifier.UsesString, &modifier.HasDefaults, &modifier.NeedsBoolDefault, &modifier.NeedsInt64Default, &modifier.NeedsStringDefault, modifier.SchemaParams)
|
|
modifier.NeedsJsonPlanMod = params.AnalyzeJsonPlanMod(modifier.SchemaParams)
|
|
modifiers = append(modifiers, modifier)
|
|
continue
|
|
}
|
|
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, 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
|
|
})
|
|
sort.Slice(modifiers, func(i, j int) bool {
|
|
if modifiers[i].ServiceName == modifiers[j].ServiceName {
|
|
return modifiers[i].ModifierName < modifiers[j].ModifierName
|
|
}
|
|
return modifiers[i].ServiceName < modifiers[j].ServiceName
|
|
})
|
|
|
|
return services, subs, actions, modifiers, 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,
|
|
"modifier": 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, modifier)", 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)
|
|
}
|
|
if op.Kind == "modifier" && strings.TrimSpace(op.Action) == "" {
|
|
return fmt.Errorf("operation[%d] %q (kind=modifier): missing required field: action", i, op.Name)
|
|
}
|
|
}
|
|
return nil
|
|
}
|