refactor(gen_v2): split into 7 files by concern (types, loader, params, writers, helpers, templates, main)
This commit is contained in:
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,176 @@
|
||||
// helpers — конвертация имён, форматирование, plan-модификаторы.
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/format"
|
||||
"os"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
func toCamel(s string) string {
|
||||
parts := strings.FieldsFunc(s, func(r rune) bool { return r == '_' || r == '-' })
|
||||
for i, p := range parts {
|
||||
if len(p) == 0 {
|
||||
continue
|
||||
}
|
||||
parts[i] = strings.ToUpper(p[:1]) + p[1:]
|
||||
}
|
||||
out := strings.Join(parts, "")
|
||||
return ensureGoIdent(out)
|
||||
}
|
||||
|
||||
func toSnake(s string) string {
|
||||
var out []rune
|
||||
for i, r := range s {
|
||||
if i > 0 && r >= 'A' && r <= 'Z' {
|
||||
out = append(out, '_')
|
||||
}
|
||||
out = append(out, rune(strings.ToLower(string(r))[0]))
|
||||
}
|
||||
return string(out)
|
||||
}
|
||||
|
||||
func paramType(p Param) string {
|
||||
switch strings.ToLower(p.Type) {
|
||||
case "bool":
|
||||
return "types.Bool"
|
||||
case "int", "int64", "number":
|
||||
return "types.Int64"
|
||||
default:
|
||||
return "types.String"
|
||||
}
|
||||
}
|
||||
|
||||
func paramDefaultExpr(p Param) string {
|
||||
if p.Default == "" {
|
||||
return ""
|
||||
}
|
||||
switch strings.ToLower(p.Type) {
|
||||
case "bool":
|
||||
val := strings.ToLower(p.Default)
|
||||
return fmt.Sprintf("booldefault.StaticBool(%s)", val)
|
||||
case "int", "int64", "number":
|
||||
return fmt.Sprintf("int64default.StaticInt64(%s)", p.Default)
|
||||
default:
|
||||
return fmt.Sprintf("stringdefault.StaticString(%q)", p.Default)
|
||||
}
|
||||
}
|
||||
|
||||
func paramFormat(p Param, varName string) string {
|
||||
switch strings.ToLower(p.Type) {
|
||||
case "bool":
|
||||
return fmt.Sprintf("resources_core.FormatBool(%s)", varName)
|
||||
case "int", "int64", "number":
|
||||
return fmt.Sprintf("resources_core.FormatInt64(%s)", varName)
|
||||
default:
|
||||
return fmt.Sprintf("resources_core.FormatString(%s)", varName)
|
||||
}
|
||||
}
|
||||
|
||||
func paramParse(p Param) string {
|
||||
switch strings.ToLower(p.Type) {
|
||||
case "bool":
|
||||
return "resources_core.ParseBool"
|
||||
case "int", "int64", "number":
|
||||
return "resources_core.ParseInt64"
|
||||
default:
|
||||
return "resources_core.ParseString"
|
||||
}
|
||||
}
|
||||
|
||||
func paramDescription(p Param) string {
|
||||
var desc string
|
||||
if strings.TrimSpace(p.Descr) != "" {
|
||||
desc = strings.TrimSpace(p.Descr)
|
||||
} else if strings.TrimSpace(p.Man) != "" {
|
||||
desc = strings.TrimSpace(p.Man)
|
||||
} else if p.RefSvcId > 0 {
|
||||
desc = fmt.Sprintf("UI display_name for service_id=%d (mapped to UUID in core)", p.RefSvcId)
|
||||
}
|
||||
if p.CreateOnly {
|
||||
if desc == "" {
|
||||
desc = "Не изменяется после создания."
|
||||
} else {
|
||||
desc = desc + " Не изменяется после создания."
|
||||
}
|
||||
}
|
||||
if p.ForceNew {
|
||||
if desc == "" {
|
||||
desc = "Изменение требует пересоздания."
|
||||
} else {
|
||||
desc = desc + " Изменение требует пересоздания."
|
||||
}
|
||||
}
|
||||
if desc == "" {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("%q", desc)
|
||||
}
|
||||
|
||||
func outputParamType(p OutputParam) string {
|
||||
switch strings.ToLower(p.Type) {
|
||||
case "map":
|
||||
return "types.Map"
|
||||
case "list":
|
||||
return "types.List"
|
||||
default:
|
||||
return "types.String"
|
||||
}
|
||||
}
|
||||
|
||||
func outputParamIsMap(p OutputParam) bool {
|
||||
return strings.EqualFold(strings.TrimSpace(p.Type), "map")
|
||||
}
|
||||
|
||||
func outputParamIsList(p OutputParam) bool {
|
||||
return strings.EqualFold(strings.TrimSpace(p.Type), "list")
|
||||
}
|
||||
|
||||
func outputParamSensitive(p OutputParam) bool {
|
||||
return p.Sensitive
|
||||
}
|
||||
|
||||
func ensureGoIdent(s string) string {
|
||||
if s == "" {
|
||||
return "R"
|
||||
}
|
||||
first := rune(s[0])
|
||||
if unicode.IsLetter(first) || first == '_' {
|
||||
return s
|
||||
}
|
||||
return "R" + s
|
||||
}
|
||||
|
||||
|
||||
func formatSourceOrWarn(filePath string, src []byte) []byte {
|
||||
formatted, err := format.Source(src)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "WARNING: gofmt failed for %s: %v — writing unformatted code\n", filePath, err)
|
||||
return src
|
||||
}
|
||||
return formatted
|
||||
}
|
||||
|
||||
func planModifierType(p Param) string {
|
||||
switch strings.ToLower(strings.TrimSpace(p.Type)) {
|
||||
case "bool":
|
||||
return "Bool"
|
||||
case "int", "int64", "number":
|
||||
return "Int64"
|
||||
default:
|
||||
return "String"
|
||||
}
|
||||
}
|
||||
|
||||
func planModifierExpr(p Param) string {
|
||||
switch strings.ToLower(strings.TrimSpace(p.Type)) {
|
||||
case "bool":
|
||||
return "boolplanmodifier.RequiresReplace()"
|
||||
case "int", "int64", "number":
|
||||
return "int64planmodifier.RequiresReplace()"
|
||||
default:
|
||||
return "stringplanmodifier.RequiresReplace()"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
// 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
|
||||
}
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
// generate_resources_v2 — главный генератор Terraform-провайдера Nubes.
|
||||
//
|
||||
// ⛔ ПОДЧИНЯЕТСЯ devops/ARCHITECTURE.md — все правила генерации прописаны там.
|
||||
//
|
||||
// Пайплайн: YAML-спеки сервисов → Go-ресурсы + registry.go.
|
||||
//
|
||||
// На вход: resources_yaml/*.yaml (сгенерированы service_spec_gen из API).
|
||||
// На выход: internal/resources_gen/*.go (CRUD-ресурсы, subresource'ы, registry).
|
||||
//
|
||||
// Три вида ресурсов:
|
||||
// - instance → nubes_{service} (основной CRUD инстанса)
|
||||
// - subresource → nubes_{service}_{sub} (пользователи, базы, ...)
|
||||
// - action → ТОЛЬКО redeploy встроен как git_revision.
|
||||
// restart/recovery/reconcile исключены (ARCHITECTURE.md).
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func main() {
|
||||
root, err := os.Getwd()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
resourcesDir := strings.TrimSpace(os.Getenv("NUBES_RESOURCES_DIR"))
|
||||
if resourcesDir == "" {
|
||||
resourcesDir = filepath.Join(root, "resources_yaml")
|
||||
}
|
||||
outDir := strings.TrimSpace(os.Getenv("NUBES_RESOURCES_GEN_DIR"))
|
||||
if outDir == "" {
|
||||
outDir = filepath.Join(root, "internal", "resources_gen")
|
||||
}
|
||||
|
||||
instanceResources, subresources, actions, err := loadSpecs(resourcesDir)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
for _, svc := range instanceResources {
|
||||
if err := writeInstanceResource(outDir, svc); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
for _, sr := range subresources {
|
||||
if err := writeSubresource(outDir, sr); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
for _, act := range actions {
|
||||
if err := writeActionResource(outDir, act); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
if err := writeRegistry(outDir, instanceResources, subresources, actions); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// 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 *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
|
||||
}
|
||||
|
||||
@@ -0,0 +1,254 @@
|
||||
// params — слияние, фильтрация, анализ параметров и ForceNew.
|
||||
package main
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func mergeParams(sets ...[]Param) []Param {
|
||||
seen := map[string]Param{}
|
||||
for _, s := range sets {
|
||||
for _, p := range s {
|
||||
key := strings.ToLower(strings.TrimSpace(p.Code))
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[key]; !ok {
|
||||
seen[key] = p
|
||||
}
|
||||
}
|
||||
}
|
||||
out := make([]Param, 0, len(seen))
|
||||
for _, p := range seen {
|
||||
out = append(out, p)
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool { return out[i].Code < out[j].Code })
|
||||
return out
|
||||
}
|
||||
|
||||
func alignParamTypes(params []Param, schema []Param) []Param {
|
||||
if len(params) == 0 || len(schema) == 0 {
|
||||
return params
|
||||
}
|
||||
codeType := make(map[string]string, len(schema))
|
||||
for _, p := range schema {
|
||||
key := strings.ToLower(strings.TrimSpace(p.Code))
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
if strings.TrimSpace(p.Type) != "" {
|
||||
codeType[key] = p.Type
|
||||
}
|
||||
}
|
||||
if len(codeType) == 0 {
|
||||
return params
|
||||
}
|
||||
|
||||
for i, p := range params {
|
||||
key := strings.ToLower(strings.TrimSpace(p.Code))
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
if t, ok := codeType[key]; ok && strings.TrimSpace(t) != "" {
|
||||
params[i].Type = t
|
||||
}
|
||||
}
|
||||
|
||||
return params
|
||||
}
|
||||
|
||||
func computeCreateOnly(createParams []Param, modifyParams []Param) map[string]struct{} {
|
||||
modifyCodes := make(map[string]struct{}, len(modifyParams))
|
||||
for _, p := range modifyParams {
|
||||
key := strings.ToLower(strings.TrimSpace(p.Code))
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
modifyCodes[key] = struct{}{}
|
||||
}
|
||||
|
||||
createOnly := make(map[string]struct{})
|
||||
for _, p := range createParams {
|
||||
key := strings.ToLower(strings.TrimSpace(p.Code))
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := modifyCodes[key]; !ok {
|
||||
createOnly[key] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
return createOnly
|
||||
}
|
||||
|
||||
func markCreateOnly(schema []Param, createOnly map[string]struct{}) []Param {
|
||||
if len(schema) == 0 || len(createOnly) == 0 {
|
||||
return schema
|
||||
}
|
||||
|
||||
for i, p := range schema {
|
||||
key := strings.ToLower(strings.TrimSpace(p.Code))
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := createOnly[key]; ok {
|
||||
schema[i].CreateOnly = true
|
||||
}
|
||||
}
|
||||
|
||||
return schema
|
||||
}
|
||||
|
||||
func filterCreateOnly(schema []Param) []Param {
|
||||
if len(schema) == 0 {
|
||||
return []Param{}
|
||||
}
|
||||
|
||||
result := make([]Param, 0, len(schema))
|
||||
for _, p := range schema {
|
||||
if p.CreateOnly {
|
||||
result = append(result, p)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func filterCreateOnlyRequired(schema []Param) []Param {
|
||||
if len(schema) == 0 {
|
||||
return []Param{}
|
||||
}
|
||||
|
||||
result := make([]Param, 0, len(schema))
|
||||
for _, p := range schema {
|
||||
if p.CreateOnly && p.Required {
|
||||
result = append(result, p)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func analyzeParams(usesBool *bool, usesInt64 *bool, usesString *bool, hasDefaults *bool, needsBoolDefault *bool, needsInt64Default *bool, needsStringDefault *bool, params []Param) {
|
||||
for _, p := range params {
|
||||
switch strings.ToLower(p.Type) {
|
||||
case "bool":
|
||||
*usesBool = true
|
||||
if p.Default != "" {
|
||||
*needsBoolDefault = true
|
||||
*hasDefaults = true
|
||||
}
|
||||
case "int", "int64", "number":
|
||||
*usesInt64 = true
|
||||
if p.Default != "" {
|
||||
*needsInt64Default = true
|
||||
*hasDefaults = true
|
||||
}
|
||||
default:
|
||||
*usesString = true
|
||||
if p.Default != "" {
|
||||
*needsStringDefault = true
|
||||
*hasDefaults = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func buildSubresourceForceNewCodes(sr *GenSubresource, createOnly map[string]struct{}) map[string]struct{} {
|
||||
forceNew := map[string]struct{}{}
|
||||
if sr.ModifyOpName == "" {
|
||||
for _, p := range sr.SchemaParams {
|
||||
key := strings.ToLower(strings.TrimSpace(p.Code))
|
||||
if key != "" {
|
||||
forceNew[key] = struct{}{}
|
||||
}
|
||||
}
|
||||
return forceNew
|
||||
}
|
||||
for key := range createOnly {
|
||||
forceNew[key] = struct{}{}
|
||||
}
|
||||
for _, p := range sr.DeleteParams {
|
||||
key := strings.ToLower(strings.TrimSpace(p.Code))
|
||||
if key != "" {
|
||||
forceNew[key] = struct{}{}
|
||||
}
|
||||
}
|
||||
return forceNew
|
||||
}
|
||||
|
||||
func markForceNew(params []Param, forceNew map[string]struct{}) []Param {
|
||||
if len(params) == 0 || len(forceNew) == 0 {
|
||||
return params
|
||||
}
|
||||
for i, p := range params {
|
||||
key := strings.ToLower(strings.TrimSpace(p.Code))
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := forceNew[key]; ok {
|
||||
params[i].ForceNew = true
|
||||
}
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
func filterForceNew(params []Param) []Param {
|
||||
result := []Param{}
|
||||
for _, p := range params {
|
||||
if p.ForceNew {
|
||||
result = append(result, p)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// analyzeJsonPlanMod возвращает true, если хотя бы один параметр является JSON-строкой.
|
||||
// Используется для условного добавления импорта planmodifier в сгенерированные файлы.
|
||||
func analyzeJsonPlanMod(params []Param) bool {
|
||||
for _, p := range params {
|
||||
if p.IsJson {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// hasRestoreCasingParams возвращает true, если есть ref_svc params с RefSvcId > 0 && != 12
|
||||
// (restore-casing блок в Create/Read/Update использует strings.EqualFold для них).
|
||||
func hasRestoreCasingParams(params []Param) bool {
|
||||
for _, p := range params {
|
||||
if p.RefSvcId > 0 && p.RefSvcId != 12 && strings.EqualFold(strings.TrimSpace(p.Type), "string") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// analyzeNeedsStrings возвращает true, если среди params есть хотя бы один строковый параметр.
|
||||
// Используется для условного добавления импорта "strings" (EqualFold в Update).
|
||||
func analyzeNeedsStrings(params []Param) bool {
|
||||
for _, p := range params {
|
||||
t := strings.ToLower(strings.TrimSpace(p.Type))
|
||||
if t != "bool" && t != "int" && t != "int64" && t != "number" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func analyzePlanModifiers(needsBool *bool, needsInt64 *bool, needsString *bool, params []Param) {
|
||||
for _, p := range params {
|
||||
if !p.ForceNew {
|
||||
continue
|
||||
}
|
||||
switch strings.ToLower(strings.TrimSpace(p.Type)) {
|
||||
case "bool":
|
||||
*needsBool = true
|
||||
case "int", "int64", "number":
|
||||
*needsInt64 = true
|
||||
default:
|
||||
*needsString = true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
// types — структуры данных для генератора Terraform-провайдера.
|
||||
package main
|
||||
|
||||
|
||||
// ─── Входные структуры (из YAML) ───────────────────────────────────────────
|
||||
|
||||
// ParamSpec — один параметр операции как он описан в YAML.
|
||||
// Соответствует cfsParam из API Nubes.
|
||||
type ParamSpec struct {
|
||||
ID int `yaml:"id"` // svcOperationCfsParamId
|
||||
Code string `yaml:"code"` // имя параметра (camelCase)
|
||||
DataType string `yaml:"data_type,omitempty"` // string | integer > 0 | boolean | map-fixed | uuid | json | ...
|
||||
Required bool `yaml:"required"` // обязательный?
|
||||
Default interface{} `yaml:"default,omitempty"` // значение по умолчанию (строка или число)
|
||||
RefSvcId *int `yaml:"ref_svc_id,omitempty"` // ссылка на другой сервис (UUID-параметр)
|
||||
Descr string `yaml:"descr,omitempty"` // описание
|
||||
Man string `yaml:"man,omitempty"` // MAN-руководство
|
||||
Sensitive bool `yaml:"is_sensitive,omitempty"` // секретный параметр?
|
||||
}
|
||||
|
||||
// OutputParam — выходной параметр сервиса (state_params, vault_secrets, ...).
|
||||
type OutputParam struct {
|
||||
Code string `yaml:"code"` // код: state_params, state_out, vault_url, ...
|
||||
Type string `yaml:"type"` // map | string | list
|
||||
Sensitive bool `yaml:"sensitive,omitempty"` // vault_secrets = true
|
||||
}
|
||||
|
||||
// OperationSpec — одна операция сервиса (create, modify, delete_user, ...).
|
||||
type OperationSpec struct {
|
||||
Name string `yaml:"name"` // имя операции (create, modify, ...)
|
||||
ID int `yaml:"id"` // svcOperationId
|
||||
Kind string `yaml:"kind"` // instance | subresource | action
|
||||
Action string `yaml:"action"` // create | modify | delete | suspend | redeploy | ...
|
||||
Subresource string `yaml:"subresource,omitempty"` // для subresource: user, database, topic, ...
|
||||
Man string `yaml:"man,omitempty"` // MAN-руководство операции
|
||||
Params []ParamSpec `yaml:"params"` // параметры операции
|
||||
}
|
||||
|
||||
// ServiceSpec — полная YAML-спецификация одного сервиса.
|
||||
type ServiceSpec struct {
|
||||
Name string `yaml:"name"` // snake_case имя (postgres, s3bucket, ...)
|
||||
ServiceID int `yaml:"service_id"` // числовой ID сервиса в Nubes
|
||||
Outputs struct {
|
||||
Params []OutputParam `yaml:"params"`
|
||||
} `yaml:"outputs"`
|
||||
Lifecycle struct {
|
||||
SuspendOnDestroyDefault *bool `yaml:"suspend_on_destroy_default"`
|
||||
AdoptExistingOnCreateDefault *bool `yaml:"adopt_existing_on_create_default"`
|
||||
} `yaml:"lifecycle"`
|
||||
Operations []OperationSpec `yaml:"operations"`
|
||||
}
|
||||
|
||||
type Param struct {
|
||||
ID int
|
||||
Code string
|
||||
Type string
|
||||
Required bool
|
||||
Default string
|
||||
RefSvcId int
|
||||
Descr string
|
||||
Man string
|
||||
Sensitive bool
|
||||
CreateOnly bool
|
||||
ForceNew bool
|
||||
// IsJson — атрибут является JSON-строкой (data_type: json в YAML-спеке).
|
||||
// При IsJson=true в схему добавляется JsonNormalize план-модификатор,
|
||||
// чтобы план и API-ответ (компактный JSON) всегда совпадали.
|
||||
IsJson bool
|
||||
}
|
||||
|
||||
type GenResource struct {
|
||||
Name string
|
||||
ServiceID int
|
||||
CreateParams []Param
|
||||
ModifyParams []Param
|
||||
SchemaParams []Param
|
||||
CreateOnlyParams []Param
|
||||
CreateOnlyRequiredParams []Param
|
||||
OutputParams []OutputParam
|
||||
HasRefSvcParams bool
|
||||
SupportsSuspendDestroy bool
|
||||
SuspendOnDestroy bool
|
||||
AdoptExistingOnCreate bool
|
||||
UsesBool bool
|
||||
UsesInt64 bool
|
||||
UsesString bool
|
||||
HasDefaults bool
|
||||
NeedsBoolDefault bool
|
||||
NeedsInt64Default bool
|
||||
NeedsStringDefault bool
|
||||
HasDomainParam bool
|
||||
DomainServiceIDs []int
|
||||
// HasRedeploy: сервис поддерживает redeploy (пересборка из git).
|
||||
// Если true — в основной ресурс добавляется поле git_revision.
|
||||
// При изменении git_revision вызывается redeploy вместо modify.
|
||||
// Правило из ARCHITECTURE.md: только redeploy включается как inline action.
|
||||
// restart, recovery, reconcile — исключены (ручные операции, только UI).
|
||||
HasRedeploy bool
|
||||
// NeedsJsonPlanMod: хотя бы один атрибут имеет data_type: json.
|
||||
// Управляет добавлением импорта planmodifier в сгенерированный файл.
|
||||
NeedsJsonPlanMod bool
|
||||
// NeedsStringsImport: есть строковые ModifyParams (нужен EqualFold в Update).
|
||||
NeedsStringsImport bool
|
||||
}
|
||||
|
||||
type GenSubresource struct {
|
||||
ServiceName string
|
||||
ServiceID int
|
||||
SubName string
|
||||
CreateOpName string
|
||||
ModifyOpName string
|
||||
DeleteOpName string
|
||||
CreateParams []Param
|
||||
ModifyParams []Param
|
||||
DeleteParams []Param
|
||||
SchemaParams []Param
|
||||
IdentityParams []Param
|
||||
HasRefSvcParams bool
|
||||
UsesBool bool
|
||||
UsesInt64 bool
|
||||
UsesString bool
|
||||
HasDefaults bool
|
||||
NeedsBoolDefault bool
|
||||
NeedsInt64Default bool
|
||||
NeedsStringDefault bool
|
||||
NeedsBoolPlanMod bool
|
||||
NeedsInt64PlanMod bool
|
||||
NeedsStringPlanMod bool
|
||||
// NeedsJsonPlanMod: хотя бы один атрибут имеет IsJson=true.
|
||||
NeedsJsonPlanMod bool
|
||||
// NeedsStringsImport: есть строковые ModifyParams (нужен EqualFold в Update).
|
||||
NeedsStringsImport bool
|
||||
}
|
||||
|
||||
type GenAction struct {
|
||||
ServiceName string
|
||||
ServiceID int
|
||||
ActionName string
|
||||
OperationName string
|
||||
Params []Param
|
||||
SchemaParams []Param
|
||||
UsesBool bool
|
||||
UsesInt64 bool
|
||||
UsesString bool
|
||||
HasDefaults bool
|
||||
NeedsBoolDefault bool
|
||||
NeedsInt64Default bool
|
||||
NeedsStringDefault bool
|
||||
// NeedsJsonPlanMod: хотя бы один атрибут имеет IsJson=true.
|
||||
NeedsJsonPlanMod bool
|
||||
// NeedsStringsImport: есть строковые SchemaParams (нужен EqualFold).
|
||||
NeedsStringsImport bool
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
// writers — генерация Go-файлов ресурсов и registry.go.
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
func writeInstanceResource(outDir string, svc GenResource) error {
|
||||
fileName := fmt.Sprintf("%d_%s_resource.go", svc.ServiceID, svc.Name)
|
||||
filePath := filepath.Join(outDir, fileName)
|
||||
|
||||
tpl, err := template.New("resource").Funcs(template.FuncMap{
|
||||
"ToCamel": toCamel,
|
||||
"ToSnake": toSnake,
|
||||
"ParamType": paramType,
|
||||
"ParamDefaultExpr": paramDefaultExpr,
|
||||
"ParamFormat": paramFormat,
|
||||
"ParamParse": paramParse,
|
||||
"ParamDescription": paramDescription,
|
||||
"OutputType": outputParamType,
|
||||
"OutputIsMap": outputParamIsMap,
|
||||
"OutputIsList": outputParamIsList,
|
||||
"OutputSensitive": outputParamSensitive,
|
||||
"bt": func() string { return "`" },
|
||||
}).Parse(instanceTemplate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := tpl.Execute(&buf, svc); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
formatted := formatSourceOrWarn(filePath, buf.Bytes())
|
||||
|
||||
return os.WriteFile(filePath, formatted, 0644)
|
||||
}
|
||||
|
||||
func writeSubresource(outDir string, sr GenSubresource) error {
|
||||
name := fmt.Sprintf("%s_%s", sr.ServiceName, sr.SubName)
|
||||
fileName := fmt.Sprintf("%d_%s_resource.go", sr.ServiceID, name)
|
||||
filePath := filepath.Join(outDir, fileName)
|
||||
|
||||
tpl, err := template.New("subresource").Funcs(template.FuncMap{
|
||||
"ToCamel": toCamel,
|
||||
"ToSnake": toSnake,
|
||||
"ParamType": paramType,
|
||||
"ParamDefaultExpr": paramDefaultExpr,
|
||||
"ParamFormat": paramFormat,
|
||||
"ParamParse": paramParse,
|
||||
"ParamDescription": paramDescription,
|
||||
"PlanModifierType": planModifierType,
|
||||
"PlanModifierExpr": planModifierExpr,
|
||||
"bt": func() string { return "`" },
|
||||
}).Parse(subresourceTemplate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := tpl.Execute(&buf, sr); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
formatted := formatSourceOrWarn(filePath, buf.Bytes())
|
||||
|
||||
return os.WriteFile(filePath, formatted, 0644)
|
||||
}
|
||||
|
||||
func writeActionResource(outDir string, act GenAction) error {
|
||||
name := fmt.Sprintf("%s_%s", act.ServiceName, act.ActionName)
|
||||
fileName := fmt.Sprintf("%d_%s_action.go", act.ServiceID, name)
|
||||
filePath := filepath.Join(outDir, fileName)
|
||||
|
||||
tpl, err := template.New("action").Funcs(template.FuncMap{
|
||||
"ToCamel": toCamel,
|
||||
"ToSnake": toSnake,
|
||||
"ParamType": paramType,
|
||||
"ParamDefaultExpr": paramDefaultExpr,
|
||||
"ParamFormat": paramFormat,
|
||||
"ParamParse": paramParse,
|
||||
"ParamDescription": paramDescription,
|
||||
"bt": func() string { return "`" },
|
||||
}).Parse(actionTemplate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := tpl.Execute(&buf, act); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
formatted := formatSourceOrWarn(filePath, buf.Bytes())
|
||||
|
||||
return os.WriteFile(filePath, formatted, 0644)
|
||||
}
|
||||
|
||||
func writeRegistry(outDir string, services []GenResource, subs []GenSubresource, actions []GenAction) error {
|
||||
var buf bytes.Buffer
|
||||
buf.WriteString("package resources_gen\n\n")
|
||||
buf.WriteString("import \"github.com/hashicorp/terraform-plugin-framework/resource\"\n\n")
|
||||
buf.WriteString("// Code generated by tools/gen_v2. DO NOT EDIT.\n")
|
||||
buf.WriteString("func AllResources() []func() resource.Resource {\n")
|
||||
buf.WriteString("\treturn []func() resource.Resource{\n")
|
||||
for _, svc := range services {
|
||||
buf.WriteString(fmt.Sprintf("\t\tNew%[1]sResource,\n", toCamel(svc.Name)))
|
||||
}
|
||||
for _, sr := range subs {
|
||||
buf.WriteString(fmt.Sprintf("\t\tNew%[1]sResource,\n", toCamel(sr.ServiceName+"_"+sr.SubName)))
|
||||
}
|
||||
for _, act := range actions {
|
||||
buf.WriteString(fmt.Sprintf("\t\tNew%[1]sResource,\n", toCamel(act.ServiceName+"_"+act.ActionName)))
|
||||
}
|
||||
buf.WriteString("\t}\n")
|
||||
buf.WriteString("}\n")
|
||||
|
||||
regPath := filepath.Join(outDir, "registry.go")
|
||||
formatted := formatSourceOrWarn(regPath, buf.Bytes())
|
||||
|
||||
return os.WriteFile(regPath, formatted, 0644)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user