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 {} +}