diff --git a/TMP/backup_2026-09-24/freeze/22_vc_nsxt.yaml b/TMP/backup_2026-09-24/freeze/22_vc_nsxt.yaml
new file mode 100644
index 0000000..bda5137
--- /dev/null
+++ b/TMP/backup_2026-09-24/freeze/22_vc_nsxt.yaml
@@ -0,0 +1,200 @@
+name: vc_nsxt
+service_id: 22
+service_display_name: Сетевой шлюз периметра (Edge)
+service_short_name: vc_nsxt
+service_man: '# Инструкция по развертыванию Сетевой шлюз периметра (Edge) через платформу
## 1. Общая информация
Сервис **Сетевой шлюз периметра (Edge)** предназначен для создания периметрового сетевого шлюза в рамках одного виртуального датацентра (vDC) или группы виртуальных датацентров (groupvDC).
При создании автоматически разворачивается routed-сеть с адресным пространством `10.10.102.0/24`.
Сервис обеспечивает сетевую изоляцию, маршрутизацию, а также может включать функциональность балансировщика нагрузки AVI (ALB) для последующей интеграции, включая поддержку кластеров Штурвал.
### Доступные операции
**create** — Создание нового Edge с привязкой к vDC или groupvDC и развёртыванием routed-сети.
**delete** — Удаление ранее созданного Edge. Недоступно при наличии зависимых услуг (vApp, VM, Кластер Штурвал).
**modify** — Изменение параметров и сетевых настроек существующего Edge.
---
## 2. Параметры развертывания
Ниже приведены параметры операции **create**.
### Тип родительской услуги
Определяет контекст размещения Edge.
Допустимые значения: `vdc`, `groupvdc`.
Использование:
- При выборе `vdc` обязателен параметр **UUID VDC**.
- При выборе `groupvdc` обязателен параметр **UUID Группы VDC**.
### UUID VDC
Идентификатор виртуального датацентра.
Необходимо предварительно создать vDC через услугу «Виртуальный датацентр (vDC)».
Указывается только при выборе родительского типа `vdc`.
### UUID Группы VDC
Идентификатор группы виртуальных датацентров.
Создаётся через услугу «Группа виртуальных датацентров (groupvDC)».
Используется при выборе родительского типа `groupvdc`.
### Включить ALB
Флаг активации AVI Load Balancer в Cloud Director.
Пример значения: `true`/`false`.
Нужен для развёртывания кластера Штурвал и возможности создания Virtual Services.
### Наименование segroup
Имя сервиса групп (segroup) на уровне ресурсной платформы.
Обязательно при включённом ALB.
Определяет группу, в которой будут выделяться пулы AVI для Virtual Services.
### virtualServicesCount
Количество резервируемых виртуальных сервисов (Virtual Services) на AVI.
Типичное значение: 1–10 в зависимости от нагрузки.
---
## 3. Рекомендованные характеристики
### Тестовое окружение (Test)
- Тип родительской услуги: `vdc`
- virtualServicesCount: 1–2
- ALB: выключен по умолчанию (включать только при необходимости тестирования Штурвала)
- segroup: задаётся только при включённом ALB
- VDC гарантии CPU/RAM: минимальные (поскольку изменить их после создания нельзя)
### Промышленное окружение (Production)
- Тип родительской услуги: `groupvdc` при работе с распределёнными нагрузками или `vdc` для локализованных проектов
- virtualServicesCount: 3–10, исходя из предполагаемого количества публикаций
- ALB: включён, если требуется высокая доступность сервисов или используется кластер Штурвал
- segroup: рекомендуется использовать выделенную группу под проект
- VDC гарантии CPU/RAM: повышенные, с учётом того, что изменить их после создания невозможно без пересоздания vDC
---
## 4. Выходные параметры
### Имя Edge
Уникальное название созданного сетевого шлюза, используется для ссылок и дальнейших операций.
### Имя routed-сети
Автоматически созданная routed-сеть с подсетью `10.10.102.0/24`, используется для подключения ресурсов.
### Параметры Edge
Набор параметров, указанных пользователем при создании: тип услуги, ALB, segroup, virtualServicesCount и др.
Используются в операциях modify и для анализа состояния сервиса.
---
## 5. Дополнительная информация
- В рамках сервиса **один раз при создании задаётся гарантированная доля CPU/RAM vDC**. Изменить её невозможно — требуется пересоздание vDC.
- Удаление Edge недоступно при наличии зависимых ресурсов (vApp, VM, кластер Штурвал).
- При использовании ALB необходимо корректно указывать segroup, чтобы обеспечить корректное выделение пулов AVI.
- При выборе режима `groupvdc` следует учитывать распределение нагрузки между несколькими vDC.
- SNAT для всех ресурсов внутри Edge можно активировать через операцию modify (параметры выделения VIP и ipSpace).
'
+lifecycle:
+ suspend_on_destroy_default: false
+ adopt_existing_on_create_default: false
+outputs:
+ params:
+ - code: state_params
+ type: map
+ - code: state_out
+ type: map
+ - code: state_params_flat
+ type: map
+ - code: state_out_flat
+ type: map
+ - code: vault_secrets
+ type: map
+ sensitive: true
+ - code: vault_url
+ type: string
+ - code: vault_user_path
+ type: string
+ - code: vault_fields
+ type: list
+operations:
+ - name: create
+ id: 10
+ kind: instance
+ action: create
+ params:
+ - id: 8
+ code: vdcUid
+ data_type: string
+ required: false
+ ref_svc_id: 21
+ descr: UUID Услуги `Виртуальный датацентр (vDC)`
+ man: 'Необходимо, если выбран тип подключаемой VDC: `vdc`'
+ sort: 10
+ - id: 340
+ code: needEnableAVI
+ data_type: boolean
+ required: true
+ default: "false"
+ value_list:
+ - "false"
+ - "true"
+ descr: Включение Load Balancer
+ man: Параметры `Наименование segroup`, `Кол-во VS на AVI` необходимо также указать
+ sort: 30
+ is_modifiable: true
+ - id: 341
+ code: virtualServicesCount
+ data_type: integer > 0
+ required: false
+ default: "1"
+ maxvalue: 4
+ minvalue: 1
+ descr: Кол-во виртуальных сервисов, которые **резервируются** на AVI
+ man: Во избежании коллапса пока выделяется до 4
Необходимо указывать, если включён параметр `Включить ALB`
+ sort: 50
+ is_modifiable: true
+ - id: 621
+ code: vdcType
+ data_type: string
+ required: true
+ default: vdc
+ value_list:
+ - vdc
+ - vdcGroup
+ descr: Тип родительской услуги
+ man: При выборе vdc обеспечивает сетевую доступность только в рамках этого vdc
При выборе groupvdc обеспечивает сетевую доступность между всеми vdc, которые включены в groupvdc
+ sort: 0
+ - id: 622
+ code: vdcGroupUid
+ data_type: string
+ required: false
+ ref_svc_id: 29
+ descr: UUID Услуги `Группа виртуальных датацентров (groupvDC)`
+ man: 'Необходимо, если выбран тип подключаемой VDC: `groupvdc`'
+ sort: 20
+ - id: 825
+ code: qosProfile
+ required: false
+ descr: Параметр пока не работает
Должен возвращать поле из ресурсной платформы типа vc из .vcd.hardware.gatewayQoSProfiles
Как по идее должен работать.
Нужно заполнить или CFS vdcUid, или vdcGroupUid
Идеально конечно проверять `if (params.vdcType == "vdc" && params.vdcUid != "")` или `if (params.vdcType == "vdcGroup" && params.vdcGroupUid != "")`
Далее надо пойти по стейту vdc -> org -> resPlatform, взять gatewayQoSProfiles
Или пойти по стейту vdcgroup -> vdc -> org -> resPlatform, взять gatewayQoSProfiles
Регулярку могу написать (Виталя)
+ man: if (vdcUid != '") {
наборфункций1
}
elif (vdcGroupUid != "") {
наборфункций2
}
else {
return "Необходимо выбрать vdc или vdcgroup"
}
+ sort: 60
+ depends_on: vdcUid,vdcGroupUid
+ is_modifiable: true
+ - id: 1110
+ code: routedNetConfiguration
+ data_type: map-fixed
+ required: true
+ sort: 70
+ is_modifiable: true
+ sub_params:
+ - id: 649
+ code: ipAddrPool
+ data_type: string
+ required: true
+ default: 10.10.102.0/24
+ regex: ^((25[0-4]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?|0)\.){3}0/24$
+ man: '**Параметр не изменяемый**
Адрессный пул, которая будет присвоена routed-сети. Описывается как (10.10.10.0/24). Маска 24 обязательна. (.1) - шлюз. (.2-.254) - Под адресацию для ВМ'
+ is_modifiable: false
+ - id: 650
+ code: mainDns
+ data_type: string
+ required: true
+ default: 81.22.46.22
+ regex: ^((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?|0)\.){3}(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?|0)$
+ man: Подставляется автоматически в resolv.conf при создании виртуальных машин
+ is_modifiable: false
+ - id: 651
+ code: secondDns
+ data_type: string
+ required: true
+ default: 185.247.187.77
+ regex: ^((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?|0)\.){3}(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?|0)$
+ man: Подставляется автоматически в resolv.conf при создании виртуальных машин
+ is_modifiable: false
+ - name: delete
+ id: 25
+ kind: instance
+ action: delete
+ man: Невозможно удалить если routed-сеть в Edge связана с:
- vApp
- VM
- Kubernetes-кластер Штурвал
Запустить операцию возможно, но будет ошибка
+ params: []
+ - name: modify
+ id: 111
+ kind: instance
+ action: modify
+ man: Для создания SNAT Правила убедитесь, что в организации есть свободные IP
Редактировать кол-во свободных IP можно в услуге `Организация в Cloud Director` -> `modify`
+ params:
+ - id: 368
+ code: needEnableAVI
+ data_type: boolean
+ required: false
+ value_list:
+ - "false"
+ - "true"
+ descr: Включение Load Balancer
+ man: Параметры `Наименование segroup`, `Кол-во VS на AVI` необходимо также указать
+ sort: 10
+ - id: 369
+ code: virtualServicesCount
+ data_type: integer > 0
+ required: false
+ maxvalue: 4
+ minvalue: 1
+ descr: Кол-во виртуальных сервисов, которые выделяются на AVI
+ man: Во избежании коллапса пока выделяется до 4
Необходимо указывать, если включён параметр `Включить ALB`
+ sort: 30
+ - id: 372
+ code: ipSpaceName
+ data_type: string
+ required: false
+ descr: Имя ip Space для внешнего IP
+ man: Необходимо указывать, если включён параметр `Выделить VIP для SNAT`
+ sort: 50
+ - id: 856
+ code: qosProfile
+ data_type: string
+ required: false
+ sort: 40
+ - id: 1112
+ code: routedNetConfiguration
+ data_type: map-fixed
+ required: true
+ sort: 70
+ sub_params:
+ - id: 652
+ code: ipAddrPool
+ data_type: string
+ required: true
+ default: ""
+ regex: ^((25[0-4]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?|0)\.){3}0/24$
+ man: '**Параметр не изменяемый**
Адрессный пул, которая будет присвоена routed-сети. Описывается как (10.10.10.0/24). Маска 24 обязательна. (.1) - шлюз. (.2-.254) - Под адресацию для ВМ'
+ is_modifiable: false
+ - id: 653
+ code: mainDns
+ data_type: string
+ required: true
+ default: ""
+ regex: ^((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?|0)\.){3}(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?|0)$
+ man: Подставляется автоматически в resolv.conf при создании виртуальных машин
+ is_modifiable: false
+ - id: 654
+ code: secondDns
+ data_type: string
+ required: true
+ default: ""
+ regex: ^((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?|0)\.){3}(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?|0)$
+ man: Подставляется автоматически в resolv.conf при создании виртуальных машин
+ is_modifiable: false
+ - name: reconcile
+ id: 252
+ kind: action
+ action: reconcile
+ params: []
diff --git a/TMP/backup_2026-09-24/freeze/edge.tf b/TMP/backup_2026-09-24/freeze/edge.tf
new file mode 100644
index 0000000..8b8552d
--- /dev/null
+++ b/TMP/backup_2026-09-24/freeze/edge.tf
@@ -0,0 +1,20 @@
+resource "nubes_vc_nsxt" "edge" {
+ resource_name = var.nsxt_resource_name
+
+ # Тип родительской услуги: "vdc" (нужен vdc_uid) или "vdcGroup" (нужен vdc_group_uid)
+ vdc_type = var.nsxt_vdc_type
+
+ # refSvc-поле: принимает UUID или имя. Здесь берём UID созданного VDC,
+ # чтобы Edge гарантированно создавался после vDC.
+ vdc_uid = nubes_vc_vdc.vdc.id
+
+ need_enable_avi = var.nsxt_need_enable_avi
+ virtual_services_count = var.nsxt_virtual_services_count
+
+ # routed-сеть, которую разворачивает Edge (SingleNestedAttribute -> объект)
+ routed_net_configuration = {
+ ip_addr_pool = var.nsxt_ip_addr_pool
+ main_dns = var.nsxt_main_dns
+ second_dns = var.nsxt_second_dns
+ }
+}
diff --git a/TMP/backup_2026-09-24/freeze/instance.go b/TMP/backup_2026-09-24/freeze/instance.go
new file mode 100644
index 0000000..62b929c
--- /dev/null
+++ b/TMP/backup_2026-09-24/freeze/instance.go
@@ -0,0 +1,583 @@
+package templates
+
+const Instance = `package resources_gen
+
+import (
+ "context"
+ {{- if .NeedsStringsImport }}
+ "strings"
+ {{- end }}
+ {{- if .NeedsFmtImport }}
+ "fmt"
+ {{- end }}
+
+ "terraform-provider-nubes/internal/core"
+ "terraform-provider-nubes/internal/resources_core"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ {{- if .NeedsInt64Default }}
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+ {{- end }}
+ {{- if .NeedsStringDefault }}
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ {{- end }}
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ {{- if .NeedsBoolUseStateForUnknown }}
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier"
+ {{- end }}
+ {{- if .NeedsInt64UseStateForUnknown }}
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier"
+ {{- end }}
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// Code generated by TOOLS/resource-generator. DO NOT EDIT.
+// Service: {{.Name}}
+// Service ID: {{.ServiceID}}
+
+var _ resource.Resource = &{{ToCamel .Name}}Resource{}
+var _ resource.ResourceWithModifyPlan = &{{ToCamel .Name}}Resource{}
+var _ resource.ResourceWithImportState = &{{ToCamel .Name}}Resource{}
+
+type {{ToCamel .Name}}Resource struct {
+ client *core.UniversalClient
+}
+
+{{- range .SchemaParams }}
+{{- if (IsNested .) }}
+// {{NestedModelName $.Name .Code}} — вложенная модель для map-fixed параметра {{.Code}}.
+type {{NestedModelName $.Name .Code}} struct {
+ {{- range .SubParams }}
+ {{ToCamel .Code}} {{ParamType .}} ` + "`" + `tfsdk:"{{ToSnake .Code}}" json:"{{.Code}}"` + "`" + `
+ {{- end }}
+}
+{{- end }}
+{{- end }}
+
+type {{ToCamel .Name}}Model struct {
+ ID types.String ` + "`" + `tfsdk:"id"` + "`" + `
+ ResourceName types.String ` + "`" + `tfsdk:"resource_name"` + "`" + `
+ OperationTimeout types.String ` + "`" + `tfsdk:"operation_timeout"` + "`" + `
+ LogLevel types.String ` + "`" + `tfsdk:"log_level"` + "`" + `
+ {{- if .HasRedeploy }}
+ // --- Redeploy support (ARCHITECTURE.md) ---
+ // git_revision: при изменении вызывает redeploy вместо modify.
+ // Опциональное поле — если не задано, modify работает как обычно.
+ GitRevision types.String ` + "`" + `tfsdk:"git_revision"` + "`" + `
+ {{- end }}
+ {{- range .SchemaParams }}
+ {{- if (IsNested .) }}
+ {{ToCamel .Code}} {{NestedTfType . $.Name}} ` + "`" + `tfsdk:"{{ToSnake .Code}}"` + "`" + `
+ {{- else }}
+ {{ToCamel .Code}} {{ParamType .}} ` + "`" + `tfsdk:"{{ToSnake .Code}}"` + "`" + `
+ {{- end }}
+ {{- end }}
+ {{- if .SupportsSuspendDestroy }}
+ SuspendOnDestroy types.Bool ` + "`" + `tfsdk:"suspend_on_destroy"` + "`" + `
+ {{- end }}
+ AdoptExistingOnCreate types.Bool ` + "`" + `tfsdk:"adopt_existing_on_create"` + "`" + `
+ {{- range .OutputParams }}
+ {{ToCamel .Code}} {{OutputType .}} ` + "`" + `tfsdk:"{{ToSnake .Code}}"` + "`" + `
+ {{- end }}
+}
+
+func New{{ToCamel .Name}}Resource() resource.Resource {
+ return &{{ToCamel .Name}}Resource{}
+}
+
+func (r *{{ToCamel .Name}}Resource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_{{.Name}}"
+}
+
+func (r *{{ToCamel .Name}}Resource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
+ attrs := map[string]schema.Attribute{
+ "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}},
+ "resource_name": schema.StringAttribute{Required: true},
+ "operation_timeout": schema.StringAttribute{Optional: true},
+ "log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."},
+ {{- range .SchemaParams }}
+ {{- if (IsNested .) }}
+ "{{ToSnake .Code}}": {{NestedSchemaBlock .}}
+ {{- range .SubParams }}
+ "{{ToSnake .Code}}": schema.{{SubSchemaType .}}Attribute{
+ {{- if .Default }}Optional: true, Computed: true, Default: {{SubDefaultExpr .}},{{else if .Required}}Required: true,{{else}}Optional: true,{{end}}
+ {{- if ne (ParamDescription .) "" }}MarkdownDescription: {{ParamDescription .}},{{end}}
+ },
+ {{- end }}
+ {{NestedSchemaEnd .}}
+ {{- else }}
+ "{{ToSnake .Code}}": schema.{{if eq (ParamType .) "types.Bool"}}Bool{{else if eq (ParamType .) "types.Int64"}}Int64{{else}}String{{end}}Attribute{
+ {{- if and .Required (eq (ParamDefaultExpr .) "") (eq .RefSvcId 0) }}Required: true,{{else}}Optional: true,{{end}}
+ {{- if ne (ParamDefaultExpr .) "" }}Computed: true, Default: {{ParamDefaultExpr .}},{{- else if or .IsJson (ShouldBeOptionalComputed .) }}Computed: true,{{- end }}
+ {{- if ne (ParamDescription .) "" }}MarkdownDescription: {{ParamDescription .}},{{end}}
+ {{- if .Sensitive }}Sensitive: true,{{end}}
+ {{- if .IsJson }}PlanModifiers: []planmodifier.String{resources_core.JsonNormalize()},{{- else if ShouldUseStateForUnknown . }}PlanModifiers: []planmodifier.{{if eq (ParamType .) "types.Bool"}}Bool{boolplanmodifier.UseStateForUnknown()}{{else if eq (ParamType .) "types.Int64"}}Int64{int64planmodifier.UseStateForUnknown()}{{else}}String{stringplanmodifier.UseStateForUnknown()}{{end}},{{- end }}
+ },
+ {{- end }}
+ {{- end }}
+ {{- if .HasRedeploy }}
+ // --- Redeploy support (ARCHITECTURE.md) ---
+ // При изменении git_revision вызывается redeploy вместо modify.
+ // Если не задано — modify работает без изменений.
+ "git_revision": schema.StringAttribute{Optional: true, MarkdownDescription: "Git revision (commit hash/tag). Changing this triggers redeploy instead of modify."},
+ {{- end }}
+ {{- if .SupportsSuspendDestroy }}
+ "suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool({{.SuspendOnDestroy}})},
+ {{- end }}
+ "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool({{.AdoptExistingOnCreate}})},
+ {{- range .OutputParams }}
+ {{- if or (OutputIsMap .) (OutputIsList .) }}
+ "{{ToSnake .Code}}": schema.{{if OutputIsMap .}}Map{{else}}List{{end}}Attribute{Computed: true, ElementType: types.StringType{{if OutputSensitive .}}, Sensitive: true{{end}}},
+ {{- else }}
+ "{{ToSnake .Code}}": schema.StringAttribute{Computed: true{{if OutputSensitive .}}, Sensitive: true{{end}}},
+ {{- end }}
+ {{- end }}
+ }
+
+ resp.Schema = schema.Schema{Attributes: attrs}
+}
+
+func (r *{{ToCamel .Name}}Resource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ if r.client == nil {
+ return
+ }
+
+ var config *{{ToCamel .Name}}Model
+ resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if config == nil {
+ return
+ }
+
+ var state *{{ToCamel .Name}}Model
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ // Destroy-план (plan == null): create-time проверку «уже существует / adopt»
+ // запускать нельзя — удаление не валидируется через существование инстанса.
+ if req.Plan.Raw.IsNull() {
+ return
+ }
+
+ if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
+ var plan {{ToCamel .Name}}Model
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ {{- if .HasRefSvcParams }}
+ // FIX(uuid-case): resolve ref_svc params НЕ делаем в ModifyPlan.
+ // Terraform правило: plan ОБЯЗАН равняться config для user-provided атрибутов.
+ // Resolve (displayName → UUID или uppercase → lowercase) нужен только для
+ // API-вызова в Create/Update. Делать его здесь = менять plan = ошибка
+ // "Provider produced invalid plan: planned value does not match config value".
+ {{- end }}
+ if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
+ if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
+ resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
+ return
+ }
+ }
+ {{- range .CreateOnlyParams }}
+ {{- if not (IsNested .) }}
+ if !plan.{{ToCamel .Code}}.IsNull() && !plan.{{ToCamel .Code}}.IsUnknown() && !state.{{ToCamel .Code}}.IsNull() && !state.{{ToCamel .Code}}.IsUnknown() {
+ {{- if eq (ParamType .) "types.Bool" }}
+ if plan.{{ToCamel .Code}}.ValueBool() != state.{{ToCamel .Code}}.ValueBool() {
+ resp.Diagnostics.AddError("Нельзя изменить {{ToSnake .Code}}", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ {{- else if eq (ParamType .) "types.Int64" }}
+ if plan.{{ToCamel .Code}}.ValueInt64() != state.{{ToCamel .Code}}.ValueInt64() {
+ resp.Diagnostics.AddError("Нельзя изменить {{ToSnake .Code}}", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ {{- else }}
+ // FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
+ // в lowercase, пользователь мог написать upper/mixed. Это одно и то же
+ // значение, менять его нельзя только если оно реально другое.
+ if !strings.EqualFold(plan.{{ToCamel .Code}}.ValueString(), state.{{ToCamel .Code}}.ValueString()) {
+ resp.Diagnostics.AddError("Нельзя изменить {{ToSnake .Code}}", "Параметр задается при создании и не может быть изменен.")
+ return
+ }
+ {{- end }}
+ }
+ {{- end }}
+ {{- end }}
+ return
+ }
+ {{- range .SchemaParams }}
+ {{- if and .Required (eq (ParamDefaultExpr .) "") (not (IsNested .)) }}
+ if config.{{ToCamel .Code}}.IsNull() {
+ resp.Diagnostics.AddError("Missing required attribute", "{{ToSnake .Code}} is required.")
+ return
+ }
+ if config.{{ToCamel .Code}}.IsUnknown() {
+ return
+ }
+ {{- end }}
+ {{- end }}
+
+ // ⛔ Create-time проверка существования/усыновления здесь СОЗНАТЕЛЬНО НЕ вызывается.
+ //
+ // Причина: при tainted-ресурсе Terraform планирует ЗАМЕНУ (destroy+create), и
+ // create-узел замены приходит в ModifyPlan с prior state = null — ровно как у
+ // нового ресурса. Отличить «замену» от «создания» на этом уровне невозможно,
+ // поэтому проверка «РЕСУРС С ТАКИМ ИМЕНЕМ УЖЕ СУЩЕСТВУЕТ» ложно срабатывала на
+ // ещё не удалённый инстанс и блокировала plan/destroy.
+ //
+ // Проверка осталась в Create (CreateExistingResourceDiagnosticsWithDomainAndServices):
+ // на apply она выполняется ПОСЛЕ удаления старого инстанса, поэтому конфликта уже нет.
+}
+
+func (r *{{ToCamel .Name}}Resource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data {{ToCamel .Name}}Model
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ {{- range .SchemaParams }}
+ {{- if and .Required (eq (ParamDefaultExpr .) "") (not (IsNested .)) }}
+ if data.{{ToCamel .Code}}.IsNull() || data.{{ToCamel .Code}}.IsUnknown() {
+ resp.Diagnostics.AddError("Missing required attribute", "{{ToSnake .Code}} is required.")
+ return
+ }
+ {{- end }}
+ {{- end }}
+ {{- if .HasRefSvcParams }}
+ {{- range .CreateParams }}
+ {{- if and (gt .RefSvcId 0) (eq (ParamType .) "types.String") }}
+ // refSvc-поле резолвим только для API-запроса.
+ // config не перезаписываем: пользовательский display name или UUID должен
+ // пройти в state ровно в том виде, в котором его передал Terraform.
+ resolved{{ToCamel .Code}} := data.{{ToCamel .Code}}.ValueString()
+ if !data.{{ToCamel .Code}}.IsNull() && !data.{{ToCamel .Code}}.IsUnknown() {
+ var err error
+ resolved{{ToCamel .Code}}, err = r.client.ResolveRefSvcParamValue(ctx, {{.RefSvcId}}, data.{{ToCamel .Code}}.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ }
+ {{- end }}
+ {{- end }}
+ {{- end }}
+ resourceName := data.ResourceName.ValueString()
+ desiredDomain := ""
+ {{- if .HasDomainParam }}
+ if !data.Domain.IsNull() && !data.Domain.IsUnknown() {
+ desiredDomain = data.Domain.ValueString()
+ }
+ {{- end }}
+ domainServiceIDs := []int{ {{- range .DomainServiceIDs }}{{.}}, {{- end }} }
+ resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, {{.ServiceID}}, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs, {{.SupportsSuspendDestroy}})...)
+ // ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
+ // not created, конфликт) сайд-эффект create не должен выполняться.
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ params := map[int]string{
+ {{- range .CreateParams }}
+ {{- if not (IsNested .) }}
+ {{- if and (gt .RefSvcId 0) (eq (ParamType .) "types.String") }}
+ {{.ID}}: resolved{{ToCamel .Code}},
+ {{- else }}
+ {{.ID}}: {{ParamFormat . (printf "data.%s" (ToCamel .Code))}},
+ {{- end }}
+ {{- end }}
+ {{- end }}
+ }
+ {{- range .CreateParams }}
+ {{- if (IsNested .) }}
+ if data.{{ToCamel .Code}} != nil {
+ params[{{.ID}}] = {{NestedJSONExpr . "data"}}
+ }
+ {{- end }}
+ {{- end }}
+
+ operationTimeout := ""
+ if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
+ operationTimeout = data.OperationTimeout.ValueString()
+ }
+ if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
+ }
+ id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, {{.ServiceID}}, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ data.ID = types.StringValue(id)
+
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, id, {{.ServiceID}}, data, []resources_core.StateField{
+ {{- range .OutputParams }}
+ {Code: "{{.Code}}"},
+ {{- end }}
+ }, []resources_core.InputField{
+ {{- range .SchemaParams }}
+ {{- if eq .RefSvcId 0 }}
+ {Code: "{{.Code}}", Field: "{{ToCamel .Code}}", Type: "{{.Type}}"},
+ {{- end }}
+ {{- end }}
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *{{ToCamel .Name}}Resource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state {{ToCamel .Name}}Model
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+ if r.client != nil {
+ remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ if remove {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ }
+
+ newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), {{.ServiceID}}, state, []resources_core.StateField{
+ {{- range .OutputParams }}
+ {Code: "{{.Code}}"},
+ {{- end }}
+ }, []resources_core.InputField{
+ {{- range .SchemaParams }}
+ {{- if eq .RefSvcId 0 }}
+ {Code: "{{.Code}}", Field: "{{ToCamel .Code}}", Type: "{{.Type}}"},
+ {{- end }}
+ {{- end }}
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
+}
+
+func (r *{{ToCamel .Name}}Resource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan {{ToCamel .Name}}Model
+ var state {{ToCamel .Name}}Model
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ instanceID := state.ID
+ if instanceID.IsNull() || instanceID.IsUnknown() {
+ instanceID = plan.ID
+ }
+ if instanceID.IsNull() || instanceID.IsUnknown() {
+ resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
+ return
+ }
+
+ hasServiceParamChanges := false
+ {{- range .ModifyParams }}
+ {{- if not (IsNested .) }}
+ if !hasServiceParamChanges {
+ if plan.{{ToCamel .Code}}.IsNull() != state.{{ToCamel .Code}}.IsNull() || plan.{{ToCamel .Code}}.IsUnknown() != state.{{ToCamel .Code}}.IsUnknown() {
+ hasServiceParamChanges = true
+ } else if !plan.{{ToCamel .Code}}.IsNull() && !plan.{{ToCamel .Code}}.IsUnknown() && !state.{{ToCamel .Code}}.IsNull() && !state.{{ToCamel .Code}}.IsUnknown() {
+ {{- if eq (ParamType .) "types.Bool" }}
+ if plan.{{ToCamel .Code}}.ValueBool() != state.{{ToCamel .Code}}.ValueBool() {
+ hasServiceParamChanges = true
+ }
+ {{- else if eq (ParamType .) "types.Int64" }}
+ if plan.{{ToCamel .Code}}.ValueInt64() != state.{{ToCamel .Code}}.ValueInt64() {
+ hasServiceParamChanges = true
+ }
+ {{- else }}
+ if !strings.EqualFold(plan.{{ToCamel .Code}}.ValueString(), state.{{ToCamel .Code}}.ValueString()) {
+ hasServiceParamChanges = true
+ }
+ {{- end }}
+ }
+ }
+ {{- end }}
+ {{- end }}
+
+ {{- if .HasRedeploy }}
+ // --- Redeploy support (ARCHITECTURE.md) ---
+ // Проверяем изменился ли git_revision.
+ // Если да — вызываем redeploy вместо modify.
+ // git_revision = "" (не задано) → обычный modify.
+ redeployRequested := false
+ if !plan.GitRevision.IsNull() && !plan.GitRevision.IsUnknown() {
+ if state.GitRevision.IsNull() || state.GitRevision.IsUnknown() || plan.GitRevision.ValueString() != state.GitRevision.ValueString() {
+ redeployRequested = true
+ }
+ }
+ {{- end }}
+
+ if !hasServiceParamChanges{{if .HasRedeploy}} && !redeployRequested{{end}} {
+ // Сервисные параметры не изменились → modify НЕ вызываем.
+ //
+ // НО read-back поля (state_params*, state_out*, vault_*) обязаны быть
+ // перечитаны из инстанса. Раньше здесь слепо копировались значения из
+ // старого state, из-за чего tfstate хранил устаревшее значение:
+ // пример — state_params["needEnableAVI"]="false", когда на платформе уже
+ // true (модификатор включил ALB). Это давало ложный дрейф
+ // «Objects have changed outside of Terraform» на каждом plan.
+ plan.ID = instanceID
+ refreshed, refreshDiags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), {{.ServiceID}}, plan, []resources_core.StateField{
+ {{- range .OutputParams }}
+ {Code: "{{.Code}}"},
+ {{- end }}
+ }, []resources_core.InputField{
+ {{- range .SchemaParams }}
+ {{- if eq .RefSvcId 0 }}
+ {Code: "{{.Code}}", Field: "{{ToCamel .Code}}", Type: "{{.Type}}"},
+ {{- end }}
+ {{- end }}
+ })
+ resp.Diagnostics.Append(refreshDiags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ resp.Diagnostics.Append(resp.State.Set(ctx, &refreshed)...)
+ return
+ }
+
+ operationTimeout := ""
+ if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() {
+ operationTimeout = plan.OperationTimeout.ValueString()
+ }
+ if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString())
+ }
+
+ {{- if .HasRedeploy }}
+ // --- Redeploy support (ARCHITECTURE.md) ---
+ // 1. Modify — только если изменились параметры сервиса
+ if hasServiceParamChanges {
+ {{- end }}
+ params := map[int]string{
+ {{- range .ModifyParams }}
+ {{- if not (IsNested .) }}
+ {{.ID}}: {{ParamFormat . (printf "plan.%s" (ToCamel .Code))}},
+ {{- end }}
+ {{- end }}
+ }
+ {{- range .ModifyParams }}
+ {{- if (IsNested .) }}
+ if plan.{{ToCamel .Code}} != nil {
+ params[{{.ID}}] = {{NestedJSONExpr . "plan"}}
+ }
+ {{- end }}
+ {{- end }}
+ if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ {{- if .HasRedeploy }}
+ }
+
+ // 2. Redeploy — только если изменился git_revision
+ if redeployRequested {
+ redeployParams := map[int]string{}
+ {{- range .RedeployParams }}
+ {{- if (IsNested .) }}
+ if plan.{{ToCamel .Code}} != nil {
+ redeployParams[{{.ID}}] = {{NestedJSONExpr . "plan"}}
+ }
+ {{- else }}
+ redeployParams[{{.ID}}] = {{ParamFormat . (printf "plan.%s" (ToCamel .Code))}}
+ {{- end }}
+ {{- end }}
+ if err := r.client.RunRedeployOperation(ctx, instanceID.ValueString(), operationTimeout, redeployParams); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+ }
+ {{- end }}
+
+ plan.ID = instanceID
+ state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), {{.ServiceID}}, plan, []resources_core.StateField{
+ {{- range .OutputParams }}
+ {Code: "{{.Code}}"},
+ {{- end }}
+ }, []resources_core.InputField{
+ {{- range .SchemaParams }}
+ {{- if eq .RefSvcId 0 }}
+ {Code: "{{.Code}}", Field: "{{ToCamel .Code}}", Type: "{{.Type}}"},
+ {{- end }}
+ {{- end }}
+ })
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *{{ToCamel .Name}}Resource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state {{ToCamel .Name}}Model
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ if state.ID.IsNull() || state.ID.IsUnknown() {
+ return
+ }
+
+ {{- if .SupportsSuspendDestroy }}
+ deleteMode := "state_only"
+ if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() {
+ deleteMode = "suspend"
+ }
+ {{- else }}
+ deleteMode := "delete"
+ {{- end }}
+
+ operationTimeout := ""
+ if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() {
+ operationTimeout = state.OperationTimeout.ValueString()
+ }
+ if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() {
+ ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString())
+ }
+ if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
+ resp.Diagnostics.AddError("Ошибка клиента", err.Error())
+ return
+ }
+}
+
+func (r *{{ToCamel .Name}}Resource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+}
+
+func (r *{{ToCamel .Name}}Resource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
+ if req.ProviderData == nil {
+ return
+ }
+ client, ok := req.ProviderData.(*core.UniversalClient)
+ if !ok {
+ resp.Diagnostics.AddError("Error", "Invalid client type")
+ return
+ }
+ r.client = client
+}
+`
diff --git a/TMP/backup_2026-09-24/freeze/loader.go b/TMP/backup_2026-09-24/freeze/loader.go
new file mode 100644
index 0000000..38082e8
--- /dev/null
+++ b/TMP/backup_2026-09-24/freeze/loader.go
@@ -0,0 +1,473 @@
+// 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"
+ "tf-tools/lib"
+)
+
+// 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),
+ DeleteStrategy: normalizeDeleteStrategy(op.DeleteStrategy),
+ Idempotency: normalizeIdempotency(op.Idempotency),
+ }
+ modifier.DeleteParams = convertDeleteParams(op.DeleteParams)
+ modifier.SchemaParams = modifier.Params
+ for idx := range modifier.SchemaParams {
+ if modifier.SchemaParams[idx].HasSubParams {
+ modifier.SchemaParams[idx].IsJson = true
+ }
+ }
+ 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)
+ gr.NeedsBoolUseStateForUnknown, gr.NeedsInt64UseStateForUnknown = analyzeUseStateForUnknown(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)
+ // Строковый import нужен только для обычных строковых сравнений и
+ // обработок в шаблоне. Старый restore-хук для refSvc больше не
+ // генерируется, поэтому отдельный флаг под него не нужен.
+ gr.NeedsStringsImport = 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,
+ IsModifiable: p.IsModifiable,
+ })
+ }
+ 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)
+ }
+ if op.Kind == "modifier" {
+ if err := validateModifierOperation(i, &op); err != nil {
+ return err
+ }
+ }
+ }
+ return nil
+}
+
+// normalizeDeleteStrategy возвращает каноническое значение delete_strategy.
+// Пусто → noop_warn; неизвестное — как есть (валидация в ValidateSpec отклонит раньше).
+func normalizeDeleteStrategy(raw string) string {
+ v := strings.ToLower(strings.TrimSpace(raw))
+ if v == "" {
+ return "noop_warn"
+ }
+ return v
+}
+
+// normalizeIdempotency возвращает каноническое значение idempotency.
+// Пусто → none.
+func normalizeIdempotency(raw string) string {
+ v := strings.ToLower(strings.TrimSpace(raw))
+ if v == "" {
+ return "none"
+ }
+ return v
+}
+
+// convertDeleteParams переносит lib.DeleteParam → types.DeleteParam (wire-строки).
+func convertDeleteParams(raw []lib.DeleteParam) []types.DeleteParam {
+ if len(raw) == 0 {
+ return nil
+ }
+ out := make([]types.DeleteParam, 0, len(raw))
+ for _, p := range raw {
+ out = append(out, types.DeleteParam{Code: strings.TrimSpace(p.Code), Value: p.Value})
+ }
+ return out
+}
+
+// validateModifierOperation — fail-fast для полей modifier-операции.
+func validateModifierOperation(i int, op *lib.OperationSpec) error {
+ ds := normalizeDeleteStrategy(op.DeleteStrategy)
+ switch ds {
+ case "noop_warn", "inverse", "error":
+ default:
+ return fmt.Errorf("operation[%d] %q (kind=modifier): unknown delete_strategy %q (valid: noop_warn, inverse, error)", i, op.Name, op.DeleteStrategy)
+ }
+
+ idem := normalizeIdempotency(op.Idempotency)
+ switch idem {
+ case "none", "check_before_run":
+ default:
+ return fmt.Errorf("operation[%d] %q (kind=modifier): unknown idempotency %q (valid: none, check_before_run)", i, op.Name, op.Idempotency)
+ }
+
+ if ds == "inverse" && len(op.DeleteParams) == 0 {
+ return fmt.Errorf("operation[%d] %q (kind=modifier): delete_strategy=inverse требует delete_params", i, op.Name)
+ }
+
+ // каждый delete_params.code обязан существовать среди params (по lower-code)
+ paramCodes := make(map[string]struct{}, len(op.Params))
+ for _, p := range op.Params {
+ paramCodes[strings.ToLower(strings.TrimSpace(p.Code))] = struct{}{}
+ }
+ for _, dp := range op.DeleteParams {
+ if _, ok := paramCodes[strings.ToLower(strings.TrimSpace(dp.Code))]; !ok {
+ return fmt.Errorf("operation[%d] %q (kind=modifier): delete_params.code %q отсутствует в params", i, op.Name, dp.Code)
+ }
+ }
+ return nil
+}
+
+// analyzeUseStateForUnknown определяет, нужны ли импорты boolplanmodifier/int64planmodifier:
+// есть ли среди SchemaParams скалярный Optional+Computed (без Default) параметр
+// соответствующего типа, для которого генерируется UseStateForUnknown().
+func analyzeUseStateForUnknown(schemaParams []types.Param) (needsBool, needsInt64 bool) {
+ for _, p := range schemaParams {
+ if p.IsJson || p.Required || p.RefSvcId != 0 || p.Default != "" {
+ continue
+ }
+ switch strings.ToLower(p.Type) {
+ case "bool":
+ needsBool = true
+ case "int", "int64", "number":
+ needsInt64 = true
+ }
+ }
+ return needsBool, needsInt64
+}
diff --git a/TMP/backup_2026-09-24/freeze/modifiers.tf b/TMP/backup_2026-09-24/freeze/modifiers.tf
new file mode 100644
index 0000000..1a319c2
--- /dev/null
+++ b/TMP/backup_2026-09-24/freeze/modifiers.tf
@@ -0,0 +1,57 @@
+# =============================================================================
+# Ресурсы-модификаторы (операции modify, которых нет в create-схеме ресурсов)
+#
+# Порядок строго такой:
+# орга (создана вручную в ЛК)
+# -> nubes_vc_vdc.vdc
+# -> nubes_vc_nsxt.edge
+# -> nubes_vc_org_ip_allocation (выделение внешних IP на орге)
+# -> nubes_vc_nsxt_snat (SNAT на эдже этим ipSpace)
+#
+# Почему аллокация ПОСЛЕ эджа: платформа строит список ipSpace из состояния
+# `job.vcd.networkProvider` / `job.vcd.providerGateway`, то есть требует уже
+# созданный vDC и Edge. Иначе modify на орге падает
+# («Can't cast Complex Object Type Struct to String»).
+# =============================================================================
+
+# 1. Внешние IP на организации (modify: vIPConfigure, массив перезаписывается целиком)
+resource "nubes_vc_org_ip_allocation" "org_ip" {
+ organization = var.organization
+
+ vip_configure = jsonencode([
+ {
+ name = var.ip_space_name
+ count = var.ip_count
+ }
+ ])
+
+ # false = при destroy отправить обратный modify с count=0 (квота обнулится)
+ keep_on_destroy = false
+
+ depends_on = [nubes_vc_nsxt.edge]
+}
+
+# 2. SNAT на эдже (modify: ipSpaceName)
+resource "nubes_vc_nsxt_snat" "snat" {
+ nsxt_uid = nubes_vc_nsxt.edge.id
+ ip_space_name = var.ip_space_name
+
+ keep_on_destroy = false
+
+ # ipSpace должен быть уже выделен на организации
+ depends_on = [nubes_vc_org_ip_allocation.org_ip]
+}
+
+output "allocated_org_ip" {
+ description = "Выделено внешних IP на организации"
+ value = {
+ organization = var.organization
+ ip_space_name = var.ip_space_name
+ ip_count = var.ip_count
+ }
+}
+
+output "snat_ip_space" {
+ description = "ipSpace, включённый как SNAT на эдже"
+ value = nubes_vc_nsxt_snat.snat.ip_space_name
+}
diff --git a/TMP/backup_2026-09-24/freeze/shturval.tf b/TMP/backup_2026-09-24/freeze/shturval.tf
new file mode 100644
index 0000000..aea389c
--- /dev/null
+++ b/TMP/backup_2026-09-24/freeze/shturval.tf
@@ -0,0 +1,157 @@
+# =============================================================================
+# Kubernetes кластер Штурвал — сервис 150, ресурс nubes_k8s_sthutrval_cluster
+# (НЕ 148 «Менеджмент Kubernetes кластер Штурвал» — это другой сервис)
+#
+# Всё, что относится к Штурвалу, лежит ТОЛЬКО в этом файле: переменные, их
+# значения по умолчанию и сам ресурс. Чтобы выключить Штурвал — удалить файл
+# или закомментировать ресурс.
+#
+# Порядок (чек-лист из инструкции на услугу в ЛК):
+# 1) Организация в Cloud Director — создана вручную в ЛК
+# 2) nubes_vc_vdc.vdc — есть
+# 3) nubes_vc_nsxt.edge — есть, обязательно ALB + AVI VS >= 3
+# 4) внешние адреса в организации — суммарно >= 3 (nubes_vc_org_ip_allocation)
+# 5) SNAT на Edge — nubes_vc_nsxt_snat
+# 6) Kubernetes кластер Штурвал — этот ресурс
+#
+# Минимальные требования к кластеру: мастер-нод >= 1, воркер-нод >= 1,
+# 4 vCPU / 8 GB RAM / 50 GB диска на ноду.
+# =============================================================================
+
+# --- Переменные Штурвала ---
+
+variable "shturval_resource_name" {
+ type = string
+ default = "shturval-dev"
+ description = "Имя услуги «Kubernetes кластер Штурвал» в ЛК"
+}
+
+variable "shturval_cluster_name" {
+ type = string
+ default = "shturval-dev-00"
+ description = "Имя кластера внутри Штурвала"
+}
+
+variable "shturval_app_version" {
+ type = string
+ default = "2.14.0"
+ description = "Версия Штурвала (значение по умолчанию платформы — 2.14.0)"
+}
+
+variable "shturval_cp_sizing_policy" {
+ type = string
+ default = "TKG 4CPU 8RAM"
+ description = "Политика размера control plane: 4 vCPU / 8 GB (минимум по инструкции). Должна существовать в ресурсной платформе vDC — список политик берётся из услуги «Виртуальный датацентр»"
+}
+
+variable "shturval_cp_sizing_disk" {
+ type = number
+ default = 50
+ description = "Диск control plane, ГБ (минимум 50)"
+}
+
+variable "shturval_cp_count" {
+ type = number
+ default = 1
+ description = "Количество мастер-нод: 1, 3 или 5"
+}
+
+variable "shturval_worker_group_name" {
+ type = string
+ default = "workers-shturval-dev"
+ description = "Имя группы воркеров (уникальное в кластере; допустимы строчные латинские буквы, цифры и дефис)"
+}
+
+variable "shturval_worker_sizing_policy" {
+ type = string
+ default = "TKG 4CPU 8RAM"
+ description = "Политика размера воркеров: 4 vCPU / 8 GB (минимум по инструкции)"
+}
+
+variable "shturval_worker_sizing_disk" {
+ type = number
+ default = 50
+ description = "Диск воркеров, ГБ (минимум 50)"
+}
+
+variable "shturval_worker_count" {
+ type = number
+ default = 1
+ description = "Количество воркер-нод (минимум 1)"
+}
+
+# --- Значения, которые собираются из переменных ---
+
+locals {
+ # Группы воркеров передаются JSON-строкой ВНУТРЬ услуги как есть, поэтому ключи
+ # должны быть ровно такими, как в манифесте услуги 150: groupName, sizingPolicy,
+ # sizingDisk, count, autoscale, labelDeck.
+ # ВНИМАНИЕ: в сгенерированном примере провайдера (docs → Example) ключи показаны
+ # в snake_case — это ошибка генератора, платформа на них падает с
+ # «Cannot invoke method split() on null object» (не находит groupName → null).
+ shturval_worker_config = jsonencode([
+ {
+ groupName = var.shturval_worker_group_name
+ sizingPolicy = var.shturval_worker_sizing_policy
+ sizingDisk = var.shturval_worker_sizing_disk
+ count = var.shturval_worker_count
+ autoscale = false # автоскейл выключен
+ labelDeck = true # разрешить разворачивать услуги из ЛК на этих нодах
+ }
+ ])
+}
+
+# --- Ресурс Штурвала ---
+
+resource "nubes_k8s_sthutrval_cluster" "shturval" {
+ resource_name = var.shturval_resource_name
+
+ # Кластер Штурвала уже существует (инстанс «shturval-dev») и в проде не
+ # удаляется неделями, поэтому ресурс должен УСЫНОВИТЬ существующий инстанс,
+ # а не падать с «РЕСУРС С ТАКИМ ИМЕНЕМ УЖЕ СУЩЕСТВУЕТ (SUSPEND)».
+ # Проверка/adopt выполняются в Create на apply (в plan будет «will be created»).
+ adopt_existing_on_create = true
+
+ # Штурвал создаётся долго (десятки минут) — поднимаем таймаут ожидания,
+ # иначе провайдер сдаётся на дефолтных 600 с.
+ operation_timeout = "60m"
+
+ startup_configuration = {
+ # vDC и Edge из этого же конфига (обязательные поля)
+ vdc_uid = nubes_vc_vdc.vdc.id
+ nsxt_uid = nubes_vc_nsxt.edge.id
+
+ cluster_name = var.shturval_cluster_name
+
+ # Дополнительные возможности кластера (в ЛК — галочки при создании)
+ ex_logging = true # логи в Loki (без него логи услуг не видны в ЛК)
+ ex_monitoring = true # метрики в VictoriaMetrics (без него метрик в ЛК нет)
+ ex_local_csi = true
+ ex_vip = true
+ ex_update = true
+ ex_ingress = true
+ ex_named_csi = true
+ }
+
+ cluster_configuration = {
+ app_version = var.shturval_app_version
+ }
+
+ control_plane_configuration = {
+ sizing_policy = var.shturval_cp_sizing_policy
+ sizing_disk = var.shturval_cp_sizing_disk
+ count = var.shturval_cp_count
+ }
+
+ worker_configuration = local.shturval_worker_config
+
+ access_configuration = {
+ need_external_address_api = true # внешний адрес для Kubernetes API (false недопустим)
+ access_ip_list_api = jsonencode([]) # пусто = доступ всем
+ need_external_address_ingress = true # внешний адрес для Ingress
+ access_ip_list_ingress = jsonencode([]) # пусто = доступ всем
+ }
+
+ # Кластер поднимается только после готовой сети: vDC -> Edge -> внешние IP -> SNAT
+ depends_on = [nubes_vc_nsxt_snat.snat]
+}
diff --git a/TMP/backup_2026-09-24/freeze/types.go b/TMP/backup_2026-09-24/freeze/types.go
new file mode 100644
index 0000000..f4ed71f
--- /dev/null
+++ b/TMP/backup_2026-09-24/freeze/types.go
@@ -0,0 +1,183 @@
+// Package types — структуры данных для генератора Terraform-провайдера.
+//
+// Моделирует три вида ресурсов:
+// - GenResource — основной CRUD инстанса (nubes_{service})
+// - GenSubresource — подресурсы (nubes_{service}_{user}, ...)
+// - GenAction — действия (почти не используется, redeploy встроен в GenResource.HasRedeploy)
+//
+// Правила обработки action'ов из ARCHITECTURE.md:
+// - redeploy → HasRedeploy=true, поле git_revision в основном ресурсе
+// - restart/recovery/reconcile → исключены (ручные, только UI)
+// - всё остальное → отдельный GenAction
+package types
+
+import "tf-tools/lib"
+
+// ─── Алиасы к lib (общий YAML-контракт) ─────────────────────────────────────
+
+type OutputParam = lib.OutputParam
+type OperationSpec = lib.OperationSpec
+type ParamSpec = lib.ParamSpec
+
+// ─── Входные структуры (из YAML) — локальное определение ────────────────────
+
+// ServiceSpec — локальное определение (отличается вложенными типами от lib).
+// Использует *bool для Lifecycle (совместимость с 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
+ // IsModifiable — флаг is_modifiable из API (можно менять в UI → можно менять в Terraform).
+ // nil означает «не указано» (как правило, изменяемо).
+ IsModifiable *bool
+ // IsJson — атрибут является JSON-строкой (data_type: json в YAML-спеке).
+ // При IsJson=true в схему добавляется JsonNormalize план-модификатор,
+ // чтобы план и API-ответ (компактный JSON) всегда совпадали.
+ IsJson bool
+ // HasSubParams — параметр имеет подполя (map-fixed или array-map-fixed).
+ HasSubParams bool
+ // SubParams — подполя для map-fixed/array-map-fixed параметров.
+ SubParams []Param
+}
+
+// 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
+ NeedsBoolUseStateForUnknown bool
+ NeedsInt64UseStateForUnknown 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
+ // RedeployParams — параметры redeploy-операции (если есть).
+ RedeployParams []Param
+ // NeedsJsonPlanMod: хотя бы один атрибут имеет data_type: json.
+ // Управляет добавлением импорта planmodifier в сгенерированный файл.
+ NeedsJsonPlanMod bool
+ // NeedsStringsImport: есть строковые ModifyParams (нужен EqualFold в Update).
+ NeedsStringsImport bool
+ // NeedsFmtImport: есть nested (map-fixed) параметры, нужен fmt.Sprintf.
+ NeedsFmtImport 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
+}
+
+// GenModifier — отдельный ресурс для отложенной parent-level modify операции.
+// Delete намеренно не содержит rollback: API-контракт обратного payload не подтверждён.
+type GenModifier struct {
+ ServiceName string
+ ServiceID int
+ ModifierName string
+ OperationName string
+ Params []Param
+ SchemaParams []Param
+ // DeleteStrategy — noop_warn | inverse | error (нормализовано из YAML, пусто → noop_warn).
+ DeleteStrategy string
+ // Idempotency — none | check_before_run (нормализовано из YAML, пусто → none).
+ Idempotency string
+ // DeleteParams — обратные значения (wire-строки), только при DeleteStrategy==inverse.
+ DeleteParams []DeleteParam
+ UsesBool bool
+ UsesInt64 bool
+ UsesString bool
+ HasDefaults bool
+ NeedsBoolDefault bool
+ NeedsInt64Default bool
+ NeedsStringDefault bool
+ NeedsJsonPlanMod bool
+}
+
+// DeleteParam — обратное значение параметра inverse-Delete (code → wire-строка).
+type DeleteParam struct {
+ Code string
+ Value string
+}
diff --git a/TMP/backup_2026-09-24/shturval.tf.before-adopt b/TMP/backup_2026-09-24/shturval.tf.before-adopt
new file mode 100644
index 0000000..b011f3c
--- /dev/null
+++ b/TMP/backup_2026-09-24/shturval.tf.before-adopt
@@ -0,0 +1,151 @@
+# =============================================================================
+# Kubernetes кластер Штурвал — сервис 150, ресурс nubes_k8s_sthutrval_cluster
+# (НЕ 148 «Менеджмент Kubernetes кластер Штурвал» — это другой сервис)
+#
+# Всё, что относится к Штурвалу, лежит ТОЛЬКО в этом файле: переменные, их
+# значения по умолчанию и сам ресурс. Чтобы выключить Штурвал — удалить файл
+# или закомментировать ресурс.
+#
+# Порядок (чек-лист из инструкции на услугу в ЛК):
+# 1) Организация в Cloud Director — создана вручную в ЛК
+# 2) nubes_vc_vdc.vdc — есть
+# 3) nubes_vc_nsxt.edge — есть, обязательно ALB + AVI VS >= 3
+# 4) внешние адреса в организации — суммарно >= 3 (nubes_vc_org_ip_allocation)
+# 5) SNAT на Edge — nubes_vc_nsxt_snat
+# 6) Kubernetes кластер Штурвал — этот ресурс
+#
+# Минимальные требования к кластеру: мастер-нод >= 1, воркер-нод >= 1,
+# 4 vCPU / 8 GB RAM / 50 GB диска на ноду.
+# =============================================================================
+
+# --- Переменные Штурвала ---
+
+variable "shturval_resource_name" {
+ type = string
+ default = "shturval-dev"
+ description = "Имя услуги «Kubernetes кластер Штурвал» в ЛК"
+}
+
+variable "shturval_cluster_name" {
+ type = string
+ default = "shturval-dev-00"
+ description = "Имя кластера внутри Штурвала"
+}
+
+variable "shturval_app_version" {
+ type = string
+ default = "2.14.0"
+ description = "Версия Штурвала (значение по умолчанию платформы — 2.14.0)"
+}
+
+variable "shturval_cp_sizing_policy" {
+ type = string
+ default = "TKG 4CPU 8RAM"
+ description = "Политика размера control plane: 4 vCPU / 8 GB (минимум по инструкции). Должна существовать в ресурсной платформе vDC — список политик берётся из услуги «Виртуальный датацентр»"
+}
+
+variable "shturval_cp_sizing_disk" {
+ type = number
+ default = 50
+ description = "Диск control plane, ГБ (минимум 50)"
+}
+
+variable "shturval_cp_count" {
+ type = number
+ default = 1
+ description = "Количество мастер-нод: 1, 3 или 5"
+}
+
+variable "shturval_worker_group_name" {
+ type = string
+ default = "workers-shturval-dev"
+ description = "Имя группы воркеров (уникальное в кластере; допустимы строчные латинские буквы, цифры и дефис)"
+}
+
+variable "shturval_worker_sizing_policy" {
+ type = string
+ default = "TKG 4CPU 8RAM"
+ description = "Политика размера воркеров: 4 vCPU / 8 GB (минимум по инструкции)"
+}
+
+variable "shturval_worker_sizing_disk" {
+ type = number
+ default = 50
+ description = "Диск воркеров, ГБ (минимум 50)"
+}
+
+variable "shturval_worker_count" {
+ type = number
+ default = 1
+ description = "Количество воркер-нод (минимум 1)"
+}
+
+# --- Значения, которые собираются из переменных ---
+
+locals {
+ # Группы воркеров передаются JSON-строкой ВНУТРЬ услуги как есть, поэтому ключи
+ # должны быть ровно такими, как в манифесте услуги 150: groupName, sizingPolicy,
+ # sizingDisk, count, autoscale, labelDeck.
+ # ВНИМАНИЕ: в сгенерированном примере провайдера (docs → Example) ключи показаны
+ # в snake_case — это ошибка генератора, платформа на них падает с
+ # «Cannot invoke method split() on null object» (не находит groupName → null).
+ shturval_worker_config = jsonencode([
+ {
+ groupName = var.shturval_worker_group_name
+ sizingPolicy = var.shturval_worker_sizing_policy
+ sizingDisk = var.shturval_worker_sizing_disk
+ count = var.shturval_worker_count
+ autoscale = false # автоскейл выключен
+ labelDeck = true # разрешить разворачивать услуги из ЛК на этих нодах
+ }
+ ])
+}
+
+# --- Ресурс Штурвала ---
+
+resource "nubes_k8s_sthutrval_cluster" "shturval" {
+ resource_name = var.shturval_resource_name
+
+ # Штурвал создаётся долго (десятки минут) — поднимаем таймаут ожидания,
+ # иначе провайдер сдаётся на дефолтных 600 с.
+ operation_timeout = "60m"
+
+ startup_configuration = {
+ # vDC и Edge из этого же конфига (обязательные поля)
+ vdc_uid = nubes_vc_vdc.vdc.id
+ nsxt_uid = nubes_vc_nsxt.edge.id
+
+ cluster_name = var.shturval_cluster_name
+
+ # Дополнительные возможности кластера (в ЛК — галочки при создании)
+ ex_logging = true # логи в Loki (без него логи услуг не видны в ЛК)
+ ex_monitoring = true # метрики в VictoriaMetrics (без него метрик в ЛК нет)
+ ex_local_csi = true
+ ex_vip = true
+ ex_update = true
+ ex_ingress = true
+ ex_named_csi = true
+ }
+
+ cluster_configuration = {
+ app_version = var.shturval_app_version
+ }
+
+ control_plane_configuration = {
+ sizing_policy = var.shturval_cp_sizing_policy
+ sizing_disk = var.shturval_cp_sizing_disk
+ count = var.shturval_cp_count
+ }
+
+ worker_configuration = local.shturval_worker_config
+
+ access_configuration = {
+ need_external_address_api = true # внешний адрес для Kubernetes API (false недопустим)
+ access_ip_list_api = jsonencode([]) # пусто = доступ всем
+ need_external_address_ingress = true # внешний адрес для Ingress
+ access_ip_list_ingress = jsonencode([]) # пусто = доступ всем
+ }
+
+ # Кластер поднимается только после готовой сети: vDC -> Edge -> внешние IP -> SNAT
+ depends_on = [nubes_vc_nsxt_snat.snat]
+}
diff --git a/TMP/terraformrc.dev b/TMP/terraformrc.dev
new file mode 100644
index 0000000..572c61b
--- /dev/null
+++ b/TMP/terraformrc.dev
@@ -0,0 +1,6 @@
+provider_installation {
+ dev_overrides {
+ "tf-registry.containerk8s.services.ngcloud.ru/nubes-dev/nubes" = "/home/naeel/TF/tf_provider/TMP/devbin"
+ }
+ direct {}
+}