tmp: бэкапы файлов перед правками заморозки + terraformrc для dev_overrides
This commit is contained in:
@@ -0,0 +1,200 @@
|
|||||||
|
name: vc_nsxt
|
||||||
|
service_id: 22
|
||||||
|
service_display_name: Сетевой шлюз периметра (Edge)
|
||||||
|
service_short_name: vc_nsxt
|
||||||
|
service_man: '# Инструкция по развертыванию Сетевой шлюз периметра (Edge) через платформу<br/><br/>## 1. Общая информация<br/><br/>Сервис **Сетевой шлюз периметра (Edge)** предназначен для создания периметрового сетевого шлюза в рамках одного виртуального датацентра (vDC) или группы виртуальных датацентров (groupvDC). <br/>При создании автоматически разворачивается routed-сеть с адресным пространством `10.10.102.0/24`. <br/>Сервис обеспечивает сетевую изоляцию, маршрутизацию, а также может включать функциональность балансировщика нагрузки AVI (ALB) для последующей интеграции, включая поддержку кластеров Штурвал.<br/><br/>### Доступные операции<br/><br/>**create** — Создание нового Edge с привязкой к vDC или groupvDC и развёртыванием routed-сети. <br/>**delete** — Удаление ранее созданного Edge. Недоступно при наличии зависимых услуг (vApp, VM, Кластер Штурвал). <br/>**modify** — Изменение параметров и сетевых настроек существующего Edge.<br/><br/>---<br/><br/>## 2. Параметры развертывания<br/><br/>Ниже приведены параметры операции **create**.<br/><br/>### Тип родительской услуги<br/>Определяет контекст размещения Edge. <br/>Допустимые значения: `vdc`, `groupvdc`. <br/>Использование:<br/>- При выборе `vdc` обязателен параметр **UUID VDC**.<br/>- При выборе `groupvdc` обязателен параметр **UUID Группы VDC**.<br/><br/>### UUID VDC<br/>Идентификатор виртуального датацентра. <br/>Необходимо предварительно создать vDC через услугу «Виртуальный датацентр (vDC)». <br/>Указывается только при выборе родительского типа `vdc`.<br/><br/>### UUID Группы VDC<br/>Идентификатор группы виртуальных датацентров. <br/>Создаётся через услугу «Группа виртуальных датацентров (groupvDC)». <br/>Используется при выборе родительского типа `groupvdc`.<br/><br/>### Включить ALB<br/>Флаг активации AVI Load Balancer в Cloud Director. <br/>Пример значения: `true`/`false`. <br/>Нужен для развёртывания кластера Штурвал и возможности создания Virtual Services.<br/><br/>### Наименование segroup<br/>Имя сервиса групп (segroup) на уровне ресурсной платформы. <br/>Обязательно при включённом ALB. <br/>Определяет группу, в которой будут выделяться пулы AVI для Virtual Services.<br/><br/>### virtualServicesCount<br/>Количество резервируемых виртуальных сервисов (Virtual Services) на AVI. <br/>Типичное значение: 1–10 в зависимости от нагрузки.<br/><br/>---<br/><br/>## 3. Рекомендованные характеристики<br/><br/>### Тестовое окружение (Test)<br/><br/>- Тип родительской услуги: `vdc` <br/>- virtualServicesCount: 1–2 <br/>- ALB: выключен по умолчанию (включать только при необходимости тестирования Штурвала) <br/>- segroup: задаётся только при включённом ALB <br/>- VDC гарантии CPU/RAM: минимальные (поскольку изменить их после создания нельзя)<br/><br/>### Промышленное окружение (Production)<br/><br/>- Тип родительской услуги: `groupvdc` при работе с распределёнными нагрузками или `vdc` для локализованных проектов <br/>- virtualServicesCount: 3–10, исходя из предполагаемого количества публикаций <br/>- ALB: включён, если требуется высокая доступность сервисов или используется кластер Штурвал <br/>- segroup: рекомендуется использовать выделенную группу под проект <br/>- VDC гарантии CPU/RAM: повышенные, с учётом того, что изменить их после создания невозможно без пересоздания vDC<br/><br/>---<br/><br/>## 4. Выходные параметры<br/><br/>### Имя Edge<br/>Уникальное название созданного сетевого шлюза, используется для ссылок и дальнейших операций.<br/><br/>### Имя routed-сети<br/>Автоматически созданная routed-сеть с подсетью `10.10.102.0/24`, используется для подключения ресурсов.<br/><br/>### Параметры Edge<br/>Набор параметров, указанных пользователем при создании: тип услуги, ALB, segroup, virtualServicesCount и др. <br/>Используются в операциях modify и для анализа состояния сервиса.<br/><br/>---<br/><br/>## 5. Дополнительная информация<br/><br/>- В рамках сервиса **один раз при создании задаётся гарантированная доля CPU/RAM vDC**. Изменить её невозможно — требуется пересоздание vDC. <br/>- Удаление Edge недоступно при наличии зависимых ресурсов (vApp, VM, кластер Штурвал). <br/>- При использовании ALB необходимо корректно указывать segroup, чтобы обеспечить корректное выделение пулов AVI. <br/>- При выборе режима `groupvdc` следует учитывать распределение нагрузки между несколькими vDC. <br/>- SNAT для всех ресурсов внутри Edge можно активировать через операцию modify (параметры выделения VIP и ipSpace).<br/><br/>'
|
||||||
|
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<br/>Необходимо указывать, если включён параметр `Включить 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<br/>При выборе 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: Параметр пока не работает<br/>Должен возвращать поле из ресурсной платформы типа vc из .vcd.hardware.gatewayQoSProfiles<br/><br/>Как по идее должен работать.<br/>Нужно заполнить или CFS vdcUid, или vdcGroupUid<br/>Идеально конечно проверять `if (params.vdcType == "vdc" && params.vdcUid != "")` или `if (params.vdcType == "vdcGroup" && params.vdcGroupUid != "")`<br/><br/>Далее надо пойти по стейту vdc -> org -> resPlatform, взять gatewayQoSProfiles<br/>Или пойти по стейту vdcgroup -> vdc -> org -> resPlatform, взять gatewayQoSProfiles<br/><br/>Регулярку могу написать (Виталя)
|
||||||
|
man: if (vdcUid != '") {<br/> наборфункций1<br/>} <br/><br/>elif (vdcGroupUid != "") {<br/> наборфункций2<br/>}<br/><br/>else {<br/> return "Необходимо выбрать vdc или vdcgroup"<br/>}
|
||||||
|
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: '**Параметр не изменяемый**<br/>Адрессный пул, которая будет присвоена 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 связана с:<br/>- vApp<br/>- VM<br/>- Kubernetes-кластер Штурвал<br/><br/>Запустить операцию возможно, но будет ошибка
|
||||||
|
params: []
|
||||||
|
- name: modify
|
||||||
|
id: 111
|
||||||
|
kind: instance
|
||||||
|
action: modify
|
||||||
|
man: Для создания SNAT Правила убедитесь, что в организации есть свободные IP<br/>Редактировать кол-во свободных 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<br/>Необходимо указывать, если включён параметр `Включить 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: '**Параметр не изменяемый**<br/>Адрессный пул, которая будет присвоена 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: []
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
`
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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]
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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]
|
||||||
|
}
|
||||||
@@ -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 {}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user