refactor: rename universal_rebuild/ → provider/

- Rename directory
- Update all 9 devops scripts
- Update 4 generator source files
- Rebuild all binaries
This commit is contained in:
“Naeel”
2026-07-05 10:20:04 +04:00
parent a9491e13ea
commit 5da22335ae
203 changed files with 1294 additions and 1270 deletions
@@ -0,0 +1,192 @@
// Package helpers — конвертация имён, форматирование, plan-модификаторы.
package helpers
import (
"fmt"
"go/format"
"os"
"strings"
"unicode"
"resource-generator/internal/types"
)
// ToCamel конвертирует snake_case → CamelCase.
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)
}
// ToSnake конвертирует CamelCase → snake_case.
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)
}
// ParamType возвращает Go-тип для параметра (types.Bool/Int64/String).
func ParamType(p types.Param) string {
switch strings.ToLower(p.Type) {
case "bool":
return "types.Bool"
case "int", "int64", "number":
return "types.Int64"
default:
return "types.String"
}
}
// ParamDefaultExpr возвращает выражение значения по умолчанию.
func ParamDefaultExpr(p types.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)
}
}
// ParamFormat возвращает вызов Format* для форматирования значения параметра.
func ParamFormat(p types.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)
}
}
// ParamParse возвращает имя Parse*-функции для параметра.
func ParamParse(p types.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"
}
}
// ParamDescription формирует MarkdownDescription для параметра.
func ParamDescription(p types.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)
}
// OutputParamType возвращает Go-тип для выходного параметра (types.Map/List/String).
func OutputParamType(p types.OutputParam) string {
switch strings.ToLower(p.Type) {
case "map":
return "types.Map"
case "list":
return "types.List"
default:
return "types.String"
}
}
// OutputParamIsMap возвращает true, если выходной параметр — map.
func OutputParamIsMap(p types.OutputParam) bool {
return strings.EqualFold(strings.TrimSpace(p.Type), "map")
}
// OutputParamIsList возвращает true, если выходной параметр — list.
func OutputParamIsList(p types.OutputParam) bool {
return strings.EqualFold(strings.TrimSpace(p.Type), "list")
}
// OutputParamSensitive возвращает true, если параметр секретный.
func OutputParamSensitive(p types.OutputParam) bool {
return p.Sensitive
}
// EnsureGoIdent проверяет, что строка начинается с буквы или _.
func EnsureGoIdent(s string) string {
if s == "" {
return "R"
}
first := rune(s[0])
if unicode.IsLetter(first) || first == '_' {
return s
}
return "R" + s
}
// FormatSourceOrWarn форматирует Go-код. При ошибке пишет warning в stderr.
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
}
// PlanModifierType возвращает суффикс типа план-модификатора (Bool/Int64/String).
func PlanModifierType(p types.Param) string {
switch strings.ToLower(strings.TrimSpace(p.Type)) {
case "bool":
return "Bool"
case "int", "int64", "number":
return "Int64"
default:
return "String"
}
}
// PlanModifierExpr возвращает выражение план-модификатора RequiresReplace.
func PlanModifierExpr(p types.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,313 @@
// Package loader — загрузка YAML-спеков и построение ресурсов.
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)
gr.NeedsJsonPlanMod = params.AnalyzeJsonPlanMod(gr.SchemaParams)
gr.NeedsStringsImport = params.HasRestoreCasingParams(gr.SchemaParams) || params.AnalyzeNeedsStrings(gr.ModifyParams) || params.AnalyzeNeedsStrings(gr.CreateOnlyRequiredParams) || params.AnalyzeNeedsStrings(gr.CreateOnlyParams)
// --- Action processing (ДО append, чтобы HasRedeploy попал в слайс) ---
for _, op := range spec.Operations {
if op.Kind != "action" {
continue
}
switch op.Action {
case "redeploy":
gr.HasRedeploy = true
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
}
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.Sensitive,
IsJson: strings.EqualFold(strings.TrimSpace(p.DataType), "json"),
})
}
return out
}
// NormalizeParamType нормализует строку data_type → bool | int64 | string.
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"
}
// 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
}
@@ -0,0 +1,263 @@
// Package params — слияние, фильтрация, анализ параметров и ForceNew.
package params
import (
"sort"
"strings"
"resource-generator/internal/types"
)
// Merge объединяет несколько наборов параметров, убирая дубликаты по Code.
func Merge(sets ...[]types.Param) []types.Param {
seen := map[string]types.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([]types.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
}
// AlignParamTypes выравнивает типы параметров по схеме.
func AlignParamTypes(params []types.Param, schema []types.Param) []types.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
}
// ComputeCreateOnly вычисляет набор полей, которые есть только в create (не в modify).
func ComputeCreateOnly(createParams []types.Param, modifyParams []types.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
}
// MarkCreateOnly проставляет CreateOnly=true для полей, которых нет в modify.
func MarkCreateOnly(schema []types.Param, createOnly map[string]struct{}) []types.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
}
// FilterCreateOnly возвращает только CreateOnly-параметры.
func FilterCreateOnly(schema []types.Param) []types.Param {
if len(schema) == 0 {
return []types.Param{}
}
result := make([]types.Param, 0, len(schema))
for _, p := range schema {
if p.CreateOnly {
result = append(result, p)
}
}
return result
}
// FilterCreateOnlyRequired возвращает обязательные CreateOnly-параметры.
func FilterCreateOnlyRequired(schema []types.Param) []types.Param {
if len(schema) == 0 {
return []types.Param{}
}
result := make([]types.Param, 0, len(schema))
for _, p := range schema {
if p.CreateOnly && p.Required {
result = append(result, p)
}
}
return result
}
// Analyze анализирует типы параметров и проставляет флаги uses/needs.
func Analyze(usesBool *bool, usesInt64 *bool, usesString *bool, hasDefaults *bool, needsBoolDefault *bool, needsInt64Default *bool, needsStringDefault *bool, params []types.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
}
}
}
}
// BuildSubresourceForceNewCodes строит карту параметров, требующих ForceNew.
func BuildSubresourceForceNewCodes(sr *types.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
}
// MarkForceNew проставляет ForceNew=true для указанных параметров.
func MarkForceNew(params []types.Param, forceNew map[string]struct{}) []types.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
}
// FilterForceNew возвращает только ForceNew-параметры.
func FilterForceNew(params []types.Param) []types.Param {
result := []types.Param{}
for _, p := range params {
if p.ForceNew {
result = append(result, p)
}
}
return result
}
// AnalyzeJsonPlanMod возвращает true, если хотя бы один параметр — JSON.
func AnalyzeJsonPlanMod(params []types.Param) bool {
for _, p := range params {
if p.IsJson {
return true
}
}
return false
}
// HasRestoreCasingParams возвращает true, если есть ref_svc-параметры (кроме S3).
func HasRestoreCasingParams(params []types.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, если есть строковые параметры.
func AnalyzeNeedsStrings(params []types.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
}
// AnalyzePlanModifiers анализирует ForceNew-параметры для добавления planmodifier-импортов.
func AnalyzePlanModifiers(needsBool *bool, needsInt64 *bool, needsString *bool, params []types.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
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,152 @@
// Package types — структуры данных для генератора Terraform-провайдера.
package types
// ─── Входные структуры (из 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"`
}
// Param — нормализованный параметр (после конвертации из ParamSpec).
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
}
// GenResource — ресурс инстанса (nubes_{service}).
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
}
// GenSubresource — подресурс (nubes_{service}_{sub}).
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 bool
NeedsStringsImport bool
}
// GenAction — action-ресурс (почти не используется, redeploy встроен в GenResource).
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 bool
NeedsStringsImport bool
}
@@ -0,0 +1,135 @@
// Package writers — генерация Go-файлов ресурсов и registry.go.
package writers
import (
"bytes"
"fmt"
"os"
"path/filepath"
"text/template"
"resource-generator/internal/helpers"
"resource-generator/internal/templates"
"resource-generator/internal/types"
)
// WriteInstanceResource генерирует nubes_{service} (CRUD инстанса).
func WriteInstanceResource(outDir string, svc types.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": helpers.ToCamel,
"ToSnake": helpers.ToSnake,
"ParamType": helpers.ParamType,
"ParamDefaultExpr": helpers.ParamDefaultExpr,
"ParamFormat": helpers.ParamFormat,
"ParamParse": helpers.ParamParse,
"ParamDescription": helpers.ParamDescription,
"OutputType": helpers.OutputParamType,
"OutputIsMap": helpers.OutputParamIsMap,
"OutputIsList": helpers.OutputParamIsList,
"OutputSensitive": helpers.OutputParamSensitive,
"bt": func() string { return "`" },
}).Parse(templates.Instance)
if err != nil {
return err
}
var buf bytes.Buffer
if err := tpl.Execute(&buf, svc); err != nil {
return err
}
formatted := helpers.FormatSourceOrWarn(filePath, buf.Bytes())
return os.WriteFile(filePath, formatted, 0644)
}
// WriteSubresource генерирует nubes_{service}_{sub} (пользователи, базы, ...).
func WriteSubresource(outDir string, sr types.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": helpers.ToCamel,
"ToSnake": helpers.ToSnake,
"ParamType": helpers.ParamType,
"ParamDefaultExpr": helpers.ParamDefaultExpr,
"ParamFormat": helpers.ParamFormat,
"ParamParse": helpers.ParamParse,
"ParamDescription": helpers.ParamDescription,
"PlanModifierType": helpers.PlanModifierType,
"PlanModifierExpr": helpers.PlanModifierExpr,
"bt": func() string { return "`" },
}).Parse(templates.Subresource)
if err != nil {
return err
}
var buf bytes.Buffer
if err := tpl.Execute(&buf, sr); err != nil {
return err
}
formatted := helpers.FormatSourceOrWarn(filePath, buf.Bytes())
return os.WriteFile(filePath, formatted, 0644)
}
// WriteActionResource генерирует action-ресурс (почти не используется).
func WriteActionResource(outDir string, act types.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": helpers.ToCamel,
"ToSnake": helpers.ToSnake,
"ParamType": helpers.ParamType,
"ParamDefaultExpr": helpers.ParamDefaultExpr,
"ParamFormat": helpers.ParamFormat,
"ParamParse": helpers.ParamParse,
"ParamDescription": helpers.ParamDescription,
"bt": func() string { return "`" },
}).Parse(templates.Action)
if err != nil {
return err
}
var buf bytes.Buffer
if err := tpl.Execute(&buf, act); err != nil {
return err
}
formatted := helpers.FormatSourceOrWarn(filePath, buf.Bytes())
return os.WriteFile(filePath, formatted, 0644)
}
// WriteRegistry генерирует registry.go со списком всех ресурсов.
func WriteRegistry(outDir string, services []types.GenResource, subs []types.GenSubresource, actions []types.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", helpers.ToCamel(svc.Name)))
}
for _, sr := range subs {
buf.WriteString(fmt.Sprintf("\t\tNew%[1]sResource,\n", helpers.ToCamel(sr.ServiceName+"_"+sr.SubName)))
}
for _, act := range actions {
buf.WriteString(fmt.Sprintf("\t\tNew%[1]sResource,\n", helpers.ToCamel(act.ServiceName+"_"+act.ActionName)))
}
buf.WriteString("\t}\n")
buf.WriteString("}\n")
regPath := filepath.Join(outDir, "registry.go")
formatted := helpers.FormatSourceOrWarn(regPath, buf.Bytes())
return os.WriteFile(regPath, formatted, 0644)
}