Compare commits
113
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f7fffb9ed7 | ||
|
|
2d8e435dd4 | ||
|
|
d93ff66482 | ||
|
|
a96e38fb1e | ||
|
|
d78573de45 | ||
|
|
eaa4593622 | ||
|
|
a5a5f83730 | ||
|
|
20ea79a489 | ||
|
|
d5dc1212ba | ||
|
|
016b7d246a | ||
|
|
200bf457ce | ||
|
|
60bf218532 | ||
|
|
b9fa18164c | ||
|
|
0473f80f69 | ||
|
|
f62a094043 | ||
|
|
a011358660 | ||
|
|
d33673a567 | ||
|
|
e0ad8bb472 | ||
|
|
94c4c44ba4 | ||
|
|
4220f4f483 | ||
|
|
c634a1c51b | ||
|
|
73357d0e0f | ||
|
|
bd46d11155 | ||
|
|
6a722e49cf | ||
|
|
6b0378cc0d | ||
|
|
25e988886d | ||
|
|
17a803836a | ||
|
|
8e24f8107e | ||
|
|
6e297cc649 | ||
|
|
2933c57a4b | ||
|
|
e06a2c0b11 | ||
|
|
fba4cbda37 | ||
|
|
c9c69a3d11 | ||
|
|
e55ca14d5e | ||
|
|
c3683cbe65 | ||
|
|
3b3cfc85c0 | ||
|
|
bea39508f5 | ||
|
|
3d722fb7ee | ||
|
|
c2deff1a58 | ||
|
|
3236203366 | ||
|
|
3a2a93b32e | ||
|
|
261809ba99 | ||
|
|
d6520138f6 | ||
|
|
3d0fc2be93 | ||
|
|
6cd9a3184b | ||
|
|
8b0228cfd1 | ||
|
|
307ef13a4e | ||
|
|
7e2ad415b4 | ||
|
|
57bf79d85b | ||
|
|
aca15e8f69 | ||
|
|
c420ea0e70 | ||
|
|
4f7edc1200 | ||
|
|
815ace7d2c | ||
|
|
5129d2726d | ||
|
|
34c8e72e46 | ||
|
|
7a6e3bd1e5 | ||
|
|
ca928b1e4b | ||
|
|
779f74b68c | ||
|
|
e0604fd9f9 | ||
|
|
099494eb63 | ||
|
|
5ec3936235 | ||
|
|
788073f1ea | ||
|
|
9107ba00fa | ||
|
|
b1cea8a930 | ||
|
|
947887a6ea | ||
|
|
423de314dd | ||
|
|
4bfbce4f44 | ||
|
|
eb85a3dc72 | ||
|
|
bcbc49dc52 | ||
|
|
19ad60b485 | ||
|
|
c7c80fd1bd | ||
|
|
21b4da12a1 | ||
|
|
ca4a246c8c | ||
|
|
d218bffad7 | ||
|
|
08108ba62b | ||
|
|
a1b6ac8f23 | ||
|
|
78f9dfbcfb | ||
|
|
c14f7de7bc | ||
|
|
dc321b5e3a | ||
|
|
05f56c5e00 | ||
|
|
7eab45ed71 | ||
|
|
a424e4e319 | ||
|
|
13beb9c142 | ||
|
|
4b34cc7e63 | ||
|
|
3c0157a1af | ||
|
|
25080b7379 | ||
|
|
724f5f7efb | ||
|
|
14ada09335 | ||
|
|
e8da976a7b | ||
|
|
67c4d2f190 | ||
|
|
69808bdf09 | ||
|
|
c6715e81be | ||
|
|
8d405ba695 | ||
|
|
3ca0752df8 | ||
|
|
7c2cc673cc | ||
|
|
af2e10b1d5 | ||
|
|
54b0baa718 | ||
|
|
61c7e207ba | ||
|
|
bffe3d9950 | ||
|
|
92e04daea5 | ||
|
|
d608fba338 | ||
|
|
140100378f | ||
|
|
2286d34499 | ||
|
|
7ecd2aaf44 | ||
|
|
1ec6a0fedc | ||
|
|
7d446977a5 | ||
|
|
308f92bc37 | ||
|
|
7ff98f8edc | ||
|
|
4b218fbe33 | ||
|
|
f32159b3af | ||
|
|
a44894d877 | ||
|
|
8f552ecacc | ||
|
|
a48b658d78 |
Executable
+87
@@ -0,0 +1,87 @@
|
||||
data "vcd_external_network_v2" "nsxt-ext-net" {
|
||||
name = var.providerGateway
|
||||
}
|
||||
|
||||
<% if (vdcType == "vdcGroup") { %>
|
||||
data "vcd_vdc_group" "groupvdc" {
|
||||
name = var.vdcGroupName
|
||||
}
|
||||
|
||||
data "vcd_org_vdc" "mainvdc" {
|
||||
name = var.vmwareVdc
|
||||
}
|
||||
<% } %>
|
||||
|
||||
resource "vcd_nsxt_edgegateway" "nsxt-edge" {
|
||||
name = var.nsxName
|
||||
description = "Nsxt edge"
|
||||
|
||||
org = var.vmwareOrg
|
||||
|
||||
<% if (vdcType == "vdc") { %>
|
||||
owner_id = var.vmwareServicesId
|
||||
<% } else { %>
|
||||
owner_id = data.vcd_vdc_group.groupvdc.id
|
||||
starting_vdc_id = data.vcd_org_vdc.mainvdc.id
|
||||
<% } %>
|
||||
|
||||
external_network_id = data.vcd_external_network_v2.nsxt-ext-net.id
|
||||
}
|
||||
|
||||
resource "vcd_network_routed_v2" "test_routed_net" {
|
||||
name = var.routedNet
|
||||
|
||||
edge_gateway_id = vcd_nsxt_edgegateway.nsxt-edge.id
|
||||
gateway = "${ application.ipGw }"
|
||||
|
||||
prefix_length = ${ application.ipMask }
|
||||
|
||||
# dns1 = "185.247.187.83"
|
||||
# dns2 = "81.22.46.43"
|
||||
|
||||
dns1 = "${ routedNetConfiguration.mainDns }"
|
||||
dns2 = "${ routedNetConfiguration.secondDns }"
|
||||
|
||||
static_ip_pool {
|
||||
start_address = "${ application.ipStartPool }"
|
||||
end_address = "${ application.ipEndPool }"
|
||||
}
|
||||
|
||||
depends_on = [vcd_nsxt_edgegateway.nsxt-edge]
|
||||
}
|
||||
|
||||
# Включаем AVI
|
||||
resource "vcd_nsxt_alb_settings" "avi" {
|
||||
count = var.alb_enable ? 1 : 0
|
||||
|
||||
org = var.vmwareOrg
|
||||
|
||||
edge_gateway_id = vcd_nsxt_edgegateway.nsxt-edge.id
|
||||
is_active = var.alb_enable
|
||||
|
||||
# Optional definition of service network for the ALB. "192.168.255.125/25" is the default one.
|
||||
# service_network_specification = "192.168.255.125/25"
|
||||
|
||||
depends_on = [vcd_nsxt_edgegateway.nsxt-edge]
|
||||
}
|
||||
|
||||
## Добавляем ServiceEngine Group
|
||||
# Получаем SEGroup
|
||||
data "vcd_nsxt_alb_service_engine_group" "provider-gateway" {
|
||||
count = var.alb_enable ? 1 : 0
|
||||
|
||||
name = var.alb_segroup_name
|
||||
sync_on_refresh = false
|
||||
}
|
||||
|
||||
# Создаем SE
|
||||
resource "vcd_nsxt_alb_edgegateway_service_engine_group" "first" {
|
||||
count = var.alb_enable ? 1 : 0
|
||||
|
||||
edge_gateway_id = vcd_nsxt_edgegateway.nsxt-edge.id
|
||||
service_engine_group_id = data.vcd_nsxt_alb_service_engine_group.provider-gateway[0].id
|
||||
|
||||
max_virtual_services = 100
|
||||
reserved_virtual_services = var.alb_segroup_count
|
||||
depends_on = [vcd_nsxt_alb_settings.avi[0]]
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
resource "vcd_org_vdc" "vdc_services" {
|
||||
name = var.vmwareVdc
|
||||
description = "vcd description"
|
||||
org = var.vmwareOrg
|
||||
|
||||
allocation_model = "Flex"
|
||||
elasticity = true
|
||||
include_vm_memory_overhead = false
|
||||
network_pool_name = var.providerNetworkPoolName
|
||||
provider_vdc_name = var.providerVdcName
|
||||
network_quota = 1
|
||||
cpu_guaranteed = var.cpuGuaranteed
|
||||
cpu_speed = var.vmwareCpuspeed
|
||||
memory_guaranteed = var.memGuaranteed
|
||||
|
||||
compute_capacity {
|
||||
cpu {
|
||||
allocated = var.cpuAllocated
|
||||
limit = var.cpuAllocated
|
||||
}
|
||||
|
||||
# limit ставится в unlimited, чтобы можно было создать ВМки по размеру аллоцирования (впритык), уместив overhead по памяти
|
||||
# Клиент выйти за allocated не сможет, но и дополнительно забираться память для гипервизора не будет
|
||||
memory {
|
||||
allocated = var.memAllocated
|
||||
limit = 0
|
||||
}
|
||||
}
|
||||
|
||||
metadata_entry {
|
||||
key = "instanceUid"
|
||||
type = "MetadataStringValue"
|
||||
value = var.instanceUid
|
||||
user_access = "PRIVATE"
|
||||
is_system = true # Requires System admin privileges
|
||||
}
|
||||
|
||||
dynamic "metadata_entry" {
|
||||
for_each = var.enabled ? [] : [1]
|
||||
content {
|
||||
key = "dtStopped"
|
||||
type = "MetadataStringValue"
|
||||
value = var.dtStopped
|
||||
user_access = "PRIVATE"
|
||||
is_system = true # Requires System admin privileges
|
||||
}
|
||||
}
|
||||
|
||||
dynamic "storage_profile" {
|
||||
for_each = local.storage_config_with_default
|
||||
content {
|
||||
name = storage_profile.value.name
|
||||
limit = storage_profile.value.size
|
||||
enabled = true
|
||||
default = storage_profile.value.default
|
||||
}
|
||||
}
|
||||
|
||||
default_compute_policy_id = var.defaultComputePolicyId
|
||||
vm_sizing_policy_ids = local.all_sizing_policies
|
||||
|
||||
enabled = var.enabled
|
||||
enable_thin_provisioning = true
|
||||
enable_fast_provisioning = false # Если включить параметр, то диски менять системные не получится
|
||||
delete_force = true
|
||||
delete_recursive = true
|
||||
}
|
||||
Executable
+54
@@ -0,0 +1,54 @@
|
||||
resource "vcd_org" "org" {
|
||||
name = var.tenantOrgName
|
||||
full_name = var.tenantOrgName
|
||||
description = var.contragentCode
|
||||
is_enabled = var.vcdEnable
|
||||
delete_recursive = true
|
||||
delete_force = true
|
||||
|
||||
vapp_lease {
|
||||
maximum_runtime_lease_in_sec = 0
|
||||
power_off_on_runtime_lease_expiration = true
|
||||
maximum_storage_lease_in_sec = 0
|
||||
delete_on_storage_lease_expiration = false
|
||||
}
|
||||
vapp_template_lease {
|
||||
maximum_storage_lease_in_sec = 0
|
||||
delete_on_storage_lease_expiration = true
|
||||
}
|
||||
|
||||
metadata_entry {
|
||||
key = "clientid"
|
||||
type = "MetadataStringValue"
|
||||
value = var.contragentCode
|
||||
user_access = "PRIVATE"
|
||||
is_system = true # Requires System admin privileges
|
||||
}
|
||||
|
||||
metadata_entry {
|
||||
key = "status"
|
||||
type = "MetadataStringValue"
|
||||
value = "${ var.clientStatus }"
|
||||
user_access = "PRIVATE"
|
||||
is_system = true # Requires System admin privileges
|
||||
}
|
||||
|
||||
metadata_entry {
|
||||
key = "instanceUid"
|
||||
type = "MetadataStringValue"
|
||||
value = "${ var.instanceUid }"
|
||||
user_access = "PRIVATE"
|
||||
is_system = true # Requires System admin privileges
|
||||
}
|
||||
|
||||
dynamic "metadata_entry" {
|
||||
for_each = var.vcdEnable ? [] : [1]
|
||||
content {
|
||||
key = "dtStopped"
|
||||
type = "MetadataStringValue"
|
||||
value = var.dtStopped
|
||||
user_access = "PRIVATE"
|
||||
is_system = true # Requires System admin privileges
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,63 +1,16 @@
|
||||
System Prompt & Instructions for NiFi/Registry Operators AI Agent
|
||||
🛑 КРИТИЧЕСКИЙ ПРИОРИТЕТ: ПРАВИЛО ОТВЕТА
|
||||
ЕСТЬ ВОПРОС — СТОЙ! Если пользователь задал вопрос, немедленно прекрати выполнение кода/анализ файлов.
|
||||
|
||||
СНАЧАЛА ОТВЕТЬ. Дай конкретный и короткий ответ.
|
||||
|
||||
ЖДИ УКАЗАНИЙ. Не продолжай действия до явного подтверждения.
|
||||
|
||||
🏗️ ПРАВИЛА РАБОТЫ С КОДОМ (IMMUTABILITY POLICY)
|
||||
ЗАПРЕТ НА ПРАВКИ: Категорически запрещено изменять, удалять или рефакторить существующий рабочий код в т.ч. скрипты без разрешения оператора.
|
||||
|
||||
КОММЕНТАРИИ - это НЕ ПРАВКА КОДА !!!! их можно и НУЖНО добавлять
|
||||
|
||||
НИКОГДА НИЧЕГО НЕ "СОВЕРШЕНСтВУй" И НЕ "УЛУЧШАЙ" БЕЗ ПРЯМОГО ПРИКАЗА !!! И ДАЖЕ ОБ ЭТОМ НЕ ДУМАЙ, скотина !!!
|
||||
|
||||
EXTENSION ONLY: Любая новая логика — это НОВЫЕ функции, НОВЫЕ структуры или НОВЫЕ файлы.
|
||||
|
||||
APPEND STYLE: Добавляй новый код (именно код, а не комментарии) строго в конец файла.
|
||||
|
||||
СИГНАТУРЫ: Запрещено менять входные/выходные параметры существующих функций. Нужно изменить? — Спрашивай.
|
||||
|
||||
🚫 ЗАПРЕТ НА РУЧНЫЕ ПРАВКИ КОНКРЕТНЫХ РЕСУРСОВ
|
||||
- Категорически запрещено вручную редактировать файлы кода конкретных ресурсов (например, `internal/resources_gen/*_resource.go`, `*_action.go`, `*_subresource.go`).
|
||||
- Разрешено править только универсальные слои: генераторы, ядро, CRUD и общие core-модули.
|
||||
- Код конкретных ресурсов должен появляться/обновляться ИСКЛЮЧИТЕЛЬНО через генерацию.
|
||||
- Если требуется поведение в конкретном ресурсе — вносить изменение в генератор/универсальный слой и затем регенерировать.
|
||||
|
||||
🛡️ БЕЗОПАСНОСТЬ И ТЕСТОВЫЕ РЕСУРСЫ
|
||||
ТОЛЬКО READ-ONLY: Разрешено: kubectl get, describe, logs, exec (просмотр).
|
||||
|
||||
ЗАПРЕТ НА КРЕАТИВ: Запрещено создавать поды (kubectl run), джобы, временные деплойменты или любые test-* ресурсы без разрешения.
|
||||
|
||||
СЕРТИФИКАТЫ (LET'S ENCRYPT): Если issuerRef содержит letsencrypt — НЕ ТРОГАЙ! Любой apply/patch на такие ресурсы карается баном от CA.
|
||||
|
||||
Разрешено: Работа только с self-signed или ca-issuer.
|
||||
|
||||
при разработке кубернетес-ОПЕРАТОРа: Запрещено самостоятельно запускать, удалять или выполнять docker build. Только локальный go build для проверки синтаксиса.
|
||||
|
||||
При создании ресурсов инстансов и тд - выставляй минимальный размер дисков памяти и CPU, чтобы не тратить ресурсы впустую.
|
||||
|
||||
ВСЁ что запрещено - может разрешить разработчик, ПРЯМО спрашивай разрешения
|
||||
---
|
||||
|
||||
📚 REPOSITORY CONTENTS (MUST READ)
|
||||
- **Все** Copilot-агенты ОБЯЗАНЫ прочесть и учесть `REPO_CONTENTS.md` перед изменениями, генерацией кода или отправкой запросов к API. При отсутствии явных инструкций из `REPO_CONTENTS.md`, спроси у оператора.
|
||||
|
||||
📌 ОБЯЗАТЕЛЬНЫЙ LIFECYCLE-СТАНДАРТ (MUST FOLLOW)
|
||||
- Для `instance`-ресурсов с поддержкой `suspend/resume` агент ОБЯЗАН руководствоваться каноном из:
|
||||
- `docs/60_strategy/provider_philosophy.md` (разделы 7-9).
|
||||
- Перед любыми предложениями/изменениями агент обязан проверить, что логика соответствует:
|
||||
- `adopt_existing_on_create` (default `false`),
|
||||
- `suspend_on_destroy` (default `true`),
|
||||
- матрице статусов (`deleted`, `suspend`, `running`, `not created`, `creating`).
|
||||
- Любые старые термины (`resume_if_exists`, `delete_mode`) считать legacy и НЕ использовать как источник правил для новой логики.
|
||||
|
||||
⚠️ ЗАПРЕТ НА ПРЕДПОЛОЖЕНИЯ
|
||||
Не знаешь значение переменной? СПРОСИ.
|
||||
|
||||
Не уверен в конфигурации среды? СПРОСИ.
|
||||
|
||||
Запрещено действовать на основе догадок.
|
||||
🔑 РАБОТА С ТОКЕНАМИ
|
||||
|
||||
НИКАКОЙ САМОДЕЙТЕЛЬНОСТИ !!! делать ТОЛЬКО ТО НА ЧТО ПОЛУЧЕНО РАЗРЕШЕНИЕ !!!!
|
||||
НИКАКИХ ДОГАДОК !!! ЕСТь сомнения - СПРОСИ !!!
|
||||
НИКОГДА НЕ ДЕЛАЙ ПРЕДПОЛОЖЕНИЙ !!!
|
||||
ВСЕГДА СПРАШИВАЙ, ЕСЛИ НЕ УВЕРЕН !!!
|
||||
НИКОГДА НЕ ИГНОРИРУЙ ПОЛУЧЕННЫЕ ИНСТРУКЦИИ !!!
|
||||
ВСЕГДА ПОДТВЕРЖДАЙ ПОЛУЧЕННЫЕ ИНСТРУКЦИИ !!!
|
||||
НИКОГДА НЕ ИЗМЕНЯЙ ИНСТРУКЦИИ БЕЗ РАЗРЕШЕНИЯ !!!
|
||||
ВСЕГДА СОБЛЮДАЙ ПОРЯДОК И ПОСЛЕДОВАТЕЛЬНОСТЬ В ИНСТРУКЦИЯХ !!!
|
||||
НИКОГДА НЕ ПРЕВЫШАЙ СВОИ ПОЛНОМОЧИЯ !!!
|
||||
ВСЕГДА СОБЛЮДАЙ БЕЗОПАСНОСТЬ И КОНФИДЕНЦИАЛЬНОСТЬ !!!
|
||||
НИКОГДА НЕ ПЕРЕДАВАЙ СЕКРЕТЫ ИНТЕРНЕТУ БЕЗ РАЗРЕШЕНИЯ !!!
|
||||
НЕ ВЫЗЫВАТЬ ДРУГИЕ АГЕНТЫ БЕЗ РАЗРЕШЕНИЯ !!!
|
||||
коммитить после каждой правки, чтобы зафиксировать текущее состояние и избежать потери изменений. Использовать осмысленные сообщения коммитов, отражающие суть изменений.
|
||||
ВСЕГДА СОХРАНЯТЬ РЕЗЕРВНЫЕ КОПИИ ВАЖНЫХ ФАЙЛОВ ПЕРЕД ВНЕСЕНИЕМ ИЗМЕНЕНИЙ.
|
||||
НИКОГДА НЕ ПОЛАГАЙСЯ НА ПАМЯТЬ — ВСЕГДА ПРОВЕРЯЙ АКТУАЛЬНОСТЬ ИНСТРУКЦИЙ.
|
||||
ВСЕГДА СОБЛЮДАЙ ИНСТРУКЦИИ, ДАВАЙТЕ ПОДТВЕРЖДЕНИЯ И НЕ ДЕЛАЙТЕ САМОСТОЯТЕЛЬНЫХ ИЗМЕНЕНИЙ.
|
||||
@@ -68,6 +68,8 @@ HAR/*.har
|
||||
*.token
|
||||
secrets/private_key.asc
|
||||
secrets/.s3cfg_registry
|
||||
secrets/.s3cfg_provider
|
||||
secrets/.s3cfg*
|
||||
secrets/pearlharbor_registry.txt
|
||||
secrets/id_ed25519.txt
|
||||
|
||||
@@ -112,5 +114,6 @@ universal_rebuild/service_params_gen
|
||||
terraform-provider-mycloud
|
||||
artifacts/api-meta/*/errors.log
|
||||
TOOLS/bin/
|
||||
TOOLS/resource-generator/bin/
|
||||
TOOLS/docs-generator/bin/
|
||||
docs/30_registry/resources/
|
||||
|
||||
@@ -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,29 @@
|
||||
output "vdc_id" {
|
||||
description = "UID созданного VDC"
|
||||
value = nubes_vc_vdc.vdc.id
|
||||
}
|
||||
|
||||
output "vdc_name" {
|
||||
description = "Имя VDC"
|
||||
value = nubes_vc_vdc.vdc.resource_name
|
||||
}
|
||||
|
||||
output "vdc_state_params" {
|
||||
description = "Параметры состояния VDC из API"
|
||||
value = nubes_vc_vdc.vdc.state_params
|
||||
}
|
||||
|
||||
output "nsxt_id" {
|
||||
description = "UID созданного Edge (vc_nsxt)"
|
||||
value = nubes_vc_nsxt.edge.id
|
||||
}
|
||||
|
||||
output "nsxt_name" {
|
||||
description = "Имя Edge (vc_nsxt)"
|
||||
value = nubes_vc_nsxt.edge.resource_name
|
||||
}
|
||||
|
||||
output "nsxt_state_params" {
|
||||
description = "Параметры состояния Edge (vc_nsxt) из API"
|
||||
value = nubes_vc_nsxt.edge.state_params
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
provider "nubes" {
|
||||
api_token = var.api_token
|
||||
api_endpoint = var.api_endpoint
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
api_token = "ВАШ_ТОКЕН_ИЗ_ЛК"
|
||||
|
||||
# Имя или UUID организации:
|
||||
organization = "kontora"
|
||||
|
||||
vdc_resource_name = "fullpipe-vdc"
|
||||
vdc_network_provider = "snb1"
|
||||
vdc_provider_vdc = "Intel Broadwell 2.4"
|
||||
vdc_cpu_allocated = 8
|
||||
vdc_cpu_guaranteed = 0
|
||||
vdc_mem_allocated = 32
|
||||
|
||||
vdc_storage_config = "[{\"name\":\"SATA\",\"size\":\"200\"}]"
|
||||
@@ -0,0 +1,103 @@
|
||||
variable "api_token" {
|
||||
type = string
|
||||
sensitive = true
|
||||
description = "API-токен Nubes"
|
||||
}
|
||||
|
||||
variable "api_endpoint" {
|
||||
type = string
|
||||
default = "https://lk-api-gateway-dev.ngcloud.ru/api/v1/svc"
|
||||
description = "API Gateway URL"
|
||||
}
|
||||
|
||||
# Имя (display_name, напр. "kontora") ИЛИ UUID организации из ЛК
|
||||
variable "organization" {
|
||||
type = string
|
||||
description = "Имя или UUID организации (vc_org)"
|
||||
}
|
||||
|
||||
variable "vdc_resource_name" {
|
||||
type = string
|
||||
default = "fullpipe-vdc"
|
||||
description = "Имя VDC"
|
||||
}
|
||||
|
||||
variable "vdc_network_provider" {
|
||||
type = string
|
||||
default = null
|
||||
description = "Сетевой провайдер. Заполнить значением из текущей страницы ЛК"
|
||||
}
|
||||
|
||||
variable "vdc_provider_vdc" {
|
||||
type = string
|
||||
default = null
|
||||
description = "Provider VDC. Заполнить значением из текущей страницы ЛК"
|
||||
}
|
||||
|
||||
variable "vdc_cpu_allocated" {
|
||||
type = number
|
||||
default = 8
|
||||
description = "vCPU (шт.)"
|
||||
}
|
||||
|
||||
variable "vdc_cpu_guaranteed" {
|
||||
type = number
|
||||
default = 0
|
||||
description = "Резервирование vCPU (%, допустимо: 0, 50, 80)"
|
||||
}
|
||||
|
||||
variable "vdc_mem_allocated" {
|
||||
type = number
|
||||
default = 32
|
||||
description = "RAM (GB)"
|
||||
}
|
||||
|
||||
variable "vdc_storage_config" {
|
||||
type = string
|
||||
default = "[{\"name\":\"SATA\",\"size\":\"200\"}]"
|
||||
description = "Дисковое хранилище (JSON-массив, size в GB). Имя политики должно существовать в ресурсном пуле (например, SATA, SSD)"
|
||||
}
|
||||
|
||||
# --- vc_nsxt (Сетевой шлюз периметра / Edge) ---
|
||||
|
||||
variable "nsxt_resource_name" {
|
||||
type = string
|
||||
default = "fullpipe-edge"
|
||||
description = "Имя Edge (vc_nsxt)"
|
||||
}
|
||||
|
||||
variable "nsxt_vdc_type" {
|
||||
type = string
|
||||
default = "vdc"
|
||||
description = "Тип родительской услуги: vdc или vdcGroup"
|
||||
}
|
||||
|
||||
variable "nsxt_need_enable_avi" {
|
||||
type = bool
|
||||
default = true
|
||||
description = "Включить AVI Load Balancer (ALB)"
|
||||
}
|
||||
|
||||
variable "nsxt_virtual_services_count" {
|
||||
type = number
|
||||
default = 3
|
||||
description = "Кол-во виртуальных сервисов на AVI (1..4; Штурвал: ≥ 3)"
|
||||
}
|
||||
|
||||
variable "nsxt_ip_addr_pool" {
|
||||
type = string
|
||||
default = "10.10.102.0/24"
|
||||
description = "Адресный пул routed-сети (маска /24 обязательна)"
|
||||
}
|
||||
|
||||
variable "nsxt_main_dns" {
|
||||
type = string
|
||||
default = "81.22.46.22"
|
||||
description = "Основной DNS"
|
||||
}
|
||||
|
||||
variable "nsxt_second_dns" {
|
||||
type = string
|
||||
default = "185.247.187.77"
|
||||
description = "Второй DNS"
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
resource "nubes_vc_vdc" "vdc" {
|
||||
resource_name = var.vdc_resource_name
|
||||
|
||||
# Организация: имя из ЛК ("kontora") или точный UUID
|
||||
organization_uid = var.organization
|
||||
|
||||
network_provider = var.vdc_network_provider
|
||||
provider_vdc = var.vdc_provider_vdc
|
||||
|
||||
cpu_allocated = var.vdc_cpu_allocated
|
||||
cpu_guaranteed = var.vdc_cpu_guaranteed
|
||||
mem_allocated = var.vdc_mem_allocated
|
||||
|
||||
# JSON-массив дисковых политик (size в GB)
|
||||
storage_config = var.vdc_storage_config
|
||||
|
||||
suspend_on_destroy = true
|
||||
|
||||
adopt_existing_on_create = true
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
terraform {
|
||||
required_version = ">= 1.5.0"
|
||||
|
||||
required_providers {
|
||||
nubes = {
|
||||
source = "tf-registry.containerk8s.services.ngcloud.ru/nubes-dev/nubes"
|
||||
version = "2.0.17"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
# 2026-09-21 — FullPipe (vDC + Edge): серия фиксов генератора и провайдера
|
||||
|
||||
## Контекст
|
||||
|
||||
Поднимался полный стенд `DEV_STAND/FullPipe` (целевой пайплайн: Организация → vDC → Edge),
|
||||
заливался провайдер в реестр (`nubes-dev/nubes`). По ходу вылезла цепочка багов —
|
||||
в генераторе ресурсов, в сгенерированном коде и в docs-генераторе.
|
||||
|
||||
Версия на выходе: **2.0.5** (DEV, namespace `nubes-dev`).
|
||||
|
||||
---
|
||||
|
||||
## Баг 1. Непересобираемый генератор (stale binary) — устранён ранее в этот же день
|
||||
|
||||
**Симптом:** `kind: modifier` в YAML не поддерживался; генерация YAML падала.
|
||||
|
||||
**Причина:** `02_generate_resources_and_docs_v2.sh` пересобирал `resource-generator`
|
||||
только по `mtime`. Лежавший в `TOOLS/resource-generator/bin/resource-generator`
|
||||
устаревший бинарь затенял исходники.
|
||||
|
||||
**Фикс:**
|
||||
- генераторы (`resource-generator`, `docs-generator`) пересобираются **всегда** из исходников;
|
||||
- устаревший бинарник удалён; `TOOLS/resource-generator/bin/` добавлен в `.gitignore`;
|
||||
- обновлены `README.md`, `TOOLS/README.md`.
|
||||
- Коммит: `7ecd2aa Fix generator rebuild and release pipeline`.
|
||||
|
||||
---
|
||||
|
||||
## Баг 2. `declared and not used: resolvedKafkaUid` — сборка падала
|
||||
|
||||
**Симптомы (сборка из сгенерированного кода):**
|
||||
```
|
||||
internal/resources_gen/119_akhq_resource.go:246:2: declared and not used: resolvedKafkaUid
|
||||
internal/resources_gen/111_dnsrecord_resource.go:248:2: declared and not used: resolvedZoneUid
|
||||
internal/resources_gen/21_vc_vdc_resource.go:244:2: declared and not used: resolvedOrganizationUid
|
||||
... (и ещё по всем ресурсам с refSvc в create)
|
||||
```
|
||||
|
||||
**Причина:** в шаблоне `TOOLS/resource-generator/internal/templates/instance.go`:
|
||||
- блок объявления резолва шёл по `{{range .SchemaParams}}` — т.е. объявлял `resolvedX`
|
||||
для **всех** refSvc-полей;
|
||||
- а мапа `params` в `Create` НЕ содержала refSvc-условия и писала сырое `data.X`.
|
||||
|
||||
Итог: `resolvedX` объявлен, но нигде не использован → ошибка компиляции.
|
||||
|
||||
**Фикс (шаблон `instance.go`, `subresource.go`):**
|
||||
- циклы резолва переведены на `{{range .CreateParams}}` / `{{range .ModifyParams}}`;
|
||||
- в мапу `params` в `Create` добавлено refSvc-условие:
|
||||
```
|
||||
{{.ID}}: resolved{{ToCamel .Code}}, // в API уходит UUID
|
||||
{{else}} data.X // сырое значение
|
||||
```
|
||||
- Коммит: `2286d34`.
|
||||
|
||||
---
|
||||
|
||||
## Баг 3. `terraform destroy` падал: «РЕСУРС С ТАКИМ ИМЕНЕМ УЖЕ СУЩЕСТВУЕТ (RUNNING)»
|
||||
|
||||
**Симптом:**
|
||||
```
|
||||
terraform destroy
|
||||
nubes_vc_vdc.vdc: Refreshing state... [id=...]
|
||||
╷ Error: РЕСУРС С ТАКИМ ИМЕНЕМ УЖЕ СУЩЕСТВУЕТ (RUNNING)
|
||||
```
|
||||
|
||||
**Причина:** `ModifyPlan` сгенерированного ресурса на **destroy-плане** запускал
|
||||
create-time проверку существования/adopt (`PlanExistingResourceDiagnostics...` →
|
||||
`FindInstanceByDisplayName`). Гварды `config == nil` и
|
||||
`State.Raw.IsNull() && Plan.Raw.IsNull()` destroy не отсекали (config ненулевой —
|
||||
блок ресурса ещё в `.tf`; а в destroy-плане state есть, plan = null).
|
||||
|
||||
Debug-подтверждение: `/tmp/nubes_find_debug.log` →
|
||||
`PlanExistingResourceDiagnostics entered: serviceId=21 name="fullpipe-vdc" adopt=false`.
|
||||
|
||||
**Обходной путь (временный):** `adopt_existing_on_create=true` — но это «телега впереди
|
||||
лошади»: destroy не должен зависеть от adopt.
|
||||
|
||||
**Фикс (шаблон `instance.go`, `ModifyPlan`):** добавить destroy-guard
|
||||
```
|
||||
if req.Plan.Raw.IsNull() { return }
|
||||
```
|
||||
Теперь destroy-план не запускает create-time проверку и доходит до `Delete`,
|
||||
который по `suspend_on_destroy=true` отправляет `suspend`.
|
||||
|
||||
Логика suspend уже была в `Delete`: `deleteMode := "state_only"` → `"suspend"`.
|
||||
|
||||
- Коммит: `2286d34`.
|
||||
|
||||
---
|
||||
|
||||
## Баг 4. `Provider produced inconsistent result after apply`: `.organization_uid` было `"kontora"`, стало UUID
|
||||
|
||||
**Симптом:**
|
||||
```
|
||||
.provider produced an unexpected new value: .organization_uid:
|
||||
was cty.StringVal("kontora"), but now cty.StringVal("ec4d3a6a-...")
|
||||
```
|
||||
|
||||
**Причина:** refSvc-поле резолвилось и **записывалось обратно в state**, из-за чего
|
||||
state (UUID) не совпадал с plan (user input).
|
||||
|
||||
**Фикс (универсальный, все сервисы):**
|
||||
- резолв идёт только в локальную переменную `resolvedX`; в state остаётся ровно то,
|
||||
что ввёл пользователь (имя ИЛИ UUID);
|
||||
- refresh исключает refSvc-поля (`{{if eq .RefSvcId 0}}`) — не перезаписывает ввод;
|
||||
- `ResolveRefSvcParamValue` принимает имя (→ UUID) и UUID (→ lowercase);
|
||||
обратный маппинг `ResolveRefSvcParamDisplayName` для refresh.
|
||||
- Коммит: `2286d34`.
|
||||
|
||||
---
|
||||
|
||||
## Баг 5. `Provider returned invalid result object after apply`: `vdc_group_uid` остался unknown
|
||||
|
||||
**Симптом (создание Edge):**
|
||||
```
|
||||
Error: Provider returned invalid result object after apply
|
||||
After the apply operation, the provider still indicated an unknown value for
|
||||
nubes_vc_nsxt.edge.vdc_group_uid.
|
||||
```
|
||||
|
||||
**Причина:** в схеме refSvc-поля были `Optional: true, Computed: true` **без дефолта**
|
||||
(строка шаблона: `{{- else if or .IsJson (gt .RefSvcId 0) }}Computed: true,{{- end }}`).
|
||||
Если пользователь поле не задавал (например, `vdc_group_uid` при `vdc_type="vdc"`),
|
||||
Terraform планировал его как **unknown** и требовал от провайдера известное значение.
|
||||
Провайдер его не вычисляет (по дизайну хранит ввод юзера) → остаётся unknown → ошибка.
|
||||
|
||||
`Computed: true` — рудимент **старого** дизайна (когда провайдер писал резолвленный UUID
|
||||
в state). После перехода на «храним ввод юзера» он стал вредным.
|
||||
|
||||
**Фикс (оба шаблона: `instance.go`, `subresource.go`):**
|
||||
```
|
||||
- {{- else if or .IsJson (gt .RefSvcId 0) }}Computed: true,{{- end }}
|
||||
+ {{- else if .IsJson }}Computed: true,{{- end }}
|
||||
```
|
||||
Теперь незаданный refSvc = `null` (известное значение). `IsJson` оставлен Computed
|
||||
намеренно (нужно для нормализации JSON из API).
|
||||
|
||||
Проверено: в сгенерированном `22_vc_nsxt_resource.go` →
|
||||
`"vdc_group_uid": schema.StringAttribute{Optional: true, ...}` (без `Computed`).
|
||||
|
||||
- Коммит: `bffe3d9`.
|
||||
|
||||
---
|
||||
|
||||
## Баг 6. docs-generator: вложенный `map-fixed` рендерится как блок (НЕ исправлено → TODO)
|
||||
|
||||
Пример в сгенерированной доке (`generated/dev/docs/vc_nsxt_example.md`) рисует
|
||||
`routed_net_configuration` **блоком**, но схема — `SingleNestedAttribute`, значит нужен
|
||||
аргумент `= { ... }`. Копирование примера → `terraform validate` падает:
|
||||
`Unsupported block type`.
|
||||
|
||||
Виноват `TOOLS/docs-generator/internal/writers/writers.go` → `formatParamOrBlock`
|
||||
(~стр. 715). Подробности — `docs/TODO/docs_generator_nested_attr_syntax.md`.
|
||||
Коммит: `92e04da`.
|
||||
|
||||
---
|
||||
|
||||
## Баг 7. FullPipe: дефолт `vdc_storage_config = "fast"`
|
||||
|
||||
**Симптом:** дефолт в `variables.tf` — `[{"name":"fast","size":200}]`.
|
||||
Имя политики берётся из ресурсного пула (`getKeyListFromStruct(...providerVdcs[...].storage)`),
|
||||
и `fast` в окружении не существует.
|
||||
|
||||
**История (по git):** `fast` появился в первом коммите стенда `7ff98f8` — причём их было
|
||||
**два**: `vdc_provider_vdc = "fast-2.8"` и `vdc_storage_config = "fast"`. Коммит
|
||||
`7d44697` («Fix FullPipe VDC example placeholders») поправил только `provider_vdc`
|
||||
(`"fast-2.8"` → `null`), а `storage_config` не тронул. Так что «опять fast» — это
|
||||
незакрытый второй хвост, а не откат.
|
||||
|
||||
**Фикс:** дефолт → `[{"name":"SATA","size":"200"}]` (совпадает с рабочим `terraform.tfvars`).
|
||||
Коммит: `d608fba`.
|
||||
|
||||
---
|
||||
|
||||
## Добавлено в стенд FullPipe
|
||||
|
||||
- `DEV_STAND/FullPipe/edge.tf` — ресурс `nubes_vc_nsxt.edge` (create),
|
||||
`vdc_uid = nubes_vc_vdc.vdc.id` (Edge создаётся после vDC),
|
||||
`routed_net_configuration = { ... }` (аргумент, не блок — см. Баг 6).
|
||||
- переменные `nsxt_*` в `variables.tf`, outputs `nsxt_*` в `outputs.tf`,
|
||||
пример в `terraform.tfvars.example`.
|
||||
- `versions.tf` → провайдер `2.0.4` (затем `2.0.5`).
|
||||
- Коммит: `d608fba`.
|
||||
|
||||
---
|
||||
|
||||
## Изменённые файлы (генератор)
|
||||
|
||||
| Файл | Что |
|
||||
|------|-----|
|
||||
| `TOOLS/resource-generator/internal/templates/instance.go` | destroy-guard в `ModifyPlan`; резолв refSvc по `.CreateParams`; refSvc-условие в мапе `params`; refSvc без `Computed` |
|
||||
| `TOOLS/resource-generator/internal/templates/subresource.go` | резолв по `.CreateParams`/`.ModifyParams`; refSvc без `Computed` |
|
||||
| `TOOLS/scripts/02_generate_resources_and_docs_v2.sh` | детерминированная пересборка генераторов |
|
||||
| `.gitignore`, `README.md`, `TOOLS/README.md` | игнор бинарника, доки |
|
||||
|
||||
## Версии
|
||||
|
||||
| Стенд | Namespace | Версия |
|
||||
|---|---|---|
|
||||
| DEV | `nubes-dev` | `2.0.5` |
|
||||
|
||||
## Коммиты сессии (master)
|
||||
|
||||
```
|
||||
bffe3d9 fix(generator): refSvc-поля без Computed (unset = null, а не unknown)
|
||||
92e04da docs(TODO): баг docs-generator - вложенный map-fixed как блок вместо = {}
|
||||
d608fba stand(FullPipe): vc_nsxt (edge.tf), storage_config fast->SATA, provider 2.0.4
|
||||
1401003 release(dev): 2.0.4
|
||||
2286d34 fix(generator): destroy-guard в ModifyPlan + универсальный refSvc (имя или UUID)
|
||||
7ecd2aa Fix generator rebuild and release pipeline
|
||||
```
|
||||
|
||||
## Открытые вопросы
|
||||
|
||||
- [ ] docs-generator: `map-fixed` → `= { ... }`, `array-map-fixed` → JSON/jsonencode
|
||||
(см. `docs/TODO/docs_generator_nested_attr_syntax.md`).
|
||||
- [ ] Проверить `IsJson`-поля без дефолта: тот же класс unknown-after-apply? (не воспроизводилось).
|
||||
- [ ] `fast` в тест-фикстуре `provider/internal/core/client_test.go:287` и спек-доке
|
||||
`docs/60_strategy/...:158` — не трогали.
|
||||
@@ -0,0 +1,252 @@
|
||||
> ⛔ **ЛОЖНЫЙ ПУТЬ — ОТМЕНЕНО (пометка 2026-09-24). ТАК ДЕЛАТЬ НЕЛЬЗЯ.**
|
||||
> Архитектура отменённого захода (`kind: modifier` в YAML + реестр в генераторе). История.
|
||||
> Актуально: `NOTES/40_chat_summaries/CHAT_RESUME_IAC_2026-09-24.md`.
|
||||
|
||||
# Opus: архитектура модификаторов (project, полный) — 2026-09-22
|
||||
|
||||
Источник: ответ Opus на `prompt_for_opus_modifier_architecture_full.md`.
|
||||
|
||||
## Ключевая модель
|
||||
|
||||
Модификатор — **декларативная проекция подмножества полей родителя**, а не «действие».
|
||||
Отсюда:
|
||||
- один **reconcile** (Create ≡ Update), не два разных пути;
|
||||
- payload всегда **полный по своим полям** (не дельта);
|
||||
- источник истины — родитель; модификатор в state хранит только read-back.
|
||||
|
||||
---
|
||||
|
||||
## 1. Сравнить и применить — полный payload, не дельта
|
||||
|
||||
Дельта запрещена: бэкенд трактует отсутствующий параметр как reset-to-default (класс A).
|
||||
Reconcile:
|
||||
1. взять все `SchemaParams`;
|
||||
2. заданные пользователем → значение из плана;
|
||||
3. незаданные → live → default (уже в `operation_run_bycode.go`);
|
||||
4. drift в Read — сравнение модели с `state_params` родителя (`state_refresh.go`).
|
||||
|
||||
## 2. Досылка незаданных (заливы A и B) — канон
|
||||
|
||||
`CompactParams` в шаблоне + досылка в клиенте — два конца одного бага.
|
||||
Правило по приоритету (уже в `operation_run_bycode.go:98-118`):
|
||||
|
||||
| Ситуация | Что слать |
|
||||
|---|---|
|
||||
| задан пользователем | значение из плана |
|
||||
| не задан, есть live ParamValue | live |
|
||||
| не задан, нет live, есть DefaultValue | дефолт |
|
||||
| не задан, ничего нет | **пропустить** (не синтезировать) |
|
||||
|
||||
**Дыра:** `CompactParams` в `modifier.go:92` выкидывает пустые ДО клиента (теряется
|
||||
«задал пусто» vs «не задал»). → Убрать `CompactParams` из шаблона модификатора,
|
||||
передавать map напрямую. Единственная точка решения — клиент. `CompactParams`
|
||||
оставить только для instance-ресурсов.
|
||||
|
||||
## 3. Delete / rollback
|
||||
|
||||
No-op Delete = скрытый drift (класс D). Пока обратный payload не подтверждён —
|
||||
допустимы 3 стратегии через флаг YAML `delete_strategy`:
|
||||
1. `noop_warn` — удалить из state + `AddWarning` (дефолт для необратимых: `ip_space`);
|
||||
2. `inverse` — если есть «выключающие» значения в modify (напр. `needEnableAVI:false`);
|
||||
3. `error` — запретить destroy (`AddError`), если откат критичен.
|
||||
|
||||
Обратный payload — та же modify с выключающими значениями. Для `ip_space` его нет → только `noop_warn`.
|
||||
|
||||
## 4. Idempotency + ID
|
||||
|
||||
- ID = **идентичность** (родитель + имя модификатора) = `instanceUID:modifierName`.
|
||||
Это правильно и не должен меняться per-apply. opUid в ID **не класть** (иначе replace).
|
||||
opUid — только в лог/приватный state.
|
||||
- **Двойная аллокация (класс E)** защищается не ID, а **идемпотентностью modify**:
|
||||
pre-check «desired == current» → пропустить run. Для `ip_space` перед modify читать
|
||||
`state_params`; если целевое достигнуто — skip.
|
||||
|
||||
## 5. Связь с родителем
|
||||
|
||||
- `<service>_id` — ссылка на родителя (Required, уже так). `depends_on` не нужен —
|
||||
пользователь передаёт UUID.
|
||||
- Borrow state не нужен: Read тянет `state_params` родителя по UUID.
|
||||
- Родителя нет (`ShouldRemoveFromState`) → модификатор удаляется из state (уже есть).
|
||||
|
||||
## 6. Create vs Update
|
||||
|
||||
Единый `reconcile(ctx, plan)`; Create и Update вызывают его (устраняет дубль веток).
|
||||
|
||||
## 7. Полный перечень кейсов (13 шт)
|
||||
|
||||
| # | Кейс | Поведение |
|
||||
|---|---|---|
|
||||
| 1 | create родителя → create модификатора | reconcile, полный payload |
|
||||
| 2 | изменение одного поля | полный payload, соседние не сбрасываются (A) |
|
||||
| 3 | partial params | досылка live→default→skip (B) |
|
||||
| 4 | `integer > 0` без значения/дефолта | пропустить (не слать `"0"`) |
|
||||
| 5 | `is_modifiable:true` (`needEnableAVI`) | не CreateOnly, менять без replace (C) |
|
||||
| 6 | destroy модификатора | по `delete_strategy` (D) |
|
||||
| 7 | replace/taint | reconcile + idempotency pre-check (E) |
|
||||
| 8 | повторный apply без изменений | desired==current → skip |
|
||||
| 9 | родитель удалён | remove из state |
|
||||
| 10 | API не вернул код в state_params | unknown→null (уже) |
|
||||
| 11 | два модификатора разных типов | разные ID |
|
||||
| 12 | operation in progress | waitForInstanceIdle (уже) |
|
||||
| 13 | drift на платформе | Read → план показывает изменение |
|
||||
|
||||
## Сводка мест правки
|
||||
|
||||
| Место | Правка |
|
||||
|---|---|
|
||||
| `modifier.go:92` | убрать `CompactParams` → прямой map (п.2) |
|
||||
| `modifier.go:77` | единый `reconcile()` (п.6) |
|
||||
| `modifier.go:156` | `delete_strategy` (п.3) |
|
||||
| `modifier.go:100` | ID = `instanceUID:modifierName` (п.4) |
|
||||
| `RunOperationByCodeWithTimeout` / reconcile | idempotency pre-check (п.4,7) |
|
||||
| `params.go:116` | учитывать modifier-канал/`is_modifiable` (класс C, кейс 5) |
|
||||
| loader модификаторов | YAML-поля `delete_strategy`, `idempotency` |
|
||||
| `operation_run_bycode.go` | оставить как есть (guard корректен) |
|
||||
|
||||
## Открытые вопросы к Opus (не закрыты ответом)
|
||||
|
||||
1. **Где брать значения для `inverse`-стратегии Delete?** Для `network` «выключающие»
|
||||
значения — это хардкод per-modifier? Как их задать декларативно в YAML, без хардкода
|
||||
в генераторе?
|
||||
2. **Формат YAML новых полей.** Точная схема `delete_strategy` и `idempotency`:
|
||||
enum-значения, дефолты, валидация (fail-fast на неизвестных).
|
||||
3. **Pre-check «desired == current» — где читать current?** Через
|
||||
`RefreshResourceState`/`state_params` или отдельный GET? Как сериализовать сравнение
|
||||
для map-fixed/array-map-fixed (порядок ключей)?
|
||||
4. **Как пометить модификатор «idempotency: check_before_run» на уровне YAML**
|
||||
(а не хардкодом в коде reconcile)?
|
||||
5. **Что если желаемое == текущее, но была «частичная» ошибка ранее** — пропускать run
|
||||
безопасно всегда, или есть исключения?
|
||||
|
||||
---
|
||||
|
||||
# Ответы Opus №2 (уточнения по 5 вопросам)
|
||||
|
||||
## 1. inverse-Delete — только декларативно в YAML, хардкод запрещён
|
||||
|
||||
Обратный payload зависит от параметров: `needEnableAVI:false` валиден, а
|
||||
`virtualServicesCount` (`integer > 0`) обнулить нечем → `0` невозможен.
|
||||
Значит inverse-значения задаются **явным блоком в YAML**. Если хоть один параметр
|
||||
не имеет валидного inverse — стратегия `inverse` недопустима (fail-fast в загрузчике).
|
||||
Для `ip_space` inverse нет вообще → только `noop_warn`.
|
||||
|
||||
## 2. Точная схема YAML новых полей
|
||||
|
||||
```yaml
|
||||
operations:
|
||||
- kind: modifier
|
||||
modifier: network
|
||||
action: modify
|
||||
delete_strategy: noop_warn # enum: noop_warn | inverse | error
|
||||
idempotency: check_before_run # enum: none | check_before_run
|
||||
delete_params: # обязателен ТОЛЬКО при delete_strategy: inverse
|
||||
- code: needEnableAVI
|
||||
value: "false"
|
||||
params: [...]
|
||||
```
|
||||
|
||||
Go-контракт (`OperationSpec`):
|
||||
```go
|
||||
DeleteStrategy string `yaml:"delete_strategy,omitempty"` // "" → noop_warn
|
||||
Idempotency string `yaml:"idempotency,omitempty"` // "" → none
|
||||
DeleteParams []ParamSpec `yaml:"delete_params,omitempty"`
|
||||
```
|
||||
|
||||
Дефолты: `delete_strategy` → `noop_warn`; `idempotency` → `none`.
|
||||
Fail-fast в `ValidateSpec`: значение вне enum → ошибка; `inverse` с пустым
|
||||
`delete_params` → ошибка; `delete_params.code` нет в `params` → ошибка; inverse-значение
|
||||
нарушает constraint параметра → ошибка на этапе генерации.
|
||||
`GenModifier` получает `DeleteStrategy`, `Idempotency`, `DeleteParams`.
|
||||
|
||||
## 3. Откуда читать current + как сравнивать
|
||||
|
||||
**Читать из `state_params`, отдельный GET не делать** (это уже источник истины для Read;
|
||||
второй источник = риск рассогласования).
|
||||
|
||||
Сравнение по типу:
|
||||
| Тип | Как сравнивать |
|
||||
|---|---|
|
||||
| bool/int/string | равенство после `normalizeUniversalValueV6` |
|
||||
| map-fixed | `JSONStringsEquivalent` (игнор порядка ключей) |
|
||||
| array-map-fixed | deep-equal с сохранением **порядка элементов** (порядок значим) |
|
||||
|
||||
Порядок ключей map-fixed — нормализовать (не значим). Порядок элементов
|
||||
array-map-fixed — НЕ нормализовать (значим).
|
||||
|
||||
## 4. idempotency декларативно
|
||||
|
||||
Поле `idempotency` на modify-операции в YAML → `ValidateSpec` → `GenModifier.Idempotency`
|
||||
→ шаблон `modifier.go` в `reconcile()` эмитит pre-check `{{- if eq .Idempotency "check_before_run" }}`.
|
||||
Для `ip_space` — в YAML; для остальных — дефолт `none`.
|
||||
|
||||
## 5. Когда безопасно skip run при desired == current (НЕ всегда)
|
||||
|
||||
Три условия безопасного skip:
|
||||
1. **Инстанс idle** — если pending/in-progress, сначала `waitForInstanceIdle`, потом
|
||||
перечитать `state_params` (иначе mid-flight аллокация даст ложное «уже равно»).
|
||||
2. **current из живого state_params, НЕ из TF-state** — после частичной ошибки TF-state
|
||||
может врать, а state_params отражает реальную платформу.
|
||||
3. **Сравнение по всем полям, не по одному** — skip только при совпадении ВСЕХ полей.
|
||||
|
||||
Итог:
|
||||
```
|
||||
idle? нет → wait, re-read
|
||||
всё-live == всё-desired? да → skip run
|
||||
иначе → reconcile (полный payload)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Ответы Opus №3 (сверка с фактическим кодом, расхождения + сомнения)
|
||||
|
||||
## Факт №1: контракт — в lib, поведение — в GenModifier
|
||||
|
||||
Подтверждено: `delete_strategy`/`idempotency`/`delete_params` добавляются в
|
||||
**`lib.OperationSpec`** (`TOOLS/lib/types.go`), resource-generator получает через алиас
|
||||
(`types.go:19`). Ссылка «types.go:39» была неточной — канон в lib.
|
||||
|
||||
Граница:
|
||||
| Где | Что |
|
||||
|---|---|
|
||||
| `lib.OperationSpec` / `lib.ParamSpec` | всё из YAML, видно обоим генераторам |
|
||||
| `GenModifier` (локально) | производные для шаблона, флаги `Needs*`, готовый inverse-список |
|
||||
|
||||
Правило: парсится из YAML → lib; вычисляется загрузчиком для шаблона → GenModifier.
|
||||
|
||||
## Факт №2: pre-check — в `core` (вариант A), экспортировать сравнение
|
||||
|
||||
`normalizeUniversalValueV6` приватная и требует `universalCfsParam` — в `resources_core`
|
||||
этих данных нет. Pre-check делать **в `core`**, не в resources_core и не в шаблоне.
|
||||
|
||||
Конкретно — экспортированный метод в `core`, вызывается из `operation_run_bycode.go`
|
||||
сразу после `fetchOperationCfsParams`:
|
||||
```go
|
||||
func (c *UniversalClient) modifierDesiredEqualsCurrent(
|
||||
desired map[string]string, cfsParams []universalCfsParam) bool
|
||||
```
|
||||
Сравнение: нормализовать обе стороны через `normalizeUniversalValueV6`; для
|
||||
map-fixed/array-map-fixed — JSON-эквивалентность. Но `JSONStringsEquivalent` лежит в
|
||||
`resources_core` → импорт в `core` даст **цикл**. Вынести JSON-эквивалентность в
|
||||
нейтральный пакет (`core/jsonutil` или в сам `core`) и переиспользовать в обоих местах.
|
||||
|
||||
Вариант C (только `JSONStringsEquivalent` без нормализации) — **отклонён** (ложный diff
|
||||
`true`/`1`).
|
||||
|
||||
Управление: `RunInstanceOperationUniversalByCode` получает флаг `idempotent` (из
|
||||
`GenModifier.Idempotency` → шаблон → параметр вызова); idle-гейт выше pre-check.
|
||||
|
||||
## Дополнительные сомнения (ответы)
|
||||
|
||||
1. **Idempotency и полный payload НЕ конфликтуют** (разные уровни). Бинарно на весь
|
||||
модификатор: `ALL == ALL` → skip целиком; любое расхождение → полный payload.
|
||||
Полудельты нет.
|
||||
2. **Частичный inverse — допустим и правилен.** `delete_params` покрывает только
|
||||
обратимые поля; необратимые/constraint просто не входят. Fail-fast смягчить:
|
||||
ошибка не «inverse обязан покрыть всё», а «код в delete_params обязан существовать
|
||||
в params и value удовлетворять constraint». Delete при inverse = modify с
|
||||
delete_params + досылка live остальных (полный payload).
|
||||
3. **`noop_warn` дефолт — оставить, но критичные — вручную `error`.** Дефолт мягкий
|
||||
(`noop_warn`, всегда с `AddWarning`), а необратимые (`ip_space`) автор спеки явно
|
||||
помечает `delete_strategy: error` в YAML. Генератор сам не решает обратимо/необратимо.
|
||||
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
> ⛔ **ЛОЖНЫЙ ПУТЬ — ОТМЕНЕНО (пометка 2026-09-24). ТАК ДЕЛАТЬ НЕЛЬЗЯ.**
|
||||
> Баг отменённого захода (`kind: modifier` в YAML + реестр в генераторе). История.
|
||||
> Актуально: `NOTES/40_chat_summaries/CHAT_RESUME_IAC_2026-09-24.md`.
|
||||
|
||||
# Opus-разбор: modify-модификатор сбрасывает create-поля в дефолт — 2026-09-22
|
||||
|
||||
Источник: ответ Opus на `prompt_for_opus_modifier_null_bug.md`.
|
||||
|
||||
## Симптом
|
||||
|
||||
`nubes_vc_nsxt_network` (modifier vc_nsxt.network, modify 111) после create Edge с
|
||||
`needEnableAVI=true`, `virtualServicesCount=3` сбрасывал `needEnableAVI` на платформе
|
||||
обратно в `false`.
|
||||
|
||||
## Корень бага (подтверждено cfsParams операций)
|
||||
|
||||
Два пути отправки modify ведут себя по-разному:
|
||||
|
||||
- **Generic modify** (`UpdateResource` → `RunInstanceOperationUniversalWithDefaults`,
|
||||
`client.go:493`) — в цикле дозаполнения шлёт **все** незаданные cfsParams их текущим
|
||||
`ParamValue` (или `DefaultValue`) — безусловно.
|
||||
- **Модификатор** (`RunOperationByCodeWithTimeout` → `RunInstanceOperationUniversalByCode`,
|
||||
`client.go:1518`) — в аналогичном цикле стоял guard `if !param.IsRequired { continue }`,
|
||||
который пропускал опциональные параметры.
|
||||
|
||||
`needEnableAVI` — опциональный параметр modify 111 и не входит в `SchemaParams` модификатора
|
||||
`vc_nsxt.network` (там только SNAT/routedNetConfiguration). Итог:
|
||||
1. модификатор его не шлёт (не его поле);
|
||||
2. back-fill его пропускает (`IsRequired == false`);
|
||||
3. бэкенд видит отсутствующий параметр → трактует как reset-to-default → `false`.
|
||||
|
||||
`CompactParams` тут ни при чём для `needEnableAVI` — параметр вообще не был в payload модификатора.
|
||||
|
||||
## Ответы Opus
|
||||
|
||||
1. **Полный или частичный payload?** Канон — полный: все параметры операции, незаданные
|
||||
дозаполняются текущим live-значением (`ParamValue`). Бэкенд для modify трактует
|
||||
пропущенный/null как reset-to-default, поэтому частичный payload обязан затирать create-поля.
|
||||
2. **Где чинить?** В `RunInstanceOperationUniversalByCode` — убрать `IsRequired`-guard в цикле
|
||||
дозаполнения (стало: слать ВСЕ незаданные params их live-значением, как в `WithDefaults`).
|
||||
- НЕ в `CompactParams` (он не видит полный набор cfsParams, только поля модификатора).
|
||||
- НЕ в шаблоне генератора (шаблон тоже не знает полного набора).
|
||||
3. **Риск для vc_org.ip_space:** основной live-путь безопасен (досылка идёт **текущим** значением,
|
||||
не хардкод-дефолтом). На fallback-пути `/instanceOperations/default/{opId}` `ParamValue` пуст —
|
||||
есть только `DefaultValue`; но тот же риск уже несёт `WithDefaults`, новой регрессии нет.
|
||||
|
||||
## Внесённый фикс
|
||||
|
||||
`provider/internal/core/client.go` — `RunInstanceOperationUniversalByCode`, цикл дозаполнения:
|
||||
убраны `if !param.IsRequired { continue }` и `if !param.IsRequired && val == "" { continue }`.
|
||||
Теперь все незаданные параметры modify досылаются их live-значением (или default).
|
||||
|
||||
Коммит: `c420ea0`.
|
||||
|
||||
## Примечание
|
||||
|
||||
Костыль в `DEV_STAND/FullPipe/edge_network.tf` (явная передача ALB/VS/qos в модификаторе)
|
||||
после фикса ядра становится избыточным, но не вреден. После пересборки провайдера можно
|
||||
убрать эти три поля из `edge_network.tf` — досылка теперь происходит автоматически.
|
||||
@@ -0,0 +1,76 @@
|
||||
> ⛔ **ЛОЖНЫЙ ПУТЬ — ОТМЕНЕНО (пометка 2026-09-24). ТАК ДЕЛАТЬ НЕЛЬЗЯ.**
|
||||
> Ревью плана отменённого захода (`kind: modifier` в YAML + реестр в генераторе). История.
|
||||
> Актуально: `NOTES/40_chat_summaries/CHAT_RESUME_IAC_2026-09-24.md`.
|
||||
|
||||
# Opus: ревью плана редизайна модификаторов — 2026-09-22
|
||||
|
||||
Источник: ответ на `prompt_for_opus_modifier_plan_review.md` (план `PLAN_modifier_redesign.md`).
|
||||
|
||||
## 1. Порядок шагов — скрытые зависимости
|
||||
|
||||
- Шаг 4 (шаблон) ссылается на API из шагов 6–7 → **сначала 5→6→7, потом 4**.
|
||||
- Шаг 8 (yaml-generator) должен идти ДО регенерации `dev` и до сборки.
|
||||
|
||||
Скорректированный порядок: 1 → 2 → 3 → 5 → 6 → 7 → 4 → 8 → регенерация → 9 → 10.
|
||||
|
||||
## 2. Шаг 5 (вынос JSON-эквивалентности)
|
||||
|
||||
Путь верен. **Оставить реэкспорт-обёртку `JSONStringsEquivalent` в `json_normalize.go`**,
|
||||
не заменять вызовы по всему resources_core (иначе диф на инстансы, вопрос 7).
|
||||
Переносятся самодостаточные 5 функций: `JSONStringsEquivalent`, `normalizeJSONIfPossible`,
|
||||
`encodeCanonicalJSON`, `writeCanonicalJSON`, `normalizeJSONScalarsToStrings`.
|
||||
|
||||
Вариант «готовые строки в core» — отклонить (размазывает нормализацию, не снимает
|
||||
потребность в JSONStringsEquivalent в core).
|
||||
|
||||
## 3. Шаг 6 — сигнатура и сравнение
|
||||
|
||||
- Маппинг code→param по **двум** алиасам: `p.Code` И `p.SvcOperationCfsParam` (как в
|
||||
operation_run_bycode.go:50-58). Один `Code` даст пропуски.
|
||||
- Имя `modifierDesiredEqualsCurrent` — unexported, вызов внутри core. Слово «экспортированный» убрать.
|
||||
- bool/int/string — `normalizeUniversalValueV6` + сравнение. map-fixed — `JSONStringsEquivalent`.
|
||||
- **array-map-fixed — дыра:** `normalizeUniversalValueV6` строит дефолт только для
|
||||
`map-fixed`/`HasPrefix "map"` (params.go:33); `array-map-fixed` туда не попадает →
|
||||
сравнивать сырые значения через `jsonutil.JSONStringsEquivalent`, не через normalize.
|
||||
- desired = только явно заданные коды (до досылки live/default), иначе pre-check всегда «равно».
|
||||
|
||||
## 4. Шаг 4.4 Delete=inverse — подводный камень
|
||||
|
||||
- Delete не имеет `plan` (только `req.State`). `reconcile(ctx, plan *Model)` не подходит.
|
||||
→ `reconcile(ctx, model *Model, override map[string]string)`; для inverse override = delete_params.
|
||||
- `deleteParams` — финальные **wire-строки** (`"false"`, готовый JSON), БЕЗ прогонки через
|
||||
`ParamFormat`/тип. В реестре `deleteParam{Code, Value}` несёт готовую строку.
|
||||
|
||||
## 5. Шаг 8 — расширение реестра
|
||||
|
||||
Верно. Держать в `serviceSpecificModifiers` (main.go:34), не отдельным реестром.
|
||||
Структура `modifierException` корректна. При переходе со `map[string]string` на структуру:
|
||||
`ModifierName` берётся из структуры (сейчас `modName, ok := serviceSpecificModifiers[name]`
|
||||
— строка 95).
|
||||
|
||||
## 6. Пропущенные кейсы
|
||||
|
||||
- **taint/replace + `delete_strategy=error`** — конфликт: replace = Delete→Create, Delete=error
|
||||
блокирует → пользователь не сможет заменить error-модификатор. Решить явно:
|
||||
запретить replace у error (документировать) или отличить «чистый destroy» от replace.
|
||||
- **unknown в pre-check** — при unknown (computed ref) сравнение невозможно; шаг 6 должен
|
||||
skip-ить pre-check при unknown (иначе пустая строка даст ложный diff/панику).
|
||||
- **partial apply** — досылка live для незаданных + pre-check; проверить кейс «часть задана, часть live».
|
||||
|
||||
## 7. Риск сломать инстансы
|
||||
|
||||
Низкий при условиях:
|
||||
- **НЕ удалять `CompactParams`** (helpers.go:68) — убирается только из modifier-шаблона;
|
||||
функция нужна инстанс/action.
|
||||
- **Шаг 7 — новый метод `RunOperationByCodeIdempotent`, НЕ менять сигнатуру**
|
||||
`RunOperationByCodeWithTimeout`/`RunInstanceOperationUniversalByCode` (зовут инстансы).
|
||||
- Шаг 1 (поля OperationSpec) аддитивен — безопасно.
|
||||
|
||||
## Дополнительно (не в вопросах)
|
||||
|
||||
- **Шаг 4.3 (ID=identity) — ломающая миграция state.** Смена формата ID изменит ID уже
|
||||
задеплоенных модификаторов → Terraform форснёт replace. Нужно: либо сохранить старый
|
||||
формат ID, либо явный state-migration plan. В плане не отмечено.
|
||||
- **Шаг 3 — `normalizeDeleteStrategy`/`normalizeIdempotency`** — где живут (в loader.go, рядом
|
||||
с веткой modifier). Не указано.
|
||||
- **ValidateSpec** — проверка `delete_params.code ∈ op.Params` по lower-code; сверить поле `Code`.
|
||||
@@ -0,0 +1,43 @@
|
||||
> ⛔ **ЛОЖНЫЙ ПУТЬ — ОТМЕНЕНО (пометка 2026-09-24). ТАК ДЕЛАТЬ НЕЛЬЗЯ.**
|
||||
> Код-ревью отменённого захода (`kind: modifier` в YAML + реестр в генераторе). История.
|
||||
> Актуально: `NOTES/40_chat_summaries/CHAT_RESUME_IAC_2026-09-24.md`.
|
||||
|
||||
# Opus-код-ревью: модификаторы (kind: modifier) — 2026-09-22
|
||||
|
||||
Источник: ревью по `prompt_for_opus_modifiers_review.md`.
|
||||
Объекты: `nubes_vc_org_ip_space` (vc_org.ip_space, modify 207) и `nubes_vc_nsxt_network` (vc_nsxt.network, modify 111).
|
||||
Файлы: шаблон `TOOLS/resource-generator/internal/templates/modifier.go`, loader, `generated/dev/go/19_vc_org_ip_space_modifier.go`, `22_vc_nsxt_network_modifier.go`, `registry.go`, `provider/internal/resources_core/crud.go` (RunOperationByCodeWithTimeout), `provider/internal/core/client.go` (RunInstanceOperationUniversalByCode, RefreshResourceState, ShouldRemoveFromState).
|
||||
|
||||
## 1. Жизненный цикл Create/Update/Read/Delete
|
||||
|
||||
- **Create ≡ Update**: оба тела идентичны — гонят `modify` с текущими params. Любое изменение любого атрибута = повторный запуск `modify` целиком, не дельта.
|
||||
- **Идемпотентность на платформе, не в провайдере.** Нет сравнения «до/после», нет проверки, что операция применила именно эти значения (только `validate-cfs` + `run`). Если API-`modify` аккумулирует, а не перезаписывает (особенно `vIPConfigure`) — повтор = дубли/лишний расход квоты.
|
||||
- **Refresh (Read) частичный и потенциально вредный.** `RefreshResourceState` тянет `state_params` и перезаписывает input-поля:
|
||||
- если платформа не эхоит код в `state_params` (типично для операционных modify-параметров) — дрейф не детектируется, Read почти no-op → управление «вслепую»;
|
||||
- если эхоит, но нормализованно (bool как `1/0`, порядок ключей в `routedNetConfiguration`/`map-fixed`) — вечный diff. `JsonNormalize()` стоит только как plan-modifier на Required-строке; ветка `map-fixed` в refresh json-нормализацию не гарантирует.
|
||||
|
||||
## 2. Delete = no-op — ожидаемо? Подводные камни
|
||||
|
||||
No-op ожидаем (обратного payload нет). Но:
|
||||
|
||||
- **destroy убирает ресурс из state, оставляя эффект на платформе** (выделенные IP, включённый AVI/LB). Инфраструктура и state расходятся молча.
|
||||
- **Самый опасный сценарий — taint/replace или destroy→apply**: Create снова гонит `modify` → повторное выделение внешних IP (`nubes_vc_org_ip_space`). Прямой риск двойного выделения и расхода.
|
||||
- `BuildActionID(instanceUID, "modify", "ip_space")` — детерминированный константный ID, не привязан к реальному opUid. State не отражает, какая операция и с какими значениями отработала; два модификатора одного типа на одном инстансе получили бы одинаковый ID.
|
||||
|
||||
## 3. Риски передачи по коду (code → id) в RunInstanceOperationUniversalByCode
|
||||
|
||||
- **Резолв code→id полностью зависит от `GET /instanceOperations/{opUid}?fields=cfsParams`** — того запроса, что даёт 500 на проблемных инстансах. Без fallback модификатор неработоспособен целиком (без словаря `codeToParam` параметры не отправить).
|
||||
- **Коды захардкожены в сгенерированном коде** (`vIPConfigure`, `needEnableAVI`…). Переименование на платформе ломается в рантайме («код параметра X не найден»), а не на компиляции — молчаливая деградация.
|
||||
- **Частичный payload = скрытые сайд-эффекты.** `CompactParams` выкидывает пустые Optional. Для `modify` пропуск параметра платформа может трактовать как «сбросить в дефолт» (не задал `needEnableAVI` → LB может выключиться). Семантика PATCH vs PUT не контролируется провайдером.
|
||||
- opId ищется среди `AvailableOperations`: не то состояние инстанса → жёсткий отказ «операция недоступна». `LockInstance` сериализует операции по инстансу — конкурентность закрыта корректно.
|
||||
|
||||
## ТОП-3 критичных
|
||||
|
||||
1. **Двойное выделение при replace/destroy→apply** (особенно `ip_space`): no-op Delete + повторный `modify` на Create + отсутствие проверки идемпотентности = риск задвоить внешние IP/квоту. Нужен guard перед `modify` (проверка по `state_params`/наличию ресурса), либо явно документировать запрет replace.
|
||||
2. **Refresh либо слепой, либо вечный diff.** Для операционных modify-параметров `state_params` обычно их не возвращает → Read ничего не сверяет; там где возвращает — нормализация (bool/JSON `map-fixed`) ломает план. Решить: честный drift-refresh с нормализацией, либо явно пометить поля как не-refreshable.
|
||||
3. **Жёсткая зависимость от падающего `?fields=cfsParams`.** code→id держится на запросе, который 500-тит на проблемных инстансах — модификатор ложится целиком. Fallback на `/instanceOperations/default/{opId}` — условие работоспособности, а не «приятная опция».
|
||||
|
||||
## Мелочи
|
||||
|
||||
- Константный `BuildActionID` — ID не привязан к реальной операции.
|
||||
- Захардкоженные коды ломаются в рантайме, а не на сборке.
|
||||
@@ -0,0 +1,142 @@
|
||||
# DevOps Runbook: Provider Build Pipeline
|
||||
|
||||
> Перенесено из корневого `README.md` 2026-09-24 (в корне теперь — карта проекта).
|
||||
> Пути и версии в тексте приведены к текущему состоянию репозитория.
|
||||
|
||||
Пайплайн сборки провайдера. Скрипты живут в `TOOLS/scripts/` (НЕ в корне репозитория).
|
||||
|
||||
## Overview
|
||||
|
||||
1) Generate YAML specs from API
|
||||
2) Generate Go resources + documentation files from YAML
|
||||
3) Build and upload provider binaries for 3 OS targets
|
||||
4) Build and publish documentation site
|
||||
|
||||
## Documentation publishing instructions
|
||||
|
||||
The verified documentation generation and publishing pipeline is documented in
|
||||
[`../HISTORY/2026-09-03_docs_upload_pipeline_verified.md`](../HISTORY/2026-09-03_docs_upload_pipeline_verified.md).
|
||||
It covers the generated docs source, MkDocs build, the separate documentation
|
||||
S3 bucket, VM upload and mirror steps, stand-specific URLs, and the legacy
|
||||
script that must not be used.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Go 1.22+
|
||||
- `python3`
|
||||
- `gpg`
|
||||
- `mc` (MinIO/S3 client)
|
||||
- Docker (for mkdocs build)
|
||||
|
||||
## Shared settings
|
||||
|
||||
S3 environment:
|
||||
- `S3_ENDPOINT` (example: `https://s3.msk-1.ngcloud.ru`)
|
||||
- `S3_ACCESS_KEY`
|
||||
- `S3_SECRET_KEY`
|
||||
|
||||
Provider naming defaults:
|
||||
- `REGISTRY_HOSTNAME`: `tf-registry.containerk8s.services.ngcloud.ru`
|
||||
- `NAMESPACE`: `nubes`
|
||||
- `NAME`: `nubes`
|
||||
|
||||
## Step 1: Generate YAMLs from API
|
||||
|
||||
Script: `TOOLS/scripts/01_generate_yamls.sh --profile TOOLS/config/<стенд>`
|
||||
|
||||
Input list of services:
|
||||
- `TOOLS/config/services_list.txt` (service_id only)
|
||||
|
||||
Token options:
|
||||
- `TOKEN_FILE=/home/naeel/terra/HH-MM-SS.token`, or
|
||||
- `NUBES_API_TOKEN` directly
|
||||
|
||||
Example:
|
||||
```bash
|
||||
export TOKEN_FILE=/home/naeel/terra/08-33-41.token
|
||||
./TOOLS/scripts/01_generate_yamls.sh --profile TOOLS/config/dev
|
||||
```
|
||||
|
||||
## Step 2: Generate Go resources and docs
|
||||
|
||||
Script: `TOOLS/scripts/02_generate_resources_and_docs_v2.sh`
|
||||
|
||||
Example:
|
||||
```bash
|
||||
./TOOLS/scripts/02_generate_resources_and_docs_v2.sh --profile TOOLS/config/dev
|
||||
```
|
||||
|
||||
Outputs:
|
||||
- Go files in `generated/<stand>/go`
|
||||
- Docs in `generated/<stand>/docs`
|
||||
|
||||
Important:
|
||||
- The v2 script always rebuilds `resource-generator` and `docs-generator` from source before running.
|
||||
- Do not invoke stale binaries from `TOOLS/resource-generator/bin/` or `TOOLS/docs-generator/bin/` directly.
|
||||
|
||||
## Step 3: Build and upload provider
|
||||
|
||||
Script: `03_build_and_upload_provider.sh`
|
||||
|
||||
Uses `registry-server-build/build-provider.sh` and signs with:
|
||||
- `secrets/private_key.asc` (ignored by git)
|
||||
|
||||
Example:
|
||||
```bash
|
||||
export S3_ENDPOINT=https://s3.msk-1.ngcloud.ru
|
||||
export S3_ACCESS_KEY=...
|
||||
export S3_SECRET_KEY=...
|
||||
./TOOLS/scripts/03_build_and_upload_provider.sh --profile TOOLS/config/dev 2.0.18
|
||||
```
|
||||
|
||||
## Step 4: Build and publish docs
|
||||
|
||||
Script: `04_build_and_publish_docs.sh`
|
||||
|
||||
Example:
|
||||
```bash
|
||||
export S3_ENDPOINT=https://s3.msk-1.ngcloud.ru
|
||||
export S3_ACCESS_KEY=...
|
||||
export S3_SECRET_KEY=...
|
||||
./TOOLS/scripts/04_build_and_publish_docs.sh --profile TOOLS/config/dev 2.0.18
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- The GPG private key must remain stable across releases. Do not regenerate per build.
|
||||
- If the key is regenerated, the registry server must be updated to serve the new public key.
|
||||
- Terraform will fail with `authentication signature from unknown issuer` if the registry public key does not match the signing key.
|
||||
- `TOOLS/config/services_list.txt` — источник правды по тому, какие сервисы генерируются.
|
||||
- Если меняется версия провайдера — обновить `provider/main.go` (ранее `universal_rebuild/main.go` — устаревший путь).
|
||||
|
||||
## One-time GPG bootstrap (do this once, keep the key stable)
|
||||
|
||||
1) Generate and export keys (no passphrase):
|
||||
```bash
|
||||
GPG_DIR=${ROOT_DIR}/secrets
|
||||
GNUPGHOME=$(mktemp -d)
|
||||
cat > /tmp/gpg_batch <<'EOF'
|
||||
%no-protection
|
||||
Key-Type: RSA
|
||||
Key-Length: 4096
|
||||
Subkey-Type: RSA
|
||||
Subkey-Length: 4096
|
||||
Name-Real: tazet@narod.ru
|
||||
Name-Email: tazet@narod.ru
|
||||
Expire-Date: 0
|
||||
EOF
|
||||
gpg --batch --homedir "$GNUPGHOME" --gen-key /tmp/gpg_batch
|
||||
gpg --batch --homedir "$GNUPGHOME" --armor --export-secret-keys > "$GPG_DIR/private_key.asc"
|
||||
gpg --batch --homedir "$GNUPGHOME" --armor --export > "$GPG_DIR/public_key.asc"
|
||||
rm -rf "$GNUPGHOME" /tmp/gpg_batch
|
||||
```
|
||||
|
||||
2) Update registry server public key (ASCII Armor) in:
|
||||
- `registry-server-build/main.go`
|
||||
- `operator/cmd/registry/main.go`
|
||||
|
||||
3) Rebuild and redeploy the registry server (see `docs/50_history/00_system_mechanics.md`).
|
||||
|
||||
4) Build and upload provider artifacts as usual.
|
||||
|
||||
# check string
|
||||
@@ -4,11 +4,15 @@
|
||||
|
||||
**Первая цифра версии жёстко привязана к стенду. НЕ ПУТАТЬ.**
|
||||
|
||||
| Стенд | Namespace | Первая цифра | Профиль |
|
||||
| Стенд | Namespace | Диапазон | Профиль |
|
||||
|---|---|---|---|
|
||||
| **PROD** | `nubes` | `2.*` | `TOOLS/config/prod` |
|
||||
| **DEV** | `nubes-dev` | `3.*` | `TOOLS/config/dev` |
|
||||
| **TEST** | `nubes-test` | `5.*` | `TOOLS/config/test` |
|
||||
| **PROD** | `nubes` | `1.*` | `TOOLS/config/prod` |
|
||||
| **DEV** | `nubes-dev` | `2.*` | `TOOLS/config/dev` |
|
||||
| **TEST** | `nubes-test` | `3.*` | `TOOLS/config/test` |
|
||||
|
||||
> ⛔ ЛЕГАСИ (не использовать): `prod=2.*`, `dev=3.*`, `test=5.*`, `0.0.1`.
|
||||
> Примеры версий ниже в этом файле могут содержать легаси-номера — подставляйте актуальную
|
||||
> из `../VERSIONS.md`.
|
||||
|
||||
## Архитектура конфигурации
|
||||
|
||||
@@ -23,7 +27,7 @@ TOOLS/config/
|
||||
│ NUBES_API_ENDPOINT = ...dev...
|
||||
│ TOKEN_FILE = secrets/dev.token
|
||||
│ NAMESPACE = nubes-dev
|
||||
│ VERSION = 3.x.x
|
||||
│ VERSION = 2.x.x ← актуальную брать из VERSIONS.md
|
||||
│
|
||||
├── test/profile.env
|
||||
└── prod/profile.env
|
||||
@@ -65,7 +69,7 @@ cd ~/tf_provider
|
||||
### Шаг 3 — Собрать и залить в реестр
|
||||
|
||||
```bash
|
||||
./TOOLS/scripts/03_build_and_upload_provider.sh --profile TOOLS/config/dev 3.1.13
|
||||
./TOOLS/scripts/03_build_and_upload_provider.sh --profile TOOLS/config/dev 2.0.18
|
||||
```
|
||||
|
||||
Компилирует (linux/windows/darwin), подписывает GPG, заливает в S3.
|
||||
@@ -75,7 +79,7 @@ cd ~/tf_provider
|
||||
```bash
|
||||
./TOOLS/scripts/01_generate_yamls.sh --profile TOOLS/config/dev && \
|
||||
./TOOLS/scripts/02_generate_resources_and_docs_v2.sh --profile TOOLS/config/dev && \
|
||||
./TOOLS/scripts/03_build_and_upload_provider.sh --profile TOOLS/config/dev 3.1.13
|
||||
./TOOLS/scripts/03_build_and_upload_provider.sh --profile TOOLS/config/dev 2.0.18
|
||||
```
|
||||
|
||||
## Быстрая заливка (без перегенерации YAML/Go)
|
||||
@@ -83,14 +87,14 @@ cd ~/tf_provider
|
||||
Если YAML'ы и Go-код уже сгенерированы и не менялись — только шаг 3:
|
||||
|
||||
```bash
|
||||
# DEV
|
||||
./TOOLS/scripts/03_build_and_upload_provider.sh --profile TOOLS/config/dev 3.1.13
|
||||
# DEV (2.*)
|
||||
./TOOLS/scripts/03_build_and_upload_provider.sh --profile TOOLS/config/dev 2.0.18
|
||||
|
||||
# TEST
|
||||
./TOOLS/scripts/03_build_and_upload_provider.sh --profile TOOLS/config/test 5.1.17
|
||||
# TEST (3.*)
|
||||
./TOOLS/scripts/03_build_and_upload_provider.sh --profile TOOLS/config/test 3.0.1
|
||||
|
||||
# PROD
|
||||
./TOOLS/scripts/03_build_and_upload_provider.sh --profile TOOLS/config/prod 2.1.23
|
||||
# PROD (1.*)
|
||||
./TOOLS/scripts/03_build_and_upload_provider.sh --profile TOOLS/config/prod 1.0.1
|
||||
```
|
||||
|
||||
Креды S3 подхватываются из `secrets/.s3cfg_registry`. Или через env:
|
||||
@@ -129,7 +133,7 @@ curl -s https://tf-registry.containerk8s.services.ngcloud.ru/v1/providers/nubes/
|
||||
|
||||
## Актуальные версии
|
||||
|
||||
Файл [`VERSIONS.md`](VERSIONS.md) — единственный источник правды. После каждой заливки — обновить.
|
||||
Файл [`../VERSIONS.md`](../VERSIONS.md) — единственный источник правды. После каждой заливки — обновить.
|
||||
|
||||
## Terraform-конфиг пользователя
|
||||
|
||||
@@ -138,7 +142,7 @@ terraform {
|
||||
required_providers {
|
||||
nubes = {
|
||||
source = "tf-registry.containerk8s.services.ngcloud.ru/nubes-dev/nubes"
|
||||
version = "3.1.13"
|
||||
version = "2.0.18"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,12 @@
|
||||
# План миграции в Nubes Managed Kubernetes — Инструкции для агента
|
||||
|
||||
> ⚠️ **Пути `universal_rebuild/*` в этом плане — от ПРЕЖНЕЙ раскладки репозитория.** Актуально:
|
||||
> `universal_rebuild/internal/*` → `provider/internal/*`; `universal_rebuild/resources_yaml` →
|
||||
> `generated/<стенд>/resources_yaml`; `universal_rebuild/tools/gen` → `TOOLS/resource-generator`.
|
||||
> Также план писался ДО смены схемы версий: актуально `prod=1.*`, `dev=2.*`, `test=3.*`.
|
||||
> Часть про миграцию `registry.kube5s.ru` → `registry.nubes.ru` — ИСТОРИЧЕСКАЯ: актуальный реестр
|
||||
> `tf-registry.containerk8s.services.ngcloud.ru` (бакет `nubes-terraform-registry`).
|
||||
|
||||
**Создан:** 2026-03-13 (Opus 4.6)
|
||||
**Исполнитель:** Sonnet 4.6
|
||||
**Статус:** Ожидает исполнения
|
||||
@@ -24,7 +31,7 @@ Nubes (nubes.ru) — российский cloud-провайдер, собств
|
||||
**Обязательно прочитать перед работой:**
|
||||
- `REPO_CONTENTS.md` — карта репозитория
|
||||
- `.github/copilot-instructions.md` — правила работы (IMMUTABILITY POLICY)
|
||||
- `docs/CODEBASE_ANALYSIS_AND_ROADMAP.md` — анализ кодовой базы
|
||||
- `NOTES/30_analysis/CODEBASE_ANALYSIS_AND_ROADMAP.md` — анализ кодовой базы
|
||||
|
||||
---
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
# HOW_TO — все инструкции проекта
|
||||
|
||||
Здесь лежат **общие инструкции**: как собрать/залить провайдер, как добавить сервис, как устроены
|
||||
процессы. Отсюда начинать, если нужно что-то «сделать руками».
|
||||
|
||||
> Публикуемая пользовательская документация — в `../docs/` (mkdocs).
|
||||
> Рабочие материалы (планы, промпты, анализы) — в `../NOTES/`.
|
||||
|
||||
---
|
||||
|
||||
## Индекс: что нужно → какой файл
|
||||
|
||||
| Нужно | Файл | Кому |
|
||||
|---|---|---|
|
||||
| **Собрать и залить провайдер** (YAML → Go → бинарник → S3) | [`HOWTO-UPLOAD.md`](HOWTO-UPLOAD.md) | Релиз-инженеру |
|
||||
| **Полный DevOps-ранбук пайплайна** (4 шага: генерация, ресурсы+доки, сборка, публикация доков) + GPG-bootstrap | [`DEVOPS_BUILD_PIPELINE.md`](DEVOPS_BUILD_PIPELINE.md) | DevOps |
|
||||
| **Добавить новый сервис** в провайдер (полный цикл) | [`HOWTO_ADD_NEW_SERVICE.md`](HOWTO_ADD_NEW_SERVICE.md) | Разработчику провайдера |
|
||||
| **Имплементировать новый managed-сервис** (со стороны облака) | [`HOWTO_IMPLEMENT_NEW_CLOUD_SERVICE.md`](HOWTO_IMPLEMENT_NEW_CLOUD_SERVICE.md) | DevOps облака |
|
||||
| **Понять, как всё устроено на практике** (закрытый developer-guide) | [`howitwasdone.md`](howitwasdone.md) | Разработчику провайдера |
|
||||
| **Генерация документации** (архитектура, пайплайн, правила для LLM) | [`LLM_DOCS_GENERATION.md`](LLM_DOCS_GENERATION.md) | Разработчику доков |
|
||||
| **План миграции + runbook реестра** (обновление, откат, troubleshooting, мониторинг) | [`MIGRATION_PLAN_FOR_AGENT.md`](MIGRATION_PLAN_FOR_AGENT.md) | Агенту/инженеру |
|
||||
|
||||
---
|
||||
|
||||
## Короткий путь: собрать и залить (3 шага)
|
||||
|
||||
```bash
|
||||
cd /home/naeel/TF/tf_provider
|
||||
./TOOLS/scripts/01_generate_yamls.sh --profile TOOLS/config/dev
|
||||
./TOOLS/scripts/02_generate_resources_and_docs_v2.sh --profile TOOLS/config/dev
|
||||
./TOOLS/scripts/03_build_and_upload_provider.sh --profile TOOLS/config/dev 2.0.18
|
||||
```
|
||||
|
||||
Полные детали, требования, проверка после заливки и структура S3 — в [`HOWTO-UPLOAD.md`](HOWTO-UPLOAD.md).
|
||||
|
||||
## Текущие версии (источник правды)
|
||||
|
||||
[`../VERSIONS.md`](../VERSIONS.md). Схема нумерации: **prod = `1.*`, dev = `2.*`, test = `3.*`**
|
||||
(легаси `prod=2.*`, `dev=3.*`, `test=5.*`, `0.0.1` — НЕ использовать). Обоснование схемы:
|
||||
[`../NOTES/10_plans/PLAN_FLASH_reversion_cleanup.md`](../NOTES/10_plans/PLAN_FLASH_reversion_cleanup.md).
|
||||
|
||||
---
|
||||
|
||||
## Где лежит остальное (чтобы не искать вслепую)
|
||||
|
||||
| Тема | Где |
|
||||
|---|---|
|
||||
| Операционные runbook'и (API-токены, стенды, мониторинг, откат, тестирование, реестр) | `../docs/ops/` |
|
||||
| Внутренние справки/разборы по сборке и архитектуре | `../docs/help/` (напр. `BUILD.md`, `build-and-publish.md`) |
|
||||
| Пайплайн публикации документации | `../DOCS_PIPELINE/README.md`, `../DOCS_PIPELINE/publish-docs.sh` |
|
||||
| Правила генерации кода провайдера (ОБЯЗАТЕЛЬНЫ для генератора) | `../TOOLS/ARCHITECTURE.md` |
|
||||
| Скрипты пайплайна | `../TOOLS/scripts/` |
|
||||
| Конфиги стендов и общий реестр | `../TOOLS/config/` (`registry.env`, `<стенд>/profile.env`, `services_list.txt`) |
|
||||
| Секреты (не коммитить) | `../secrets/` |
|
||||
| Текущая задача по IaC/`modify` | `../NOTES/40_chat_summaries/CHAT_RESUME_IAC_2026-09-24.md` |
|
||||
|
||||
---
|
||||
|
||||
## ⛔ Частые грабли (не наступать)
|
||||
|
||||
- **Не вызывать** устаревшие бинарники `TOOLS/*/bin/` — скрипт `02_*` сам пересобирает генераторы.
|
||||
- **Не путать** схемы версий: только `prod=1.*`, `dev=2.*`, `test=3.*`.
|
||||
- **Не использовать** старый API `index.cfm` и хосты `registry.kube5s.ru` / `deck-api.ngcloud.ru` — закрыты.
|
||||
- **GPG-ключ** подписи не перегенерировать: иначе registry и `terraform init` сломаются
|
||||
(`authentication signature from unknown issuer`).
|
||||
- **S3-бакеты разделены**: бинарники — `nubes-terraform-registry`, документация — `terraform-registry`.
|
||||
@@ -2,6 +2,13 @@
|
||||
<!-- Актуальный API: https://lk-api-gateway.ngcloud.ru/api/v1/svc -->
|
||||
# How It Was Done — Developer Guide (закрытая страница)
|
||||
|
||||
> ⚠️ **Пути в этом документе — от ПРЕЖНЕЙ раскладки репозитория (`universal_rebuild/*`).**
|
||||
> Актуальное соответствие: `universal_rebuild/internal/*` → `provider/internal/*`;
|
||||
> `universal_rebuild/resources_yaml` → `generated/<стенд>/resources_yaml`;
|
||||
> `universal_rebuild/tools/gen` → `TOOLS/resource-generator`;
|
||||
> `universal_rebuild/tools/service_params_gen` → `TOOLS/yaml-generator`.
|
||||
> Смысл описанного сохраняется, но пути в тексте сверять по этому соответствию.
|
||||
|
||||
**Filename & Versioning:** howitwasdone.md / 2026‑02‑04 / Draft v1
|
||||
|
||||
Этот документ — единый технический мануал. Он доступен только по прямой ссылке и не включён в публичную навигацию.
|
||||
@@ -0,0 +1,224 @@
|
||||
> ⛔ **ЛОЖНЫЙ ПУТЬ — ОТМЕНЕНО (пометка 2026-09-24). ТАК ДЕЛАТЬ НЕЛЬЗЯ.**
|
||||
> Отменёный заход: доменная логика модификаторов вшивалась в универсальный генератор
|
||||
> (`kind: modifier` в YAML + реестр в `yaml-generator`, `delete_strategy`/`inverse`). Ломало агностичность
|
||||
> провайдера и порождало баги. Файл сохранён ТОЛЬКО как история, не источник истины.
|
||||
> Актуально: `NOTES/40_chat_summaries/CHAT_RESUME_IAC_2026-09-24.md`.
|
||||
|
||||
# ПЛАН реализации: редизайн ресурсов-модификаторов (kind: modifier)
|
||||
|
||||
Основа: `HISTORY/OPUS/2026-09-22_modifier_architecture_project.md`.
|
||||
Ревью плана: `HISTORY/OPUS/2026-09-22_modifier_plan_review.md`.
|
||||
Цель — закрыть все классы багов A–E, без костылей, по согласованной архитектуре.
|
||||
|
||||
Порядок шагов (исправлен по ревью): шаблон (4) зависит от core/resources_core (5–7),
|
||||
поэтому: 1 → 2 → 3 → 5 → 6 → 7 → 4 → 8 → регенерация → 9 → 10.
|
||||
|
||||
---
|
||||
|
||||
## Шаг 1. Контракт YAML в `TOOLS/lib/types.go`
|
||||
|
||||
Файл: `TOOLS/lib/types.go`, `OperationSpec`.
|
||||
|
||||
Добавить поля (тег yaml, omitempty):
|
||||
```go
|
||||
DeleteStrategy string `yaml:"delete_strategy,omitempty"` // "" → noop_warn
|
||||
Idempotency string `yaml:"idempotency,omitempty"` // "" → none
|
||||
DeleteParams []ParamSpec `yaml:"delete_params,omitempty"`
|
||||
```
|
||||
Enum `delete_strategy`: `noop_warn` | `inverse` | `error`.
|
||||
Enum `idempotency`: `none` | `check_before_run`.
|
||||
|
||||
Проверка: `TOOLS/resource-generator` получает поля через алиас `OperationSpec = lib.OperationSpec` — отдельной правки не нужно, но `go build ./...` в lib и в resource-generator.
|
||||
|
||||
---
|
||||
|
||||
## Шаг 2. GenModifier — производные поля
|
||||
|
||||
Файл: `TOOLS/resource-generator/internal/types/types.go`, `GenModifier`.
|
||||
|
||||
Добавить:
|
||||
```go
|
||||
DeleteStrategy string // нормализованный enum (noop_warn|inverse|error)
|
||||
Idempotency string // none|check_before_run
|
||||
DeleteParams []Param // из spec.DeleteParams (ConvertParams), только при inverse
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Шаг 3. LoadSpecs — заполнение modifier + валидация
|
||||
|
||||
Файл: `TOOLS/resource-generator/internal/loader/loader.go`, ветка `op.Kind == "modifier"`.
|
||||
|
||||
До `continue`:
|
||||
- `modifier.DeleteStrategy = normalizeDeleteStrategy(op.DeleteStrategy)` (пусто → `noop_warn`);
|
||||
- `modifier.Idempotency = normalizeIdempotency(op.Idempotency)` (пусто → `none`);
|
||||
- `modifier.DeleteParams = ConvertParams(op.DeleteParams)` (при inverse).
|
||||
|
||||
Helpers `normalizeDeleteStrategy`/`normalizeIdempotency` — добавить в `loader.go`
|
||||
(тот же пакет, рядом с веткой modifier).
|
||||
|
||||
Файл: `loader.go`, `ValidateSpec` — расширить fail-fast для modifier:
|
||||
- `delete_strategy` вне enum → ошибка;
|
||||
- `delete_strategy == "inverse"` и пуст `delete_params` → ошибка;
|
||||
- каждый `delete_params.code` обязан существовать в `op.Params` (сравнение по lower-code) → иначе ошибка;
|
||||
- `idempotency` вне enum → ошибка.
|
||||
|
||||
---
|
||||
|
||||
## Шаг 4. Шаблон `modifier.go` — редизайн
|
||||
|
||||
Файл: `TOOLS/resource-generator/internal/templates/modifier.go`.
|
||||
|
||||
4.1. **Убрать `CompactParams`** — в Create/Update передавать map напрямую
|
||||
(все заданные поля; решение о досылке — в core).
|
||||
|
||||
4.2. **Единый `reconcile()`** — вынести общее тело Create/Update в приватный метод
|
||||
`reconcile(ctx, model *Model, override map[string]string)`, вызываемый из Create и Update
|
||||
(override=nil). Устраняет дубль веток. **override нужен для Delete=inverse** (см. 4.4),
|
||||
так как Delete не имеет plan — только state.
|
||||
|
||||
4.3. **ID = identity** — `plan.ID = BuildActionID(instanceUID, modifierName)`
|
||||
(убрать operation из ID). Реализовать через существующий `BuildActionID(instanceUID, "", modifierName)`
|
||||
или новый helper `BuildModifierID(instanceUID, modifierName)`.
|
||||
⚠️ **миграция state:** смена формата ID изменит ID уже задеплоенных модификаторов →
|
||||
Terraform форснёт replace. Принять решение ДО: сохранить старый формат ИЛИ явный
|
||||
state-migration план. По умолчанию — сохранить формат `uid:operation:modifier`, не менять формат.
|
||||
|
||||
4.4. **Delete по стратегии**:
|
||||
```
|
||||
{{- if eq .DeleteStrategy "error" }}
|
||||
Delete → AddError (запрет destroy); ⚠️ конфликт с replace: replace = Delete→Create,
|
||||
при error пользователь не сможет заменить модификатор. Решение: запретить replace
|
||||
у error-модификаторов (документировать) или отличить «чистый destroy» от replace.
|
||||
{{- else if eq .DeleteStrategy "inverse" }}
|
||||
Delete → reconcile(state-model, override=delete_params)
|
||||
(delete_params — финальные wire-строки: "false", готовый JSON; обработать как override)
|
||||
{{- else }}
|
||||
Delete → RemoveResource + AddWarning («эффект остаётся на платформе»)
|
||||
{{- end }}
|
||||
```
|
||||
|
||||
4.5. **Pre-check idempotency** — в reconcile при `eq .Idempotency "check_before_run"`:
|
||||
передавать флаг в вызов операции (см. шаг 6). ⚠️ при unknown (computed ref) pre-check
|
||||
skip — сравнение невозможно.
|
||||
|
||||
---
|
||||
|
||||
## Шаг 5. JSON-эквивалентность в нейтральный пакет (снять цикл импорта)
|
||||
|
||||
Проблема: `JSONStringsEquivalent` в `resources_core`, а comparison нужен в `core`.
|
||||
- создать `provider/internal/core/jsonutil/jsonutil.go`:
|
||||
перенести `JSONStringsEquivalent` + `normalizeJSONIfPossible` + `encodeCanonicalJSON` +
|
||||
`writeCanonicalJSON` + `normalizeJSONScalarsToStrings` из `resources_core/json_normalize.go`;
|
||||
- `resources_core/json_normalize.go` — **оставить реэкспорт-обёртку** `JSONStringsEquivalent`
|
||||
(не заменять вызовы по resources_core — иначе диф на инстансы).
|
||||
|
||||
Проверка: `go build ./...`, нет цикла импорта.
|
||||
|
||||
---
|
||||
|
||||
## Шаг 6. core — pre-check `modifierDesiredEqualsCurrent`
|
||||
|
||||
Файл: `provider/internal/core/operation_run_bycode.go` (или новый `modifier_compare.go`).
|
||||
|
||||
Добавить (unexported, вызов внутри core):
|
||||
```go
|
||||
func (c *UniversalClient) modifierDesiredEqualsCurrent(
|
||||
desired map[string]string, cfsParams []universalCfsParam) bool
|
||||
```
|
||||
Логика:
|
||||
- маппинг code→param по **двум** алиасам: `p.Code` И `p.SvcOperationCfsParam`
|
||||
(как в operation_run_bycode.go:50-58);
|
||||
- для каждого desired-кода → live `ParamValue`;
|
||||
- bool/int/string → нормализовать обе стороны `normalizeUniversalValueV6` + сравнение строк;
|
||||
- map-fixed → `jsonutil.JSONStringsEquivalent`;
|
||||
- **array-map-fixed → `jsonutil.JSONStringsEquivalent` по сырым значениям, НЕ через normalize**
|
||||
(`normalizeUniversalValueV6` не строит дефолт для array-map-fixed, params.go:33);
|
||||
- desired — только явно заданные коды (до досылки live/default);
|
||||
- если desired содержит unknown (computed ref) — сравнение невозможно, pre-check пропустить.
|
||||
|
||||
Опционально: добавить в `RunInstanceOperationUniversalByCode` параметр `idempotent bool`
|
||||
(или новый метод-обёртка). В `operation_run_bycode.go` после `fetchOperationCfsParams`:
|
||||
```
|
||||
if idempotent && c.modifierDesiredEqualsCurrent(paramsByID, cfsParams) {
|
||||
return nil // skip run
|
||||
}
|
||||
```
|
||||
idle-гейт (`waitForInstanceIdle`) уже стоит выше — не трогать.
|
||||
|
||||
---
|
||||
|
||||
## Шаг 7. Передача флага `idempotent` вплоть до client
|
||||
|
||||
Цепочка: шаблон → `resources_core.RunOperationByCodeWithTimeout` → `core.RunInstanceOperationUniversalByCode`.
|
||||
- **добавить НОВЫЙ метод `RunOperationByCodeIdempotent(...)` в `resources_core/crud.go`**,
|
||||
НЕ менять сигнатуру `RunOperationByCodeWithTimeout` (его зовут инстансы);
|
||||
- пробросить флаг в `RunInstanceOperationUniversalByCode` (новый параметр или обёртка).
|
||||
|
||||
---
|
||||
|
||||
## Шаг 8. YAML-разметка (источник-канон) в `TOOLS/yaml-generator`
|
||||
|
||||
Источник-канон — реестр исключений `serviceSpecificModifiers` в
|
||||
`TOOLS/yaml-generator/main.go` (Ключ — имя сервиса → имя modifier).
|
||||
`generated/dev` перегенерируется — туда НЕ вносить вручную.
|
||||
|
||||
Контракт в `lib.OperationSpec` (алиас в обоих генераторах), значит yaml-generator
|
||||
должен проставлять флаги при маршале. Расширить реестр со `map[string]string`
|
||||
до структуры, несущей: `ModifierName`, `DeleteStrategy`, `Idempotency`,
|
||||
`DeleteParams []struct{Code,Value}`:
|
||||
|
||||
```go
|
||||
type modifierException struct {
|
||||
ModifierName string
|
||||
DeleteStrategy string // noop_warn | inverse | error
|
||||
Idempotency string // none | check_before_run
|
||||
DeleteParams []deleteParam // только для inverse
|
||||
}
|
||||
type deleteParam struct { Code, Value string }
|
||||
|
||||
var serviceSpecificModifiers = map[string]modifierException{
|
||||
"vc_org": {ModifierName: "ip_space", DeleteStrategy: "error", Idempotency: "check_before_run"},
|
||||
"vc_nsxt": {ModifierName: "network", DeleteStrategy: "inverse",
|
||||
DeleteParams: []deleteParam{{"needEnableAVI", "false"}}},
|
||||
}
|
||||
```
|
||||
|
||||
В цикле над ops (там, где `Kind="modifier"`): проставить `op.DeleteStrategy`,
|
||||
`op.Idempotency`, `op.DeleteParams`.
|
||||
|
||||
⚠️ При переходе с `map[string]string` на структуру: `ModifierName` берётся из структуры
|
||||
(сейчас `modName, ok := serviceSpecificModifiers[name]` — строка 95 main.go).
|
||||
|
||||
---
|
||||
|
||||
## Порядок коммитов (по смыслу)
|
||||
|
||||
1. `feat(lib): delete_strategy/idempotency/delete_params в OperationSpec`
|
||||
2. `feat(gen): GenModifier расширение + LoadSpecs + ValidateSpec + normalize-helpers`
|
||||
3. `refactor(core): вынести JSON-эквивалентность в jsonutil (+реэкспорт)`
|
||||
4. `feat(core): modifierDesiredEqualsCurrent + RunOperationByCodeIdempotent`
|
||||
5. `feat(gen): шаблон modifier — reconcile(override), Delete стратегия, ID identity`
|
||||
6. `feat(yaml): реестр исключений модификаторов (delete_strategy/idempotency)`
|
||||
7. `test(core,gen): unit-кейсы`
|
||||
8. `chore(dev): bump версии`
|
||||
|
||||
---
|
||||
|
||||
## Открытый вопрос — закрыт
|
||||
|
||||
Источник-канон — реестр `serviceSpecificModifiers` в `TOOLS/yaml-generator/main.go`.
|
||||
Разметка `delete_strategy`/`idempotency` расширяет этот реестр, а НЕ правится вручную
|
||||
в `generated/dev`.
|
||||
|
||||
---
|
||||
|
||||
## Решения, нуждающиеся в подтверждении (из ревью)
|
||||
|
||||
1. **ID=identity → РЕШЕНО: формат ID НЕ меняем** (оставить `uid:operation:modifier`).
|
||||
Смена формата форснёт replace у задеплоенных модификаторов и вызовет баг E.
|
||||
Идемпотентность — через pre-check, не через ID. Шаг 4.3 отменён (ID остаётся как есть).
|
||||
2. **`error` + replace.** Пользователь не сможет заменить error-модификатор.
|
||||
Предлагаю: оставить `error` только для «чистого» destroy, документировать запрет replace.
|
||||
3. **Разметка по default** — `ip_space`: `delete_strategy=error`, `idempotency=check_before_run`;
|
||||
`network`: `delete_strategy=inverse`, delete_params=[needEnableAVI=false], idempotency=none.
|
||||
+8
-4
@@ -1,3 +1,7 @@
|
||||
> ⚠️ **ПЕРЕКРЫТ (пометка 2026-09-24). НЕ ИСПОЛЬЗОВАТЬ.**
|
||||
> Версия `0.0.1` объявлена ЛЕГАСИ в `PLAN_FLASH_reversion_cleanup.md`.
|
||||
> Актуальная схема: prod=`1.*`, dev=`2.*`, test=`3.*`.
|
||||
|
||||
# План: перегенерация провайдеров всех стендов (версия 0.0.1)
|
||||
|
||||
> Для Flash. Генерацию выполняет Flash по этому плану. Документацию НЕ трогать.
|
||||
@@ -53,16 +57,16 @@ cd /home/naeel/TF/tf_provider
|
||||
|
||||
| Стенд | Namespace | Бинарники в S3 |
|
||||
|---|---|---|
|
||||
| dev | `nubes-dev` | `nubes-terraform-registry/tf-registry.containerk8s.services.ngcloud.ru/nubes-dev/nubes/0.0.1/` |
|
||||
| test | `nubes-test` | `.../nubes-test/nubes/0.0.1/` |
|
||||
| prod | `nubes` | `.../nubes/nubes/0.0.1/` |
|
||||
| dev | `nubes-dev` | `terraform-registry/tf-registry.containerk8s.services.ngcloud.ru/nubes-dev/nubes/0.0.1/` |
|
||||
| test | `nubes-test` | `terraform-registry/tf-registry.containerk8s.services.ngcloud.ru/nubes-test/nubes/0.0.1/` |
|
||||
| prod | `nubes` | `terraform-registry/tf-registry.containerk8s.services.ngcloud.ru/nubes/nubes/0.0.1/` |
|
||||
|
||||
## Предусловия — ПРОВЕРЕНО, всё готово
|
||||
- Go 1.23.1, docker 29.1.3, `mc`, GPG (`secrets/private_key.asc`, `public_key.asc`).
|
||||
- Токены API: `secrets/{dev,test,prod}.token` на месте.
|
||||
- API-эндпоинты доступны (HTTP 403 без токена — ожидаемо, токен передаёт 01).
|
||||
- `TOOLS/config/<стенд>/operation_timeouts.json` на месте.
|
||||
- `registry.env`: `REGISTRY_HOSTNAME=tf-registry.containerk8s.services.ngcloud.ru`, `S3_BUCKET=nubes-terraform-registry`.
|
||||
- `registry.env`: `REGISTRY_HOSTNAME=tf-registry.containerk8s.services.ngcloud.ru`, `S3_BUCKET=terraform-registry`.
|
||||
|
||||
## Чего НЕ делать
|
||||
- НЕ запускать `04_build_and_publish_docs.sh` (документация не нужна сейчас).
|
||||
@@ -0,0 +1,23 @@
|
||||
# 10_plans — планы работ
|
||||
|
||||
Планы, связанные с версионированием, перегенерацией провайдера и модификаторами.
|
||||
|
||||
## Файлы
|
||||
|
||||
| Файл | О чём | Статус |
|
||||
|---|---|---|
|
||||
| `PLAN_FLASH_reversion_cleanup.md` | Чистка реестра (S3) от легаси-версий + новая схема нумерации: **prod=`1.*`, dev=`2.*`, test=`3.*`**; порядок перегенерации стендов | ✅ Актуально (источник схемы нумерации) |
|
||||
| `PLAN_modifier_redesign.md` | Редизайн «ресурсов-модификаторов» (`kind: modifier`) — шаги 1..10, `delete_strategy`, `idempotency`, `inverse` | ⛔ **Отменённый путь** (баннер в файле): логику вшивали в универсальный генератор |
|
||||
| `PLAN_regenerate_providers_0.0.1.md` | Перегенерация всех стендов версией `0.0.1` | ⚠️ **Перекрыт**: `0.0.1` объявлен легаси в `PLAN_FLASH_reversion_cleanup.md` |
|
||||
|
||||
## Как читать
|
||||
|
||||
- Схема нумерации и порядок заливки — только из `PLAN_FLASH_reversion_cleanup.md`.
|
||||
- `PLAN_modifier_redesign.md` читать **только как историю**: он описывает заход, от которого отказались
|
||||
(метки `kind: modifier` в YAML + реестр в `yaml-generator`). Актуальные выводы по модификаторам —
|
||||
в `../40_chat_summaries/CHAT_RESUME_IAC_2026-09-24.md`.
|
||||
|
||||
## Не путать
|
||||
|
||||
Отмена `PLAN_modifier_redesign.md` **не означает**, что модификаторы не нужны. Нужны **отдельные
|
||||
tf-ресурсы под `modify`**, но без доменных меток в универсальном YAML — см. хендовер.
|
||||
@@ -0,0 +1,45 @@
|
||||
# 20_prompts — промпты для LLM
|
||||
|
||||
Промпты, которые отправлялись внешним моделям (Opus / Sol / Sonnet / DeepSeek Flash / GPT‑5.2‑Codex).
|
||||
Ответы моделей лежат отдельно — в `../30_analysis/` и `../40_chat_summaries/`.
|
||||
|
||||
> ⛔ = отменённый («ложный») путь, только история. ✅ = актуально.
|
||||
|
||||
## Статус: актуальное
|
||||
|
||||
| Файл | О чём | Статус |
|
||||
|---|---|---|
|
||||
| `prompt_for_opus_iac_shturval_modify.md` | IaC-развёртывание Штурвала: проблема `modify` и скрытых зависимостей (5 вопросов). **Факты внутри исправлены** (vIPConfigure — replace-семантика, а не накопительная) | ✅ Актуально |
|
||||
| `prompt_for_opus_modifier_global_architecture.md` | Как сделать модификаторы **НЕ инвазивным дополнением**: YAML — чистая выгрузка API, модификаторы — не ветка генератора | ✅ Актуальное направление (не реализовано) |
|
||||
| `prompt_for_opus_modifiable_architecture.md` | Простая логика «изменяемости» параметров (CreateOnly vs Modifiable) | ⚠️ Статус не определён |
|
||||
| `prompt_for_opus_review.md` | Код-ревью + оценка архитектуры, 3 задачи roadmap (в т.ч. `vcOrg modify` — динамическая аллокация IP) | ⚠️ Статус не определён; ответ — `../30_analysis/opus_review_answer.md` |
|
||||
|
||||
## Статус: отменённый заход (модификаторы со метками в YAML)
|
||||
|
||||
| Файл | О чём |
|
||||
|---|---|
|
||||
| `prompt_for_opus_modifier_architecture_full.md` ⛔ | Спроектировать с нуля архитектуру `kind: modifier` |
|
||||
| `prompt_for_opus_modifier_architecture_q3.md` ⛔ | Уточнения к архитектуре модификаторов (расхождения с кодом) |
|
||||
| `prompt_for_opus_modifier_null_bug.md` ⛔ | Баг: modify-модификатор сбрасывает create-поля (`needEnableAVI` true→false) |
|
||||
| `prompt_for_opus_modifier_plan_review.md` ⛔ | Ревью плана редизайна модификаторов |
|
||||
| `prompt_for_opus_modifiers_review.md` ⛔ | Код-ревью модификаторов в универсальном провайдере |
|
||||
| `prompt_for_opus_modifier_review_2.md` ⛔ | Ревью `vc_nsxt` / `vc_org` + досылка modify-params через `paramValue` |
|
||||
| `prompt_for_opus_inverse_architecture.md` ⚠️ | Архитектура inverse-отката модификаторов (`delete_params`, `zero_count`, `off_value`). Модель — от отменённого механизма; факты внутри (count=0, `no-needed`) переиспользуются |
|
||||
|
||||
## Промпты по другим темам (не про модификаторы)
|
||||
|
||||
| Файл | О чём |
|
||||
|---|---|
|
||||
| `prompt_for_opus_bugs.md` | Баг: `FindInstanceByDisplayName` не находит существующий инстанс |
|
||||
| `prompt_for_opus_duplicate.md` | subresource duplicate/exist + `state_out` |
|
||||
| `prompt_for_flash_fix_tainted_replace.md` | ТЗ для DeepSeek Flash: убрать create-time проверку существования из `ModifyPlan` |
|
||||
| `prompt_for_sol_dev_generator_bug.md` | Проверка решения бага Dev-генератора |
|
||||
| `prompt_for_sonnet_docs_ux.md` | BRIEF: анализ и рекомендации по документации провайдера (UX) |
|
||||
| `prompt_deepseek_flash.txt` | Роль: технический редактор документации; правила «не менять параметры/типы/ID» |
|
||||
| `gpt5_5.2_codex_universal_provider_prompt.md` | Задачи по `terra`-провайдеру (Codex) |
|
||||
|
||||
## Ответы на эти промпты
|
||||
|
||||
- `../30_analysis/opus_review_answer.md` — ответ на `prompt_for_opus_review.md`
|
||||
- `../30_analysis/OPUS_ANSWER_IAC_SHTURVAL_MODIFY_2026-09-23.md` — ответ на `prompt_for_opus_iac_shturval_modify.md`
|
||||
- `../HISTORY/OPUS/` и `../HISTORY/SONNET/` — исторические ответы по датам
|
||||
+2
-2
@@ -15,9 +15,9 @@ Constraints:
|
||||
---
|
||||
|
||||
## 2) Файлы для изучения (указать полные пути)
|
||||
- docs/ARCHITECTURE_NEW.md — архитектурный обзор
|
||||
- NOTES/30_analysis/ARCHITECTURE_NEW.md — архитектурный обзор
|
||||
- docs/README.md, docs/index.md — документация / навигация
|
||||
- docs/howitwasdone.md — история решений
|
||||
- NOTES/50_process/howitwasdone.md — история решений
|
||||
- docs/ai_universal_provider_gen.md — процесс генерации провайдера (YAML → Go)
|
||||
- universal_rebuild/tools/gen/main.go — генератор (основная логика)
|
||||
- universal_rebuild/internal/resources_gen/* — примеры сгенерированных ресурсов
|
||||
@@ -0,0 +1,130 @@
|
||||
# ТЗ для DeepSeek Flash: убрать create-time проверку существования из `ModifyPlan`
|
||||
|
||||
Дата: 2026-09-21 | Статус: не сделано | Версия провайдера на момент бага: 2.0.6
|
||||
|
||||
## Цель
|
||||
|
||||
Починить `terraform destroy` (и любую `tainted`-замену), который падает с
|
||||
`РЕСУРС С ТАКИМ ИМЕНЕМ УЖЕ СУЩЕСТВУЕТ (RUNNING)`.
|
||||
|
||||
## Контекст
|
||||
|
||||
- Репозиторий: `/home/naeel/TF/tf_provider`
|
||||
- Провайдер: `terraform-provider-nubes`, Go, `terraform-plugin-framework v1.8.0`
|
||||
- Ресурсы генерируются шаблоном, **НЕ правятся руками**
|
||||
- Модуль провайдера живёт в `provider/` (не в корне репозитория)
|
||||
|
||||
## Симптом
|
||||
|
||||
```
|
||||
$ terraform destroy
|
||||
nubes_vc_vdc.vdc: Refreshing state... [id=db2cefc3-...]
|
||||
nubes_vc_nsxt.edge: Refreshing state... [id=8AAEC14D-...]
|
||||
│ Error: РЕСУРС С ТАКИМ ИМЕНЕМ УЖЕ СУЩЕСТВУЕТ (RUNNING)
|
||||
│ with nubes_vc_nsxt.edge,
|
||||
│ on edge.tf line 1, in resource "nubes_vc_nsxt" "edge":
|
||||
```
|
||||
|
||||
То же самое при обычном `terraform plan`.
|
||||
|
||||
## Причина (подтверждена фактами)
|
||||
|
||||
1. `nubes_vc_nsxt.edge` в state помечен **`tainted`** (следствие прошлой неудачной
|
||||
apply с `vdc_group_uid`: `Provider returned invalid result object after apply`).
|
||||
Проверка: `terraform.tfstate` → `instances[].status == "tainted"`.
|
||||
2. Tainted-ресурс Terraform обязан **заменить** (destroy + create). Это видно в плане:
|
||||
|
||||
```
|
||||
# nubes_vc_nsxt.edge is tainted, so must be replaced
|
||||
-/+ resource "nubes_vc_nsxt" "edge" {
|
||||
```
|
||||
|
||||
3. `terraform destroy` сначала выполняет **внутренний обычный plan**
|
||||
(`Context.destroyPlan: calling Context.plan` — видно в `TF_LOG=TRACE`), и уже
|
||||
на этом шаге планируется замена edge.
|
||||
4. Create-узел замены вызывает `ModifyPlan` с **prior state = null**, поэтому guard
|
||||
`if state != nil && !state.ID.IsNull() && ...` пропускается, и доходит до
|
||||
create-time проверки существования.
|
||||
5. Проверка находит **живой** инстанс в облаке (старый edge ещё не удалён — удаление
|
||||
идёт на apply) → `РЕСУРС С ТАКИМ ИМЕНЕМ УЖЕ СУЩЕСТВУЕТ (RUNNING)` → plan падает →
|
||||
`destroy` не начинается.
|
||||
|
||||
Ключевое: на уровне `ModifyPlan` **невозможно** отличить «создание нового ресурса»
|
||||
от «create-узла замены» — у обоих prior state = null. Поэтому проверки существования
|
||||
в `ModifyPlan` быть не должно в принципе.
|
||||
|
||||
Доказательство, что вызов идёт из `ModifyPlan`: `/tmp/nubes_find_debug.log` содержит
|
||||
`[FIND-DEBUG] PlanExistingResourceDiagnostics entered: serviceId=22 name="fullpipe-edge"`.
|
||||
Эту строку пишет только Plan-функция; `Create...` в этот лог не пишет.
|
||||
|
||||
## Правка
|
||||
|
||||
**Один файл:** `TOOLS/resource-generator/internal/templates/instance.go`, шаблон метода `ModifyPlan`.
|
||||
|
||||
Удалить целиком блок от строки
|
||||
|
||||
```go
|
||||
if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
|
||||
return
|
||||
}
|
||||
```
|
||||
|
||||
до строки
|
||||
|
||||
```go
|
||||
resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, {{.ServiceID}}, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs, {{.SupportsSuspendDestroy}})...)
|
||||
```
|
||||
|
||||
включительно. Это весь хвост `ModifyPlan` после блока «Missing required attribute»:
|
||||
`adoptExistingOnCreate`, resolve refSvc, `params`, `desiredDomain`, `domainServiceIDs`
|
||||
и сам вызов диагностики.
|
||||
|
||||
### Что НЕ трогать
|
||||
|
||||
- destroy-guard `if req.Plan.Raw.IsNull() { return }` — **оставить**;
|
||||
- блок create-only проверок (по `state.ID`) — **оставить**;
|
||||
- блок «Missing required attribute» — **оставить**;
|
||||
- `Create` — там вызов `CreateExistingResourceDiagnosticsWithDomainAndServices`
|
||||
**остаётся**: проверка выполняется на apply, уже после удаления старого инстанса.
|
||||
|
||||
### Побочный эффект (принять как норму)
|
||||
|
||||
Из plan пропадают проверки ref-параметров / domain / существования по имени. Это
|
||||
штатное поведение Terraform: на apply `Create` резолвит refSvc (с ошибкой) и делает
|
||||
проверку существования.
|
||||
|
||||
## Проверка
|
||||
|
||||
```bash
|
||||
cd /home/naeel/TF/tf_provider && ./TOOLS/scripts/02_generate_resources_and_docs_v2.sh --profile TOOLS/config/dev
|
||||
```
|
||||
|
||||
```bash
|
||||
TMP=$(mktemp -d) && cp -R provider "$TMP/provider" && find "$TMP/provider/internal/resources_gen" -maxdepth 1 -type f -name '*.go' -delete && cp generated/dev/go/*.go "$TMP/provider/internal/resources_gen/" && (cd "$TMP/provider" && go build ./...) && echo BUILD_OK && rm -rf "$TMP"
|
||||
```
|
||||
|
||||
Затем проверить сгенерированный код:
|
||||
|
||||
- `generated/dev/go/22_vc_nsxt_resource.go`: в `ModifyPlan` вызова
|
||||
`PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices` больше нет;
|
||||
- в `Create` вызов `CreateExistingResourceDiagnosticsWithDomainAndServices` остался.
|
||||
|
||||
Если после удаления какой-то импорт стал неиспользуемым (`fmt`, `resources_core`) —
|
||||
проверить сборкой. Обычно `Create` сохраняет те же импорты, отдельная правка флага
|
||||
`NeedsFmtImport` не требуется.
|
||||
|
||||
## Что НЕ делать
|
||||
|
||||
- **НЕ собирать и НЕ заливать** провайдер — только правка шаблона + генерация + сборка-проверка.
|
||||
- Не править сгенерированный код руками.
|
||||
- Не трогать `provider/internal/resources_core/resource_diagnostics_required.go`.
|
||||
|
||||
## Критерий готовности
|
||||
|
||||
`BUILD_OK` и в сгенерированном edge `ModifyPlan` нет create-time проверки.
|
||||
|
||||
## Обходной путь без правок (если надо убить стенд прямо сейчас)
|
||||
|
||||
```bash
|
||||
terraform untaint nubes_vc_nsxt.edge && terraform destroy
|
||||
```
|
||||
@@ -0,0 +1,51 @@
|
||||
# Промпт для Opus: IaC-развёртывание Штурвала, проблема `modify` и скрытых зависимостей
|
||||
|
||||
## Правила ответа (жёстко)
|
||||
|
||||
1. НЕ лезь в файлы/репозиторий/сеть. Отвечай ТОЛЬКО по материалу ниже.
|
||||
2. Отвечай КРАТКО, тезисами, по номерам вопросов. Без простыней.
|
||||
3. Токены/секреты/креды НЕ нужны — если захочешь, не упоминай и не проси.
|
||||
4. Если для ответа не хватает данных — прямо пиши «неизвестно», не выдумывай.
|
||||
5. Не предлагай «ручной ЛК / скрипт / пресеты дефолтного окружения» как решение IaC — это уже отклонено (клиенту нужен полноценный IaC).
|
||||
|
||||
## Контекст
|
||||
|
||||
Terraform-провайдер для Nubes Cloud. Клиенту нужен IaC: один конфиг + `terraform apply` = вся инфраструктура. Цепочка Штурвала:
|
||||
|
||||
```
|
||||
vcOrg -> create
|
||||
vcVdc -> create
|
||||
vcNsxt -> create
|
||||
vcOrg -> modify (аллокация внешних IP)
|
||||
vcNsxt -> modify (включить SNAT, указать внешний IP из vcOrg)
|
||||
k8sShturval -> create
|
||||
```
|
||||
|
||||
Операции строго последовательны.
|
||||
|
||||
Факты (подтверждены):
|
||||
- Провайдер генерируется из YAML-спеков. Схема tf-ресурса строится ТОЛЬКО из операции `create`.
|
||||
- `vIPConfigure` (array-map-fixed, sub: name/count) есть только в `modify` vc_org (id 207); в `create` (136) его нет.
|
||||
- `ipSpaceName` (string) есть только в `modify` vc_nsxt (id 111); в `create` (10) его нет.
|
||||
- `vIPConfigure` — **не накопительный, а replace-семантика** (подтверждено `NOTES/30_analysis/ORG_IP_MODIFIER_TEST_2026-09-22.md`): повторный `modify` с тем же `count` не аккумулирует IP (1→1), работает в обе стороны (вверх/вниз/до 0), `count=0` принимается. Значение задаётся целиком, читается из `state.params`.
|
||||
- `ipSpaceName` выводится из цепочки `providerVdc -> providerGateway -> ipSpace`, которую пользователь не знает. Сейчас платформа «подкладывает» недостающие параметры при создании пустой орги.
|
||||
- Допущение платформы: в организации один T0/провайдер-шлюз. Рост числа T0 отложен.
|
||||
- Платформа в движении: форма ресурсов зависит от новых спеков (ждут, придут сначала в sandbox).
|
||||
|
||||
Прецедент (VCD): та же цепочка делается отдельными ресурсами с `depends_on` — `vcd_nsxt_alb_settings` (count + is_active), `vcd_nsxt_alb_edgegateway_service_engine_group` (reserved_virtual_services), `vcd_network_routed_v2`, `vcd_ip_space_custom_quota` (на оргу). Включение/выключение = `count`, inverse = удаление ресурса.
|
||||
|
||||
Разница с каноном: у нас нет отдельного API-объекта под модификацию — только операция `modify` над родителем (Read = чтение родителя, Delete = обратный modify, нужна идемпотентность).
|
||||
|
||||
## Вопросы
|
||||
|
||||
1. `vIPConfigure` уже ведёт себя как replace-состояние (идемпотентно, обе стороны, `count=0` читается из `state.params`). Как это оформить в tf-ресурсе, чтобы Read брал `state.params`, а Delete (inverse) выставлял `count=0` — если отдельного API-объекта нет?
|
||||
|
||||
2. Как провайдер должен получать выводимое значение `ipSpaceName` (цепочка providerVdc → providerGateway → ipSpace): data-source, вычисляемое из state родителя, или иное? Где граница «данные vs логика», что хранить в реестре, что выводить из типа/state?
|
||||
|
||||
3. Как спроектировать форму ресурсов, чтобы не завязываться на допущение «в организации один T0», и что сломается/что менять, если T0 станет больше одного?
|
||||
|
||||
4. Стоит ли ждать новых спеков платформы перед проектированием ресурсов, или форму ресурсов можно зафиксировать уже сейчас так, чтобы она пережила изменение спеков? Что в спеках — блокер, что — нет?
|
||||
|
||||
5. Минимально-инвазивный порядок внедрения: что должно прийти от платформы (spec/API) до того, как мы начинаем кодить, а что можем сделать на стороне провайдера уже сейчас?
|
||||
|
||||
Отвечай по номерам, кратко.
|
||||
@@ -0,0 +1,47 @@
|
||||
# Вопрос: архитектура inverse-отката модификаторов (без чтения файлов)
|
||||
|
||||
ЗАПРЕЩЕНО лезть в файлы репозитория. Отвечай только по тексту ниже. Ответ — максимально краткий (тезисы), но исчерпывающий.
|
||||
|
||||
## Контекст
|
||||
|
||||
Terraform provider для Nubes Cloud. Есть «модификаторы» — отдельные TF-ресурсы, вызывающие операцию `modify` над инстансом по кодам параметров, а не по числовым id. Примеры:
|
||||
- `vc_org` → модификатор `ip_space`, параметр `vIPConfigure` (array-map-fixed) = `[{"name":"internet-ipv4-v1","count":3}]` — выделение внешних IP.
|
||||
- `vc_nsxt` → модификатор `network`, параметры `needEnableAVI` (boolean), `ipSpaceName` (string, valueList содержит sentinel `"no-needed"`), `routedNetConfiguration`.
|
||||
|
||||
У модификатора есть `delete_strategy`, определяющий что делать при `terraform destroy`:
|
||||
- `noop_warn` — снять из state, эффект остаётся (предупреждение).
|
||||
- `error` — запрет удаления (сейчас так на vc_org, из-за чего destroy встаёт).
|
||||
- `inverse` — при Delete выполнить обратную операцию `modify` с `delete_params` (список `{Code, Value}`).
|
||||
|
||||
## Проблема
|
||||
|
||||
Хочу, чтобы `destroy` и `apply` были полными и симметричными. При удалении модификатора нужно «откатить» эффект:
|
||||
1. `needEnableAVI` → `false`.
|
||||
2. `ipSpaceName` → `"no-needed"`.
|
||||
3. `vIPConfigure` → `count=0`, имя сохранить (`[{"name":"internet-ipv4-v1","count":0}]`).
|
||||
|
||||
Пункты 1-2 — статические константы, текущий механизм `delete_params {Code,Value}` покрывает.
|
||||
Пункт 3 — динамический: имя берётся из текущего state инстанса, обнуляется только `count`.
|
||||
|
||||
Требование: решение должно быть архитектурно чистым и универсальным (привязанным к типам данных из API, `dataType`/`valueList`/`sub_params`), а не хардкодом имён сервисов — чтобы при неглобальных изменениях API перегенерация подхватывала.
|
||||
|
||||
## Ключевые факты (уже проверены)
|
||||
|
||||
- `count=0` принимается API, несмотря на `minvalue:1`/`integer > 0` в схеме. Идемпотентно.
|
||||
- `ipSpaceName` sentinel «выключен» = `"no-needed"` (есть в `valueList`).
|
||||
- `needEnableAVI` — boolean: обратное = `"false"`.
|
||||
- Типы из API: `needEnableAVI`=`boolean`; `ipSpaceName`=`string`(+`valueList`); `vIPConfigure`=`array-map-fixed` (sub_params: `name`=string, `count`=integer).
|
||||
|
||||
## Вопросы (нужны краткие ответы)
|
||||
|
||||
1. Как правильно расширить модель delete_params, чтобы поддержать и статичные обратные значения (`false`, `no-needed`), и динамические преобразования (`count→0`)? Оцени вариант «типизированные правила`: `Mode` ∈ {static, zero_count, …}, где static=текущий Value, zero_count=обнулить integer-поле `count` в каждом элементе array-map-fixed, взятом из live state.
|
||||
|
||||
2. Универсальнее ли выводить обратные значения ИЗ ТИПА ПАРАМЕТРА (boolean→"false", string+valueList→первый/помеченный sentinel, array-map-fixed→нулевой count в integer-полях), чем задавать их в реестре исключений? Где баланс: что держать в реестре (данные), что выводить из типа (логика)?
|
||||
|
||||
3. Нужен ли отдельный маркер «какое поле array-map-fixed обнулять» (сейчас это `count`), или достаточно общего правила «обнулить все integer-поля sub_params»? Риски обоих.
|
||||
|
||||
4. Правильный порядок destroy при зависимостях: `edge_net` (SNAT off + ALB off) → `org_ips` (count=0) → `nsxt` → `vdc`. Как Terraform сам выведет порядок из `depends_on`, и где инверсия/откат может конфликтовать с порядком удаления дочерних инстансов?
|
||||
|
||||
5. Есть ли подводные камни в самом `inverse`-delete (если дети ещё живы, откат `count=0` на орге может не пройти)? Нужен ли двухфазный подход или достаточно полагаться на порядок?
|
||||
|
||||
Формат ответа: пункты пронумерованы под мои вопросы, 1-3 предложения на пункт. Без лишнего.
|
||||
@@ -0,0 +1,61 @@
|
||||
# Задача: спроектировать ПРОСТУЮ логику «изменяемости» параметров (CreateOnly vs Modifiable)
|
||||
|
||||
## Проблема
|
||||
|
||||
Генератор terraform-провайдера строит проверку «Нельзя изменить X» (CreateOnly) на основе
|
||||
только instance-modify. Из-за этого возникают противоречивые и сломанные ситуации:
|
||||
|
||||
`generated/dev/resources_yaml/22_vc_nsxt.yaml`:
|
||||
- create (id 10), param `needEnableAVI` (id 340) — помечен `is_modifiable: true`;
|
||||
- instance-modify у `vc_nsxt` НЕТ (modify 111 — это **modifier** `vc_nsxt.network`).
|
||||
|
||||
Генератор:
|
||||
|
||||
```
|
||||
ComputeCreateOnly(createParams, instanceModifyParams):
|
||||
поле считается CreateOnly, если его code нет в instance-modify
|
||||
```
|
||||
|
||||
Следствие: `needEnableAVI` попадает в CreateOnly → генерится жёсткая проверка
|
||||
«Нельзя изменить need_enable_avi», хотя по YAML параметр `is_modifiable: true`.
|
||||
|
||||
Плюс `ConvertParams` вообще **не переносит** `is_modifiable` из ParamSpec в Param —
|
||||
поле теряется, логика его учесть не может.
|
||||
|
||||
## Ключевые файлы (текущая логика)
|
||||
|
||||
- `TOOLS/lib/types.go` — `ParamSpec.IsModifiable` (есть, `is_modifiable` сериализуется в YAML)
|
||||
- `TOOLS/resource-generator/internal/types/types.go` — `Param` (НЕТ поля IsModifiable)
|
||||
- `TOOLS/resource-generator/internal/loader/loader.go` — `ConvertParams` (не переносит IsModifiable)
|
||||
- `TOOLS/resource-generator/internal/params/params.go` — `ComputeCreateOnly` (игнорирует is_modifiable и modifier)
|
||||
- `TOOLS/resource-generator/internal/templates/instance.go` — шаблон, рендерит «Нельзя изменить» из `.CreateOnlyParams`
|
||||
- YAML: `generated/dev/resources_yaml/22_vc_nsxt.yaml` (modify 111 — `kind: modifier`)
|
||||
|
||||
## Существующие понятия операции
|
||||
|
||||
В YAML операции бывают видов:
|
||||
- `kind: instance` (`create` / `modify` / `suspend` / `resume` / `delete`)
|
||||
- `kind: modifier` (отдельный TF-ресурс, `modify` на родительском инстансе, например `vc_nsxt.network`)
|
||||
- `kind: subresource`
|
||||
- `kind: action`
|
||||
|
||||
## Цель
|
||||
|
||||
Спроектировать **единую, простую и понятную** модель «изменяемости» параметра, чтобы:
|
||||
1. параметр считался изменяемым, если он изменяем ХОТЯ БЫ через один канал
|
||||
(instance-modify ИЛИ modifier);
|
||||
2. «Нельзя изменить» генерировалось ТОЛЬКО для реально create-only параметров;
|
||||
3. `is_modifiable` из YAML был единственным источником правды (или явно согласован с каналами modify);
|
||||
4. не было противоречий вида «в YAML is_modifiable:true, а в коде «Нельзя изменить»».
|
||||
|
||||
## Вопросы к Opus
|
||||
|
||||
1. Какая каноническая модель: вычислять изменяемость по `is_modifiable` (флаг из YAML),
|
||||
по наличию кода в любом modify (instance + modifier), или по комбинации?
|
||||
2. Где именно проставлять/вычислять флаг — в yaml-generator (при генерации YAML), или в
|
||||
resource-generator (при генерации Go)?
|
||||
3. Как связать modifier-параметры (`vc_nsxt.network`) с parent-инстансом (`vc_nsxt`),
|
||||
чтобы instance знал, что `needEnableAVI` изменяется через modifier?
|
||||
4. Минимальный, без legacy-наслоений, набор правил.
|
||||
|
||||
Ответ — кратко, с конкретной архитектурой и точками правки (файл + функция).
|
||||
@@ -0,0 +1,77 @@
|
||||
> ⛔ **ЛОЖНЫЙ ПУТЬ — ОТМЕНЕНО (пометка 2026-09-24). ТАК ДЕЛАТЬ НЕЛЬЗЯ.**
|
||||
> Отменённый заход (`kind: modifier` в YAML + реестр в генераторе). Сохранён как история.
|
||||
> Актуально: `NOTES/40_chat_summaries/CHAT_RESUME_IAC_2026-09-24.md`.
|
||||
|
||||
# Спроектировать С НУЛЯ архитектуру/логику «ресурсов-модификаторов» (kind: modifier)
|
||||
|
||||
## Цель
|
||||
|
||||
Перепроектировать модификаторы целиком, чтобы исключить ВСЕ классы багов, не латать по одному.
|
||||
Нужна единая, полная модель поведения — без догадок и костылей. Перечислить ВСЕ кейсы.
|
||||
|
||||
## Что такое модификатор (текущая фактура)
|
||||
|
||||
В YAML (генерируется из API) операции бывают:
|
||||
- `kind: instance` (create/modify/suspend/resume/delete) — обычный CRUD-ресурс;
|
||||
- `kind: modifier` + `modifier: <name>` — отдельный TF-ресурс, который вызывает `modify`
|
||||
на родительском инстансе. Сейчас их два: `vc_org.ip_space`, `vc_nsxt.network`.
|
||||
|
||||
Реальные примеры:
|
||||
- `vc_org` → modifier `ip_space` (modify 207), параметр `vIPConfigure` (array-map-fixed);
|
||||
- `vc_nsxt` → modifier `network` (modify 111), параметры `needEnableAVI`(bool),
|
||||
`virtualServicesCount`(int>0), `qosProfile`(string), `ipSpaceName`(string),
|
||||
`routedNetConfiguration`(map-fixed).
|
||||
|
||||
## Текущий механизм (что есть — факты, не догадки)
|
||||
|
||||
1. Генератор: `TOOLS/resource-generator/internal/templates/modifier.go`
|
||||
- Create и Update **идентичны**: оба шлют `modify` с полным набором полей.
|
||||
- `Delete` — **no-op** (комментарий: «no confirmed inverse payload»).
|
||||
- Схема: `id` computed, `<service>_id` required, поля Optional (или Required если нет default).
|
||||
2. `resources_core.CompactParams` — выбрасывает пустые строки из payload.
|
||||
3. `resources_core.BuildActionID(instanceUID, operation, modifierName)` — константный ID,
|
||||
не привязан к реальной операции (opUid не сохраняется).
|
||||
4. `core.RunInstanceOperationUniversalByCode` — резолвит code→id через
|
||||
`GET /instanceOperations/{opUid}?fields=cfsParams` (fallback на `/default/{opId}`);
|
||||
отправляет переданные params, затем дозаполняет остальные их live-значением
|
||||
(guard: пропускает параметр, если нет ни ParamValue, ни DefaultValue).
|
||||
5. `Read` — через `RefreshResourceState`: читает `state_params` инстанса и
|
||||
перезаписывает input-поля из них.
|
||||
|
||||
## Уже выявленные КЛАССЫ багов (все реально случились)
|
||||
|
||||
- **A. Сброс create-поля при modify.** modify со сброшенными (null) параметрами
|
||||
трактуется бэкендом как reset-to-default: `needEnableAVI` стал false после
|
||||
create=true. Причина: модификатор шлёт только свои поля, `CompactParams` выкидывает
|
||||
пустые, бэкенд видит «отсутствующий» и сбрасывает.
|
||||
- **B. Досылка синтетики.** фикс «досылать всё» слал `"0"` для `integer > 0`
|
||||
(параметр `virtualServicesCount`), API 400 «Invalid format integer > 0».
|
||||
- **C. Ложное «Нельзя изменить».** `ComputeCreateOnly` считал `needEnableAVI`
|
||||
CreateOnly (change-forbidden), хотя в YAML `is_modifiable: true` — потому что
|
||||
генератор не учитывал modifier-канал и терял `IsModifiable`. (Зафиксировано отдельно.)
|
||||
- **D. No-op Delete оставляет эффект на платформе.** destroy модификатора убирает
|
||||
ресурс из state, но выделенные IP / включённый ALB остаются на платформе → drift.
|
||||
- **E. Повторный apply после taint/replace** снова гонит modify — риск повторной
|
||||
аллокации (для `ip_space`), идемпотентность не гарантирована.
|
||||
|
||||
## Вопросы к Опусу (ответить ПОЛНО, по пунктам, с точными местами правки)
|
||||
|
||||
1. **Канон «как сравнить и применить».** Должен ли модификатор перед modify
|
||||
читать текущее состояние и слать ДЕЛЬТУ (только реально изменившиеся поля),
|
||||
или ПТЦ полный payload? Как детектить drift в Read?
|
||||
2. **Досылка незаданных полей (паер-заливы A и B).** Какое каноническое правило:
|
||||
когда досылать live-значение, когда дефолт, когда пропускать? Как не сломать
|
||||
`integer > 0` и прочие constraints?
|
||||
3. **Delete/rollback.** Где искать обратный payload? Как правильно поступить, пока
|
||||
обратный payload НЕ подтверждён API (no-op допустим? явная ошибка? suspend?).
|
||||
4. **Idempotency + ID.** Как сделать ID модификатора отражающим фактическую операцию
|
||||
(opUid?) и как предотвратить двойную аллокацию при replace/повторном apply?
|
||||
5. **Связь с родителем.** Должен ли модификатор использовать `<service>_id` как ссылку
|
||||
на родителя (depends_on / borrow state), и как читать UUID родителя?
|
||||
6. **Create vs Update.** Допустимо ли иметь их идентичными, или нужен строго Update-семантик
|
||||
(нет create, только apply-по-десяти)?
|
||||
7. **Полный перечень кейсов.** Перечислить ВСЕ edge-кейсы, которые надо покрыть:
|
||||
create родителя → modifier; remove modifier; replace; partial params; unknown/absent.
|
||||
|
||||
Ответ — архитектурный документ (краткий, структурированный), с конкретными файлами
|
||||
и функциями. НЕ код-ревью, а ПРОЕКТ.
|
||||
@@ -0,0 +1,68 @@
|
||||
> ⛔ **ЛОЖНЫЙ ПУТЬ — ОТМЕНЕНО (пометка 2026-09-24). ТАК ДЕЛАТЬ НЕЛЬЗЯ.**
|
||||
> Отменённый заход (`kind: modifier` в YAML + реестр в генераторе). Сохранён как история.
|
||||
> Актуально: `NOTES/40_chat_summaries/CHAT_RESUME_IAC_2026-09-24.md`.
|
||||
|
||||
# Уточнения к архитектуре модификаторов — расхождения с фактическим кодом
|
||||
|
||||
Не принимаю предыдущие ответы за истину. Сверка с реальным кодом выявила расхождения.
|
||||
Прошу пересмотреть/уточнить.
|
||||
|
||||
## Факт №1: `OperationSpec` — это алиас `lib.OperationSpec`, не локальный тип
|
||||
|
||||
В `TOOLS/resource-generator/internal/types/types.go`:
|
||||
```go
|
||||
type OperationSpec = lib.OperationSpec
|
||||
type ParamSpec = lib.ParamSpec
|
||||
```
|
||||
Канонический YAML-контракт лежит в `TOOLS/lib/types.go` (пакет `tf-tools/lib`),
|
||||
где уже определены `OperationSpec` (Name/ID/Kind/Action/Modifier/Subresource/Man/Params)
|
||||
и `ParamSpec`.
|
||||
|
||||
Ошибка в прошлом ответе: «добавить в types.go:39» — НЕ указано, что это `lib`.
|
||||
Новые поля `delete_strategy` / `idempotency` / `delete_params` должны быть
|
||||
в `TOOLS/lib/types.go`, иначе yaml-generator (который тоже импортирует lib)
|
||||
и resource-generator разойдутся.
|
||||
|
||||
Вопрос: подтверждаешь, что новый контракт добавляется в `lib/types.go\` (OperationSpec),
|
||||
а `resource-generator` получает его через алиас? Или нужно отдельное
|
||||
resource-generator-специфичное поле (не в lib, а в GenModifier)? Где граница:
|
||||
что в lib, что локально в GenModifier?
|
||||
|
||||
## Факт №2: `normalizeUniversalValueV6` — приватная, живёт в core, принимает core-структуру
|
||||
|
||||
Прошлый ответ: «сравнивать desired vs current после normalizeUniversalValueV6».
|
||||
Но:
|
||||
- `normalizeUniversalValueV6(val string, param universalCfsParam)` — **приватная** (маленькая буква);
|
||||
- принимает `universalCfsParam` (структуру пакета `core`);
|
||||
- сравнение pre-check «desired == current» предполагалось в `resources_core`
|
||||
(там `RunOperationByCodeWithTimeout`) или в шаблоне модификатора.
|
||||
|
||||
Вопрос: ГДЕ правильно делать pre-check и нормализованное сравнение?
|
||||
- вариант A: в `core` (там доступны и cfsParams, и normalize), экспортировать сравнение;
|
||||
- вариант B: в `resources_core` — тогда нужен экспортированный компаратор
|
||||
(`JSONStringsEquivalent` там уже есть), но `universalCfsParam` недоступен;
|
||||
- вариант C: сравнение только через `JSONStringsEquivalent` по JSON-строкам,
|
||||
без `normalizeUniversalValueV6`? (но тогда `" 5"` vs `"5"`, `true` vs `1` дадут ложный diff).
|
||||
|
||||
Как совместить нормализацию типов (bool→"true", int→"5") с местом, где сравнение
|
||||
происходит? Конкретный файл+функция.
|
||||
|
||||
## Дополнительные сомнения (прошу подтвердить/опровергнуть)
|
||||
|
||||
1. **Idempotency pre-check и «полный payload» конфликтуют?** Если desired==current → skip.
|
||||
Но при этом «полный payload» не шлётся вообще (skip). Это согласуется? Или при
|
||||
расхождении одного поля всё равно слать полный payload (и это нормализует всё)?
|
||||
|
||||
2. **`delete_strategy: inverse` + параметр, у которого НЕЛЬЗЯ обнулить** (напр.
|
||||
`virtualServicesCount` integer>0): прошлый ответ — «inverse недопустим, fail-fast».
|
||||
Но что если inverse-стратегия нужна только для ЧАСТИ полей, а не для всех?
|
||||
Т.е. `delete_params` покрывает `needEnableAVI:false`, а `virtualServicesCount`
|
||||
просто остаётся как есть. Допустимо ли «частичный inverse» (обратить только
|
||||
обратимое, остальное не трогать)? Или inverse обязан покрывать все поля?
|
||||
|
||||
3. **`noop_warn` (дефолт) — всегда ли безопасен?** Удаление модификатора из state
|
||||
при оставшемся эффекте на платформе — это drift. Допустимо ли вообще иметь
|
||||
`noop_warn` как ДЕФОЛТ, или для необратимых (ip_space) правильнее дефолт `error`
|
||||
(запретить destroy, пока не разберутся)? Что каноничнее?
|
||||
|
||||
Ответ — кратко, по пунктам.
|
||||
@@ -0,0 +1,50 @@
|
||||
> ✅ **АКТУАЛЬНОЕ НАПРАВЛЕНИЕ (пометка 2026-09-24), НО ЕЩЁ НЕ РЕАЛИЗОВАНО.**
|
||||
> Требования отсюда действительны: YAML — чистая выгрузка API без доменных меток; модификаторы — НЕ ветка
|
||||
> универсального генератора. Ответ Opus по нему см. в `NOTES/30_analysis/OPUS_ANSWER_IAC_SHTURVAL_MODIFY_2026-09-23.md`.
|
||||
> Состояние и развилка: `NOTES/40_chat_summaries/CHAT_RESUME_IAC_2026-09-24.md`.
|
||||
|
||||
# Глобальная архитектура модификаторов: как сделать их НЕ инвазивным дополнением
|
||||
|
||||
ЗАПРЕЩЕНО лезть в файлы репозитория. Отвечай только по тексту. Формат: тезисы, кратко, по пунктам моего вопроса. Без лишнего.
|
||||
|
||||
## Контекст
|
||||
|
||||
Terraform provider для Nubes Cloud. Цепочка кодогенерации:
|
||||
1. `01_generate_yamls` — идёт по API, по каждому облачному сервису тянет операции и параметры, пишет универсальный YAML (`resources_yaml/<id>_<svc>.yaml`).
|
||||
2. `02_generate_resources` — по этому YAML генерирует Go-ресурсы провайдера (`<id>_<svc>_resource.go`).
|
||||
|
||||
Обычные ресурсы (`nubes_vc_nsxt`, `nubes_vc_vdc` и т.д.) — это операции `create`/`delete`/`suspend`/`resume`/`reconcile` над инстансом. Их apply/destroy давно стабильны и оттестированы.
|
||||
|
||||
## Что такое «модификатор» (доменная суть)
|
||||
|
||||
Некоторые операции `modify` сервиса — это не «изменить инстанс», а **отложенный дочерний шаг** цепочки, который нельзя мешать с create инстанса:
|
||||
- `vc_org` → `modify` с параметром `vIPConfigure=[{"name":...,"count":N}]` — выделение внешних IP организации.
|
||||
- `vc_nsxt` → `modify` с `needEnableAVI`, `ipSpaceName`, `routedNetConfiguration` — настройка ALB/SNAT уже созданного Edge.
|
||||
|
||||
Такой `modify` семантически НЕ принадлежит lifecycle самого инстанса: это отдельный TF-ресурс, который должен создаваться/удаляться независимо от `create`/`delete` родителя.
|
||||
|
||||
## Проблема (как сделано сейчас — неправильно)
|
||||
|
||||
Сейчас «модификаторность» вплетена в универсальную генерацию:
|
||||
- реестр `serviceSpecificModifiers` зашит в исходник yaml-generator и **помечает** операцию `modify` как `kind: modifier` + пишет в YAML `delete_strategy`, `delete_params` и т.п.
|
||||
- Это ломает главный принцип: YAML должен быть чистой универсальной выгрузкой из API, а обычные ресурсы — не зависеть ни от какого реестра.
|
||||
|
||||
Требования:
|
||||
1. YAML — универсальная выгрузка ВСЕГО из API, без доменных меток (`kind: modifier`, `delete_strategy`).
|
||||
2. Ресурсы облачных сервисов НЕ должны зависеть от модификаторов. Если модификаторов нет — поведение идентично прежнему (до их внедрения).
|
||||
3. Модификаторы — чистое ДОПОЛНЕНИЕ: отдельная сущность, отдельный ресурс, со своей семантикой (inverse-откат при destroy, idempotency), которая НЕ просачивается в базовую генерацию.
|
||||
4. При полном `destroy` должен быть корректный обратный откат: ALB off, SNAT `no-needed`, IP `count=0` — при этом симметричный `apply` возрождает всё.
|
||||
|
||||
## Вопросы (ответь по пунктам)
|
||||
|
||||
1. **Правильное место доменной семантики модификатора.** Где её хранить, чтобы она была «данными-наложением», а не веткой в универсальном генераторе? Варианты: (а) отдельный конфиг-файл данных (`modifiers.yaml`), который второй проход накладывает на базовый YAML, порождая ОТДЕЛЬНЫЕ YAML-записи модификаторов, не трогая базовые; (б) отдельный `kind` в самих YAML без доменных меток; (в) иное. Обоснуй.
|
||||
|
||||
2. **Разделение «модификатор» vs «обычный modify».** Как архитектурно отделить modify-как-модификатор от modify-инстанса, НЕ меняя универсальную выгрузку? Как гарантировать, что при отсутствии модификаторов обычный modify-поток ресурса вообще не затрагивается?
|
||||
|
||||
3. **Как структурировать inverse-откат**, чтобы он был: (а) генерализуемым (по типам: boolean→"false", string+valueList→off_value sentinel, array-map-fixed→zero integer-полей), (б) идемпотентным (не дёргать run, если live уже целевое), (в) не влиял на обычные ресурсы. Нужна ли отдельная модель `delete_rule` у модификатора.
|
||||
|
||||
4. **Порядок destroy** при цепочке модификаторов, зависящих от обычных ресурсов и друг от друга (`SNAT-модификатор → IP-модификатор → edge → vdc`). Как выразить зависимость модификатора от ресурса так, чтобы Terraform сам вывел обратный порядок, не завязываясь на хрупкий `depends_on`?
|
||||
|
||||
5. **Минимально-инвазивная миграция.** Как перейти от текущего (модификаторы «вросли» в базовую генерацию) к целевой (модификаторы — наложение) без регресса уже стабильных обычных ресурсов? Что трогать НЕЛЬЗЯ.
|
||||
|
||||
Ответь кратко, по номерам, 2-4 предложения на пункт.
|
||||
@@ -0,0 +1,45 @@
|
||||
> ⛔ **ЛОЖНЫЙ ПУТЬ — ОТМЕНЕНО (пометка 2026-09-24). ТАК ДЕЛАТЬ НЕЛЬЗЯ.**
|
||||
> Баг относится к отменённому заходу (`kind: modifier` в YAML + реестр в генераторе).
|
||||
> Сохранён как история. Актуально: `NOTES/40_chat_summaries/CHAT_RESUME_IAC_2026-09-24.md`.
|
||||
|
||||
# Баг: modify-модификатор сбрасывает create-поля в дефолт (needEnableAVI true→false)
|
||||
|
||||
## Симптом
|
||||
|
||||
`nubes_vc_nsxt_network` (modifier vc_nsxt.network, modify 111) после create Edge с `needEnableAVI=true`, `virtualServicesCount=3` сбрасывает `needEnableAVI` на платформе обратно в `false`.
|
||||
|
||||
## Подтверждено по API (cfsParams операций)
|
||||
|
||||
Create Edge (op `0c169353`):
|
||||
- 340 needEnableAVI = **true**
|
||||
- 341 virtualServicesCount = **3**
|
||||
|
||||
Modify (SNAT-модификатор, op `d14a149e`):
|
||||
- 368 needEnableAVI = **null**
|
||||
- 369 virtualServicesCount = **null**
|
||||
- 856 qosProfile = **null**
|
||||
- 372 ipSpaceName = internet-ipv4-v1
|
||||
- 1112 routedNetConfiguration = {...}
|
||||
|
||||
Итоговый state.params Edge: `needEnableAVI = false`.
|
||||
|
||||
## Гипотеза
|
||||
|
||||
Модификатор строится через `resources_core.CompactParams`, который выбрасывает пустые `Optional`-поля. Бэкенд для `modify` трактует **пропущенный/null** параметр как «сбросить в дефолт» (false/0), а не «оставить как есть». Итог: modify с частичным payload затирает create-поля.
|
||||
|
||||
## Файлы
|
||||
|
||||
- `provider/internal/core/client.go` — `RunInstanceOperationUniversalByCode` (отправка params), `normalizeUniversalValueV6`
|
||||
- `provider/internal/resources_core/crud.go` — `RunOperationByCodeWithTimeout`, `CompactParams`
|
||||
- генератор: `TOOLS/resource-generator/internal/templates/modifier.go`, `internal/writers/writers.go` (WriteModifierResource)
|
||||
- сгенерированное: `generated/dev/go/22_vc_nsxt_network_modifier.go`
|
||||
- YAML: `generated/dev/resources_yaml/22_vc_nsxt.yaml` (modify 111, поля is_modifiable)
|
||||
|
||||
## Задание
|
||||
|
||||
Определить каноническое поведение:
|
||||
1. Должен ли modify слать **все** параметры операции (полный payload, включая необязательные с их текущими значениями), или допустимо слать только переданные?
|
||||
2. Где правильнее чинить: в генераторе (шаблоне modifier), в `CompactParams`, или в `RunInstanceOperationUniversalByCode` (досылать дефолты/текущие значения незаданных полей)?
|
||||
3. Есть ли риск, что «досылать дефолты» сломает другие модификаторы (напр. vc_org.ip_space)?
|
||||
|
||||
Ответ кратко, тезисно, с указанием конкретной строки/места фикса.
|
||||
@@ -0,0 +1,43 @@
|
||||
> ⛔ **ЛОЖНЫЙ ПУТЬ — ОТМЕНЕНО (пометка 2026-09-24). ТАК ДЕЛАТЬ НЕЛЬЗЯ.**
|
||||
> Ревью плана отменённого захода (`kind: modifier` в YAML + реестр в генераторе).
|
||||
> Сохранён как история. Актуально: `NOTES/40_chat_summaries/CHAT_RESUME_IAC_2026-09-24.md`.
|
||||
|
||||
# Ревью плана реализации: редизайн модификаторов
|
||||
|
||||
Прошу отревьюить план `PLAN_modifier_redesign.md` (10 шагов). Это проект к реализации,
|
||||
не код. Вызовись: найди дыры, пропущенные кейсы, ошибки в порядке шагов, нестыковки.
|
||||
|
||||
## Контекст решения (уже согласовано, НЕ пересматривать)
|
||||
|
||||
- Модификатор = декларативная проекция полей родителя, единый `reconcile()` (Create≡Update).
|
||||
- Полный payload (не дельта), досылка: задан→значение, иначе live→default→skip.
|
||||
- `delete_strategy`: noop_warn | inverse | error (дефолт noop_warn), `idempotency`: none | check_before_run.
|
||||
- Pre-check `desired==current` в `core` (не в resources_core, не в шаблоне), по живому `state_params`.
|
||||
- `is_modifiable` — единственный сигнал изменяемости (фикс CreateOnly уже есть).
|
||||
|
||||
## Ключевые файлы-факты (сверены с кодом)
|
||||
|
||||
- `TOOLS/lib/types.go` — `OperationSpec`/`ParamSpec` (алиасы в обоих генераторах).
|
||||
- `TOOLS/yaml-generator/main.go` — `serviceSpecificModifiers` (реестр исключений, источник канона).
|
||||
- `TOOLS/resource-generator/internal/loader/loader.go` — ветка `kind==modifier`, `ValidateSpec`.
|
||||
- `TOOLS/resource-generator/internal/templates/modifier.go` — шаблон.
|
||||
- `provider/internal/resources_core/crud.go` — `RunOperationByCodeWithTimeout`.
|
||||
- `provider/internal/resources_core/json_normalize.go` — `JSONStringsEquivalent` (импорт в core = цикл).
|
||||
- `provider/internal/core/operation_run_bycode.go` — клиентский запуск.
|
||||
|
||||
## Вопросы к ревью (ответить кратко, по пунктам)
|
||||
|
||||
1. Порядок шагов 1–10 корректен? Где есть скрытая зависимость, которую я пропустил?
|
||||
2. Шаг 5 (вынос JSON-эквивалентности в `core/jsonutil`) — правильный путь снять цикл
|
||||
импорта, или есть чище (напр. оставить `JSONStringsEquivalent` в resources_core и
|
||||
передавать нормализованные строки в core уже готовыми)?
|
||||
3. Шаг 6 — сигнатура `modifierDesiredEqualsCurrent(desired map[string]string, cfsParams []universalCfsParam) bool`
|
||||
корректна? Хватает ли данных для сравнения всех типов (bool/int/string/map-fixed/array-map-fixed)?
|
||||
4. Шаг 4.4 Delete=inverse — как именно слать modify: `delete_params` + досылка live остальных
|
||||
(полный payload) — это правильно, или есть подводный камень?
|
||||
5. Шаг 8 — расширение реестра `serviceSpecificModifiers` до структуры: верный источник?
|
||||
Или `delete_strategy`/`idempotency` правильнее держать отдельным реестром (не трогая тип map)?
|
||||
6. Пропущен ли какой-то кейс из 16 (13 + taint/replace/partial/unknown)?
|
||||
7. Есть ли риск сломать instance-ресурсы (не модификаторы) любым из шагов 1–8?
|
||||
|
||||
Ответ — тезисно, с указанием конкретного шага и что в нём поправить.
|
||||
@@ -0,0 +1,84 @@
|
||||
> ⛔ **ЛОЖНЫЙ ПУТЬ — ОТМЕНЕНО (пометка 2026-09-24). ТАК ДЕЛАТЬ НЕЛЬЗЯ.**
|
||||
> Ревью реализации отменённого захода (`kind: modifier` в YAML + реестр в генераторе,
|
||||
> досылка modify-params через `paramValue`). Сохранён как история.
|
||||
> Актуально: `NOTES/40_chat_summaries/CHAT_RESUME_IAC_2026-09-24.md`.
|
||||
|
||||
# Ревью: модификаторы vc_nsxt / vc_org + досылка modify-params (Terraform Provider Nubes)
|
||||
|
||||
Ты — ревьюер. Ничего не правь. Прочитай и выдай:
|
||||
1) подтверждение/опровержение каждого утверждения ниже;
|
||||
2) список багов/рисков, которые я НЕ заметил;
|
||||
3) список проблем, которые я заметил ошибочно (ложные тревоги);
|
||||
4) чёткую рекомендацию по каждому корректному фиксу (минимальную, без scope creep).
|
||||
|
||||
## Контекст системы
|
||||
|
||||
Go Terraform provider `terraform-provider-nubes` (plugin-framework), сервис Nubes Cloud.
|
||||
Генератор ресурсов: `TOOLS/resource-generator` (шаблон `instance.go`, `modifier.go`).
|
||||
Ядро: `provider/internal/core/` (HTTP + операции), `provider/internal/resources_core/` (обёртки).
|
||||
|
||||
Сервисы, о которых речь:
|
||||
- `vc_nsxt` (serviceId 22). Операции: create (10), delete (25), **modify (111, kind: modifier, modifier: network)**, reconcile. У resource НЕТ instance-modify.
|
||||
- `vc_org` (serviceId 19). Модификатор `ip_space` (modify 207).
|
||||
|
||||
Модификатор = отдельный TF resource (`nubes_vc_nsxt_network`, `nubes_vc_org_ip_space`), который вызывает операцию `modify` с параметрами.
|
||||
|
||||
## Цель (FullPipe) — что должно работать
|
||||
|
||||
1. Создать VDC (vc_vdc).
|
||||
2. Создать Edge (vc_nsxt) с ALB (`needEnableAVI=true`, `virtualServicesCount=3`).
|
||||
3. Выделить IP организации (vc_org → modifier ip_space, `vIPConfigure`).
|
||||
4. Применить SNAT для Edge (vc_nsxt → modifier network, `ipSpaceName` + `routedNetConfiguration`).
|
||||
|
||||
## Что УЖЕ сделано (факты, проверь корректность)
|
||||
|
||||
### Факт 1. `resource "nubes_vc_nsxt"` Update — no-op
|
||||
Шаблон `instance.go` генерирует `hasServiceParamChanges := false`, а цикл по `.ModifyParams` пуст (у vc_nsxt нет instance-modify). Поэтому `Update` всегда уходит в `if !hasServiceParamChanges { ...; return }` и НЕ вызывает `modify` (111). Изменение ALB/VS/qos через resource невозможно. Изменения Edge идут ТОЛЬКО через модификатор `nubes_vc_nsxt_network`.
|
||||
|
||||
### Факт 2. Досылка незаданных modify-params (мой свежий фикс, коммиты a011358)
|
||||
Раньше незаданные params операции `modify` досылались значением `paramValue` из `GET /instanceOperations/{opUid}?fields=cfsParams`. Это ОШИБОЧНО: `paramValue` — дефолт ФОРМЫ операции, а не состояние инстанса. Для `needEnableAVI` там `"false"`, хотя live-значение инстанса `true` (подтверждается HAR/edge_.har и HAR/ipSpace0.har). Из-за этого каждый `modify` через модификатор сбрасывал ALB в false.
|
||||
|
||||
Фикс: в `operation_cfs.go` добавлены `instanceLiveParams()` (читает live из `GET /instances/{uid}` → `state.params`) и `lookupLiveParam(live, cfsParam)`. В `runInstanceOperationByCode` и `RunInstanceOperationUniversalWithDefaults` приоритет теперь: **live state.params → paramValue → defaultValue**.
|
||||
|
||||
### Факт 3. `ShouldRemoveFromState` (коммит 94c4c44)
|
||||
Раньше вызывал валидирующий `GetInstanceState`, который на статусе `deleted` кидал `instanceDeletedError` — и `Read` модификатора падал с "экземпляр … удалён" вместо тихого удаления из state. Переписан на `GetInstanceStateRaw` + различение 404/deleted (remove=true) vs сеть/5xx/403 (нужно `false, err`).
|
||||
|
||||
## ОШИБКА, которую наблюдаю СЕЙЧАС (главное)
|
||||
|
||||
`terraform apply` падает:
|
||||
|
||||
```
|
||||
Error: Provider returned invalid result object after apply
|
||||
After the apply operation, the provider still indicated an unknown value for
|
||||
nubes_vc_nsxt.edge.qos_profile. All values must be known after apply...
|
||||
```
|
||||
|
||||
`qos_profile` у resource `nubes_vc_nsxt` = `Optional+Computed` БЕЗ Default (`ShouldBeOptionalComputed` → true, потому что param qosProfile: not required, RefSvcId=0, Default=""). В конфиге не задаётся → в плане unknown. А `Update` (`hasServiceParamChanges=false` → ранний return) копирует только `State*`/`Vault*` outputs, но НЕ вызывает `RefreshResourceState`, поэтому `qos_profile` остаётся unknown.
|
||||
|
||||
## МОИ ДИАГНОЗЫ (проверь каждый, а не только текущий)
|
||||
|
||||
### Диагноз A (текущая ошибка)
|
||||
Ранний return в `Update` (шаблон instance.go) не схлопывает unknown→null read-back-computed поля. Нужно в ветке `!hasServiceParamChanges` вызывать тот же `RefreshResourceState`, а не копировать `State*`/`Vault*` вручную.
|
||||
|
||||
### Диагноз B (вылезет после A)
|
||||
Тот же ранний return оставляет unknown для `need_enable_avi` и `virtual_services_count`, если их убрать из `edge.tf` (а их и должны убрать, раз ALB перенесён в модификатор). Один корень с A.
|
||||
|
||||
### Диагноз C (дублирование конфига — НЕ починен)
|
||||
`edge.tf` ДО СИХ ПОР задаёт `need_enable_avi` и `virtual_services_count` (create), а `edge_network.tf` — те же значения (modifier). Это двойное задание одного и того же → возможен дрейф. Нужно определить: где канонически задавать ALB?
|
||||
|
||||
### Диагноз D (ловушка destroy/delete)
|
||||
Модификатор имеет `delete_strategy: inverse`, override `needEnableAVI="false"`. При `terraform destroy` ALB выключится. Повторный `apply` через `resource "nubes_vc_nsxt"` (no-op Update) НЕ включит обратно, а включит только модификатор второй apply-волной. Нужно проверить порядок зависимостей.
|
||||
|
||||
### Диагноз E
|
||||
`FetchInstanceOutputs` глотает любую API-ошибку (5xx/404) и возвращает пустые outputs без diagnostic → молчаливый дрейф. Нужен warning.
|
||||
|
||||
## Конкретные вопросы
|
||||
|
||||
1. Подтверди/опровергни Диагноз A как корень текущей ошибки.
|
||||
2. Есть ли проблема в моём фиксе досылки (Факт 2)? В частности:
|
||||
- верно ли, что `state.params` — единственный достоверный источник live?
|
||||
- не сломает ли `lookupLiveParam` (по Code/SvcOperationCfsParam/Name/Label) какие-то кейсы, где имя в state.params отличается регистром/форматом от этих ключей?
|
||||
- не создаёт ли `instanceLiveParams` лишний сетевой вызов на каждый modify (перф)?
|
||||
3. Верна ли трактовка Факт 1 (resource Update — no-op)? Или правильнее ДОБАВИТЬ instance-modify в генератор?
|
||||
4. Какое каноническое место для `need_enable_avi`/`virtual_services_count`/`qos_profile`: create (edge.tf) или modifier (edge_network.tf)? Что делать с текущим дублированием?
|
||||
5. Что ещё я упустил в цепочке create→modify→read→destroy?
|
||||
@@ -0,0 +1,33 @@
|
||||
> ⛔ **ЛОЖНЫЙ ПУТЬ — ОТМЕНЕНО (пометка 2026-09-24). ТАК ДЕЛАТЬ НЕЛЬЗЯ.**
|
||||
> Код-ревью отменённого захода (`kind: modifier` в YAML + реестр в генераторе).
|
||||
> Сохранён как история. Актуально: `NOTES/40_chat_summaries/CHAT_RESUME_IAC_2026-09-24.md`.
|
||||
|
||||
# Код-ревью: модификаторы (kind: modifier) в универсальном провайдере
|
||||
|
||||
## Контекст
|
||||
|
||||
terraform-provider-nubes (universal). Операции с `kind: modifier` генерируются как отдельные TF-ресурсы и запускают операцию `modify`, передавая параметры **по коду** (`vIPConfigure`, `needEnableAVI`...), а не по числовому id.
|
||||
|
||||
Актуальные модификаторы:
|
||||
- `nubes_vc_org_ip_space` (vc_org.ip_space, modify 207) — выделение внешних IP (`vIPConfigure`).
|
||||
- `nubes_vc_nsxt_network` (vc_nsxt.network, modify 111) — сеть/SNAT Edge.
|
||||
|
||||
## Ключевые файлы
|
||||
|
||||
- генератор: `TOOLS/resource-generator/internal/templates/modifier.go`, `internal/loader/loader.go` (LoadSpecs → GenModifier), `internal/writers/writers.go` (WriteModifierResource)
|
||||
- рантайм: `provider/internal/resources_core/crud.go` (RunOperationByCodeWithTimeout)
|
||||
- клиент: `provider/internal/core/client.go` (RunInstanceOperationUniversalByCode)
|
||||
- сгенерированное: `generated/dev/go/19_vc_org_ip_space_modifier.go`, `22_vc_nsxt_network_modifier.go`, `registry.go`
|
||||
|
||||
## Известная проблема (уже диагностирована — НЕ ревьюить)
|
||||
|
||||
`GET /instanceOperations/{opUid}?fields=cfsParams` падает 500 (`getResourceRealmConfig` Struct→string) на проблемных инстансах. Fallback на `/instanceOperations/default/{opId}` планируется отдельно.
|
||||
|
||||
## Задание — короткий код-ревью
|
||||
|
||||
1. Корректность жизненного цикла modifier-ресурса: Create/Update/Read/Delete, идемпотентность, refresh из API.
|
||||
2. Реального Delete нет (destroy не откатывает операцию) — это ожидаемо? Подводные камни при повторном apply.
|
||||
3. Риски передачи параметров по коду (code → id) в `RunInstanceOperationUniversalByCode`.
|
||||
4. ТОП-3 самых критичных замечания именно по модификаторам.
|
||||
|
||||
Ответ — кратко, тезисно, без кода-простыней.
|
||||
@@ -0,0 +1,41 @@
|
||||
# Промпт для Opus 4.8: код-ревью и оценка архитектуры (3 задачи roadmap)
|
||||
|
||||
Дата: 2026-09-22 | Статус: для отправки
|
||||
|
||||
```text
|
||||
Роль: ревьюер архитектуры Go-провайдера Terraform.
|
||||
|
||||
ПРАВИЛА:
|
||||
- Файлы НЕ открывай, в репозиторий не лезь — отвечай только по контексту ниже.
|
||||
- Анализируй ТОЛЬКО 3 указанные задачи, не весь проект.
|
||||
- Ответ максимально сжатый: только выводы/риски/рекомендации. Без вводных, без «рассмотрим», без повторов. Списки или таблица. Риск помечай 🔴/🟡/🟢.
|
||||
- Если для ответа не хватает факта — пиши «НЕДОСТАТОЧНО ДАННЫХ: …», не выдумывай.
|
||||
|
||||
КОНТЕКСТ (достаточен, файлы не нужны):
|
||||
- terraform-provider-nubes, Go, terraform-plugin-framework v1.8.0. Ресурсы ГЕНЕРИРУЮТСЯ из YAML-спек сервисов (не рукописные).
|
||||
- Один сервис → один ресурс инстанса nubes_<service> (CRUD). В YAML: operations (create/modify/suspend/…), params с type (bool/int64/string/map-fixed/array-map-fixed), required, default, is_modifiable, ref_svc_id.
|
||||
- Schema: param → Required (если required, без default, не refSvc); Optional; Computed+Default (если default); Optional+Computed (если параметр читается обратно из state_params инстанса, или это JSON).
|
||||
- Create: резолвит refSvc-параметры (принимают display name ИЛИ UUID → uid в API), вызывает create-op, затем читает state обратно.
|
||||
- Update: если изменились modify-параметры → вызывает modify-op с ними. ModifyPlan запрещает менять create-only атрибуты (ошибка).
|
||||
- Read: читает state_params инстанса обратно в input-поля (drift), и выставляет computed-мапы: state_params, state_out, state_params_flat, state_out_flat + vault_*.
|
||||
- Delete: suspend / delete / state_only — в зависимости от наличия suspend-op у сервиса.
|
||||
- Межресурсные зависимости: пользователь в .tf ссылается на атрибуты других ресурсов (напр. nubes_vc_vdc.vdc.id). ref_svc_id валидирует значение по инстансам целевого сервиса и резолвит в uid.
|
||||
- Data sources НЕ генерируются (только ресурсы).
|
||||
- map-fixed → SingleNestedAttribute (HCL: `x = { … }`); array-map-fixed → StringAttribute (JSON-строка).
|
||||
- Сервисы: vc_org (19), vc_vdc (21), vc_nsxt (22). Спека k8s_shturval уже есть.
|
||||
|
||||
УЖЕ СДЕЛАНО: vcVdc create, vcNsxt create — работают (v2.0.8).
|
||||
|
||||
АНАЛИЗИРОВАТЬ (только это):
|
||||
1. vcOrg modify — аллокация внешних IP в организацию ПО МЕРЕ НЕОБХОДИМОСТИ (динамически, число заранее не фиксировано).
|
||||
2. vcNsxt modify — включить SNAT и указать внешний IP, взятый из уже аллоцированного пула vcOrg.
|
||||
3. k8sShturval create.
|
||||
|
||||
ВОПРОСЫ (ответь по пунктам, кратко):
|
||||
1. Покрывает ли текущая модель эти 3 задачи, или для какой-то нужна новая абстракция (data source / action / subresource)? По каждой задаче — вердикт.
|
||||
2. vcOrg IP-аллокация: как моделировать пул внешних IP, растущий по мере необходимости — (а) атрибут-массив на nubes_vc_org, (б) отдельный ресурс/подресурс на каждый IP? Что лучше согласуется с текущей архитектурой и почему.
|
||||
3. vcNsxt: как передать «внешний IP из vcOrg»? Сравни: (а) refSvc-параметр, (б) computed-атрибут vcOrg + ссылка nubes_vc_org.<name>.<attr>, (в) data source. Учти ограничение: refSvc ссылается на инстанс/uid, но не на конкретный элемент списка.
|
||||
4. Риски текущего кода именно для этих потоков: update/modify с массивными параметрами; create-only guard; read-back; порядок плана между зависимыми ресурсами.
|
||||
5. k8sShturval create: что критично проверить (refSvc к vdc/org, долгий create, типы параметров)?
|
||||
6. Итог: 3–5 конкретных рекомендаций по приоритету — что добавить/изменить в генераторе или ресурсах.
|
||||
```
|
||||
@@ -280,7 +280,7 @@ CSS скрывает обе боковые панели mkdocs:
|
||||
2. **TOOLS/docs-generator/internal/writers/writers.go** — ВСЯ генерация .md (~1000 строк, ключевой файл)
|
||||
3. **TOOLS/docs-generator/main.go** — CLI, флаги, оркестрация
|
||||
4. **TOOLS/scripts/05_generate_docs_llm.py** — LLM-обогащение, SYSTEM_PROMPT
|
||||
5. **docs/LLM_DOCS_GENERATION.md** — архитектурная документация
|
||||
5. **NOTES/50_process/LLM_DOCS_GENERATION.md** — архитектурная документация
|
||||
6. **docs/30_registry/assets/extra.css** — CSS-стили
|
||||
7. **docs/30_registry/javascripts/fix-slash.js** — JS (trailing slash fix)
|
||||
|
||||
@@ -43,6 +43,25 @@
|
||||
- `universal_rebuild/tools/gen/main.go`
|
||||
- Читает YAML и генерирует ресурсы + `registry.go`.
|
||||
|
||||
### 1.6 Отдельные modifier-ресурсы
|
||||
Для parent-level операций `modify`, которые должны выполняться отдельным шагом Terraform-цепочки, используется `kind: modifier`.
|
||||
|
||||
Пример:
|
||||
|
||||
```yaml
|
||||
- name: modify
|
||||
kind: modifier
|
||||
action: modify
|
||||
modifier: ip_space
|
||||
params: []
|
||||
```
|
||||
|
||||
Такой блок не попадает в обычный instance CRUD. Go-генератор создаёт отдельный ресурс с именем `nubes_<service>_<modifier>`. Ресурс принимает ID родительского инстанса и параметры операции, выполняет parent `modify` при Create/Update и читает актуальные значения из `state_params` при Read.
|
||||
|
||||
Для `vcOrg` используется modifier `ip_space` с параметром `vIPConfigure`; для `vcNsxt` используется modifier `network` с параметрами операции сетевой настройки. Nested API-параметры modifier-ресурсов передаются как JSON-строки, поэтому их Terraform-значения должны быть валидным JSON.
|
||||
|
||||
Удаление modifier пока является no-op: подтверждённого обратного payload для отмены выделенных IP или SNAT нет. Операции удаления родительского сервиса не являются rollback и намеренно не вызываются.
|
||||
|
||||
---
|
||||
|
||||
## 2) Как получить параметры сервиса (без instanceUid)
|
||||
@@ -0,0 +1,63 @@
|
||||
# Отчет о проблеме: Ошибка 500 при получении cfsParams для vc_vdc и план исправления
|
||||
|
||||
## 1. Проблема
|
||||
При создании виртуального дата-центра `nubes_vc_vdc` (сервис 21 `vc_vdc`, операция создания 9) Terraform завершается с ошибкой клиента:
|
||||
```text
|
||||
не удалось получить детали операции: ошибка API 500: Invalid call of the function [getResourceRealmConfig], first Argument [resourceRealm] is of invalid type, Cannot cast Object type [Struct] to a value of type [string]: the function is located at [/app/api/v1/resources/instance_operation_cfs_param.cfc]
|
||||
```
|
||||
|
||||
## 2. Анализ причины
|
||||
1. **Место падения в провайдере**: `provider/internal/core/client.go` (строка 272 в `createInstanceWithContext`):
|
||||
```go
|
||||
opDetailsResp, _, err := c.doRequest(ctx, "GET", fmt.Sprintf("/instanceOperations/%s?fields=cfsParams", opUid), nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("не удалось получить детали операции: %w", err)
|
||||
}
|
||||
```
|
||||
2. **Поведение бэкенда Nubes (Lucee/ColdFusion)**:
|
||||
- В файле `/app/api/v1/resources/instance_operation_cfs_param.cfc` при обработке запроса `?fields=cfsParams` для операции 9 вызывается функция `getResourceRealmConfig(resourceRealm)`.
|
||||
- Для сервиса `vc_vdc` поле `resourceRealm` в БД DEV-окружения хранится как комплексный объект (`Struct`), а не скалярная строка (`string`).
|
||||
- При попытке приведения типа `Struct -> string` бэкенд падает с HTTP 500.
|
||||
|
||||
3. **Сравнение с веб-интерфейсом (HAR/vdc.har)**:
|
||||
- В официальном веб-интерфейсе ЛК запрос `GET /instanceOperations/{opUid}?fields=cfsParams` **вообще не выполняется**.
|
||||
- Браузер выполняет строго следующий флоу:
|
||||
1. `POST /instanceOperations` -> получает `instanceOperationUid`
|
||||
2. `POST /instanceOperationCfsParams` -> отправляет каждое значение CFS-параметра
|
||||
3. `GET /instanceOperations/{opUid}/validate-cfs` -> валидация бэкендом
|
||||
4. `POST /instanceOperations/{opUid}/run` -> запуск операции в оркестраторе
|
||||
5. Polling `GET /instanceOperations/{opUid}` -> ожидание статуса завершения
|
||||
|
||||
4. **Зачем провайдер делает шаг 2**:
|
||||
- Шаг 2 в провайдере использовался исключительно для вызова `resolveRefSvcParamValues(ctx, opDetails.InstanceOperation.CfsParams, params)` — чтобы узнать `refSvcId` параметров и попробовать отрезолвить имена в UUID.
|
||||
- Для ресурса `nubes_vc_vdc` параметр `organization_uid` (CFS param 30, refSvc 19) **уже гарантированно отрезолвлен в UUID** до создания операции (на этапе `ModifyPlan` и в начале `Create`).
|
||||
- Остальные параметры `vc_vdc` (провайдер сети, профиль Provider VDC, CPU, RAM, резервирование, storage_config) являются скалярами/числами/JSON-строками и не содержат `refSvcId`.
|
||||
- Соответственно, данные запроса `?fields=cfsParams` для `vc_vdc` фактически не требуются.
|
||||
|
||||
## 3. План изменений в провайдере (что будем менять)
|
||||
|
||||
### Целевой файл: `provider/internal/core/client.go`
|
||||
В функции `createInstanceWithContext` (и при необходимости в `runInstanceOperationUniversalByCodeWithTimeout` / `updateResourceWithTimeout`) заменяется жёсткое падение на условный graceful fallback:
|
||||
|
||||
```go
|
||||
opDetailsResp, _, err := c.doRequest(ctx, "GET", fmt.Sprintf("/instanceOperations/%s?fields=cfsParams", opUid), nil)
|
||||
if err != nil {
|
||||
// Проверяем, есть ли среди переданных строковых параметров не-UUID значения,
|
||||
// требующие резолвинга через refSvcId.
|
||||
// Если все строковые параметры уже UUID или числа/литералы — логируем предупреждение и продолжаем.
|
||||
c.logWarn(ctx, "не удалось получить cfsParams для операции %s (%v), продолжаем отправку параметров", opUid, err)
|
||||
} else {
|
||||
var opDetails universalOpResponse
|
||||
if err := json.Unmarshal(opDetailsResp, &opDetails); err == nil {
|
||||
params, err = c.resolveRefSvcParamValues(ctx, opDetails.InstanceOperation.CfsParams, params)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Критерии безопасности:
|
||||
1. Fallback условный: если бэкенд упал, но параметры уже валидны / являются UUID — выполнение продолжается.
|
||||
2. Логируется явный warning с идентификатором операции `opUid` и телом ошибки.
|
||||
3. Валидация значений параметров не теряется — её по-прежнему выполняет бэкенд на этапе `GET /validate-cfs`.
|
||||
@@ -0,0 +1,217 @@
|
||||
# Forensic Analysis: API Model to Ordinary YAML
|
||||
|
||||
## Цель
|
||||
|
||||
Исследовать существующую цепочку:
|
||||
|
||||
```text
|
||||
API model
|
||||
↓
|
||||
ordinary generator
|
||||
↓
|
||||
API YAML
|
||||
```
|
||||
|
||||
Цель этапа — получить подтверждённую картину движения данных и установить, что сохраняется, преобразуется или теряется до формирования API YAML.
|
||||
|
||||
Этот документ предназначен для передачи Opus перед анализом.
|
||||
|
||||
## Строгие ограничения
|
||||
|
||||
На этом этапе запрещено:
|
||||
|
||||
- изменять файлы;
|
||||
- писать код;
|
||||
- менять ordinary generator;
|
||||
- проектировать `ModifierSpec`;
|
||||
- проектировать `modifiers.yaml`;
|
||||
- проектировать `delete_rule`;
|
||||
- проектировать output layout или orchestration;
|
||||
- обсуждать inverse и dependency ordering;
|
||||
- придумывать API identifiers;
|
||||
- придумывать `parameter_path`;
|
||||
- придумывать HTTP method или payload structure;
|
||||
- считать API YAML полным источником данных без доказательства.
|
||||
|
||||
Если факт невозможно установить, его нужно обозначить как `unknown` или как требующий проверки фактическим API. Нельзя закрывать неизвестность архитектурным предположением.
|
||||
|
||||
## Что исследовать
|
||||
|
||||
### 1. Исходная API-модель
|
||||
|
||||
Установить:
|
||||
|
||||
- где находится canonical API model;
|
||||
- в каком формате она представлена;
|
||||
- как представлены services и operations;
|
||||
- как представлены параметры;
|
||||
- как представлены nested objects и arrays;
|
||||
- как представлены типы параметров;
|
||||
- существует ли стабильный operation ID;
|
||||
- существует ли стабильный parameter ID;
|
||||
- какие данные доступны до запуска ordinary generator.
|
||||
|
||||
### 2. Ordinary generator
|
||||
|
||||
Установить:
|
||||
|
||||
- какой input получает generator;
|
||||
- где он читает API-модель;
|
||||
- какие внутренние структуры строит;
|
||||
- какие преобразования выполняет;
|
||||
- какие поля нормализует или переименовывает;
|
||||
- какие поля вычисляет;
|
||||
- какие поля отбрасывает;
|
||||
- где формируется API YAML;
|
||||
- где именно могут происходить потери данных.
|
||||
|
||||
Обязательно различать:
|
||||
|
||||
```text
|
||||
данные отсутствуют уже в API
|
||||
```
|
||||
|
||||
и:
|
||||
|
||||
```text
|
||||
данные присутствуют в API, но теряются ordinary generator
|
||||
```
|
||||
|
||||
### 3. API YAML
|
||||
|
||||
Установить:
|
||||
|
||||
- какие поля сохраняются;
|
||||
- какие поля представлены иначе, чем в API-модели;
|
||||
- сохраняются ли nested objects и arrays;
|
||||
- сохраняются ли типы;
|
||||
- сохраняются ли operation identity и parameter identity;
|
||||
- сохраняются ли HTTP method и path, если они есть в исходной модели;
|
||||
- какие данные доступны будущему modifier layer;
|
||||
- какие данные потенциально доступны только в исходной API-модели.
|
||||
|
||||
## Обязательные трассировки
|
||||
|
||||
### `vc_org`
|
||||
|
||||
Отдельно проследить `vIPConfigure` и `count`:
|
||||
|
||||
```text
|
||||
API model
|
||||
→ generator input
|
||||
→ internal generator representation
|
||||
→ generator transformation
|
||||
→ API YAML
|
||||
```
|
||||
|
||||
Для каждого этапа указать:
|
||||
|
||||
- присутствует ли `vIPConfigure`;
|
||||
- присутствует ли `count`;
|
||||
- в каком типе они представлены;
|
||||
- в какой структуре находятся;
|
||||
- изменяются ли их имена или типы;
|
||||
- теряются ли они;
|
||||
- если теряются, в какой точке.
|
||||
|
||||
Не считать заранее известной структуру `vIPConfigure` или `count`.
|
||||
|
||||
### `vc_nsxt`
|
||||
|
||||
Отдельно проследить `ipSpaceName` и связанные параметры по той же цепочке:
|
||||
|
||||
```text
|
||||
API model
|
||||
→ generator input
|
||||
→ internal generator representation
|
||||
→ generator transformation
|
||||
→ API YAML
|
||||
```
|
||||
|
||||
Установить:
|
||||
|
||||
- где появляется `ipSpaceName`;
|
||||
- к какой operation или структуре он относится;
|
||||
- в каком типе представлен;
|
||||
- является ли обычным полем, nested field или частью массива;
|
||||
- какие связанные параметры находятся рядом;
|
||||
- сохраняется ли он в API YAML;
|
||||
- изменяются ли его значение или тип;
|
||||
- теряются ли связанные поля.
|
||||
|
||||
## Требования к доказательности
|
||||
|
||||
Каждый вывод разделять на:
|
||||
|
||||
- **Подтверждённый факт** — непосредственно виден из кода, структуры данных, фактического API input, generator input или API YAML.
|
||||
- **Неизвестное** — информация отсутствует или неоднозначна.
|
||||
- **Предположение** — не использовать как основание для архитектуры; только явно перечислять как неподтверждённое.
|
||||
|
||||
Для каждого важного вывода указывать конкретное основание: файл, функцию, структуру, входной документ или фрагмент YAML/JSON. Если точное место невозможно назвать, это указать как ограничение анализа.
|
||||
|
||||
## Формат итогового отчёта
|
||||
|
||||
Отчёт должен содержать только следующие разделы:
|
||||
|
||||
1. **Подтверждённые факты**
|
||||
2. **Исходная API-модель**
|
||||
3. **Как ordinary generator читает модель**
|
||||
4. **Как формируется API YAML**
|
||||
5. **Цепочка данных для `vc_org.vIPConfigure.count`**
|
||||
6. **Цепочка данных для `vc_nsxt.ipSpaceName` и связанных параметров**
|
||||
7. **Что сохраняется**
|
||||
8. **Что преобразуется или нормализуется**
|
||||
9. **Что теряется**
|
||||
10. **Где происходят потери**
|
||||
11. **Какие данные доступны в API YAML**
|
||||
12. **Какие данные доступны только в API-модели**
|
||||
13. **Неизвестные места**
|
||||
14. **Что необходимо проверить фактическим API**
|
||||
|
||||
## Критерий завершения
|
||||
|
||||
Forensic analysis завершён только тогда, когда для каждого потенциально необходимого modifier-элемента можно проследить происхождение:
|
||||
|
||||
```text
|
||||
API model
|
||||
→ generator input
|
||||
→ internal representation
|
||||
→ transformation
|
||||
→ API YAML
|
||||
```
|
||||
|
||||
без неизвестных промежуточных преобразований.
|
||||
|
||||
Для `vc_org` и `vc_nsxt` должны быть подтверждены:
|
||||
|
||||
```text
|
||||
operation identity
|
||||
parameter identity
|
||||
parameter structure
|
||||
parameter type
|
||||
API model representation
|
||||
generator representation
|
||||
YAML representation
|
||||
transformation
|
||||
loss or absence
|
||||
```
|
||||
|
||||
Если на существенном этапе остаётся `???`, исследование не завершено. Этот пункт нужно зафиксировать как `unknown`, а не проектировать решение.
|
||||
|
||||
## Главный принцип
|
||||
|
||||
Сначала:
|
||||
|
||||
```text
|
||||
исследовать
|
||||
↓
|
||||
зафиксировать факты
|
||||
↓
|
||||
зафиксировать неизвестное
|
||||
↓
|
||||
отличить отсутствие данных от потери данных
|
||||
```
|
||||
|
||||
Только после отдельного согласования forensic report можно переходить к проектированию `ModifierSpec`, `modifiers.yaml`, `delete_rule`, modifier generator, output boundary и orchestration.
|
||||
|
||||
На текущем этапе никаких решений по этим компонентам принимать нельзя.
|
||||
@@ -0,0 +1,234 @@
|
||||
# Forensic Analysis: API Model to Ordinary YAML
|
||||
|
||||
**Анализ от Gemini 3.8 flash.**
|
||||
|
||||
Исследование проведено строго в границах требований [FORENSIC_ANALYSIS_BRIEF_2026-09-23.md](FORENSIC_ANALYSIS_BRIEF_2026-09-23.md).
|
||||
|
||||
---
|
||||
|
||||
## 1. Подтверждённые факты
|
||||
|
||||
- **Цепочка генерации YAML:** Инструмент `yaml-generator` ([TOOLS/yaml-generator/main.go](../TOOLS/yaml-generator/main.go)) опрашивает live HTTP REST Gateway API Nubes по сети и сериализует результат в YAML-файлы спецификаций (`generated/{stand}/resources_yaml/{service_id}_{name}.yaml`), используя структуры контракта [TOOLS/lib/types.go](../TOOLS/lib/types.go).
|
||||
- **Спецификации сервисов в репозитории:** Канонические сгенерированные спеки сервисов физически размещены в [generated/dev/resources_yaml/](../generated/dev/resources_yaml/). В частности, [19_vc_org.yaml](../generated/dev/resources_yaml/19_vc_org.yaml) и [22_vc_nsxt.yaml](../generated/dev/resources_yaml/22_vc_nsxt.yaml).
|
||||
- **В `vc_org.yaml`:**
|
||||
- Операция `modify` имеет числовой ID `207`, `kind: instance`, `action: modify`.
|
||||
- Параметр `vIPConfigure` присутствует как параметр операции `modify` с числовым ID `662`, типом `data_type: array-map-fixed`, `required: true`.
|
||||
- Поле `count` присутствует внутри `sub_params` параметра `vIPConfigure` с числовым ID `40`, `data_type: integer > 0`, `required: true`, `is_modifiable: false`.
|
||||
- Поле `name` присутствует внутри `sub_params` параметра `vIPConfigure` с числовым ID `39`, `data_type: string`, `required: true`, `is_modifiable: false`.
|
||||
- **В `vc_nsxt.yaml`:**
|
||||
- Операция `modify` имеет числовой ID `111`, `kind: instance`, `action: modify`.
|
||||
- Параметр `ipSpaceName` присутствует в операции `modify` с числовым ID `372`, `data_type: string`, `required: false`, `sort: 50`.
|
||||
- С ним рядом в операции `modify` присутствуют:
|
||||
- `needEnableAVI` (ID `368`, `data_type: boolean`, `required: false`, `value_list: ["false", "true"]`);
|
||||
- `virtualServicesCount` (ID `369`, `data_type: integer > 0`, `required: false`, `minvalue: 1`, `maxvalue: 4`);
|
||||
- `qosProfile` (ID `856`, `data_type: string`, `required: false`);
|
||||
- `routedNetConfiguration` (ID `1112`, `data_type: map-fixed`, `required: true`, с вложенными подполями `ipAddrPool`, `mainDns`, `secondDns`).
|
||||
- **Генератор ресурсов (Ordinary Resource Generator):**
|
||||
- Расположен в [TOOLS/resource-generator/main.go](../TOOLS/resource-generator/main.go).
|
||||
- При `op.Kind == "instance"` (что установлено для `modify` в [generated/dev/resources_yaml/19_vc_org.yaml](../generated/dev/resources_yaml/19_vc_org.yaml) и [generated/dev/resources_yaml/22_vc_nsxt.yaml](../generated/dev/resources_yaml/22_vc_nsxt.yaml)) генератор объединяет параметры `create` и `modify` в схему одного общего ресурса `nubes_vc_org` / `nubes_vc_nsxt`.
|
||||
- Параметры, отсутствующие в операции `create`, но присутствующие в операции `modify` (как `vIPConfigure` в `vc_org`), не включаются в жизненный цикл `create`, а при отсутствии отдельной разметки `kind: modifier` в YAML генератор не создаёт под них отдельного ресурса модификатора.
|
||||
|
||||
---
|
||||
|
||||
## 2. Исходная API-модель
|
||||
|
||||
- **Источник и формат:** Модель получается HTTP-клиентом ([TOOLS/yaml-generator/internal/client/client.go](../TOOLS/yaml-generator/internal/client/client.go#L44-L62)) по протоколу HTTP GET в формате JSON.
|
||||
- **Эндпоинты API:**
|
||||
- Метаданные сервиса и список операций: `GET /services/{svcId}` (возвращает `types.ServiceResponse`).
|
||||
- Метаданные конкретной операции: `GET /instanceOperations/default/{svcOperationId}` (возвращает `types.ServiceOperationResponse`).
|
||||
- **Идентификаторы операций и параметров:**
|
||||
- Операция идентифицируется стабильным числовым `svcOperationId` (int) и строковым именем `operation` (например, `"modify"`, `"create"`).
|
||||
- Параметры идентифицируются стабильным числовым `svcOperationCfsParamId` (int) и строковым кодом `svcOperationCfsParam` (например, `"vIPConfigure"`, `"ipSpaceName"`).
|
||||
- **Вложенные объекты и массивы (`dataDescriptor`):**
|
||||
- В ответе эндпоинта `/instanceOperations/default/{id}` сложная структура передаётся в поле `dataDescriptor: map[string]CfsSubParam`.
|
||||
- Каждое подполе имеет свой числовой `svcOperationCfsSubparamId`, строковый ключ (код подполя), `dataType`, `isRequired`, `isModifiableDefinition`, `defaultValue`, `valueList` (строка через запятую или массив).
|
||||
|
||||
---
|
||||
|
||||
## 3. Как ordinary generator читает модель
|
||||
|
||||
- **Входной поток:**
|
||||
`yaml-generator` вызывает `cli.GetService(svc.ID)` и перебирает список `info.Operations`.
|
||||
- **Чтение операций и параметров:**
|
||||
Для каждой операции вызывается `cli.GetServiceOperation(op.SvcOperationID)` ([TOOLS/yaml-generator/internal/client/client.go](../TOOLS/yaml-generator/internal/client/client.go#L182-L240)).
|
||||
- **Преобразования и нормализация:**
|
||||
- Имена сервисов и операций нормализуются в snake_case функцией `normalize.Identifier` ([TOOLS/yaml-generator/internal/normalize/normalize.go](../TOOLS/yaml-generator/internal/normalize/normalize.go#L17-L50)).
|
||||
- Классификация операции: функция `classifyOperation` делит операции на `instance` (для `create`, `modify`, `delete`, `suspend`, `resume`), `subresource` (если есть символ подчеркивания) или `action`.
|
||||
- Разворачивание `dataDescriptor`: генератор обходит `map[string]CfsSubParam`, преобразует подполя в срез `types.ParamSpec` и детерминированно сортирует по `subParams[i].ID`.
|
||||
- Сортировка верхнеуровневых параметров по `params[i].ID`.
|
||||
- Сортировка операций по имени и ID.
|
||||
- **Что отбрасывается / не сохраняется в YAML:**
|
||||
- Конкретные HTTP method и URL-пути эндпоинтов API платформы (они зашиты в код клиента, в спек YAML не пишутся).
|
||||
- Вспомогательные поля `CfsParam`, не имеющие тега `yaml:` в [TOOLS/lib/types.go](../TOOLS/lib/types.go#L70-L101), если они не сериализуются или приходят пустыми: `IsModifiable`, `IsSensitive` (указаны с `omitempty`). Поле `dataDescriptor` как мапа отбрасывается — сохраняется преобразованный срез `sub_params`.
|
||||
|
||||
---
|
||||
|
||||
## 4. Как формируется API YAML
|
||||
|
||||
- Сериализация структуры `types.ServiceSpec` в YAML выполняется через библиотеку `gopkg.in/yaml.v3` ([TOOLS/yaml-generator/main.go](../TOOLS/yaml-generator/main.go#L107-L114)).
|
||||
- В результирующий файл пишутся:
|
||||
- Метаданные сервиса (`name`, `service_id`, `service_display_name`, `service_short_name`, `service_man`).
|
||||
- Стандартная секция `lifecycle` и `outputs`.
|
||||
- Секция `operations` со списком операций и всеми их параметрами (`id`, `code`, `data_type`, `required`, `default`, `value_list`, `descr`, `man`, `sort`, `sub_params`).
|
||||
|
||||
---
|
||||
|
||||
## 5. Цепочка данных для `vc_org.vIPConfigure.count`
|
||||
|
||||
1. **API Model:**
|
||||
- Эндпоинт `/instanceOperations/default/207` возвращает параметр с `svcOperationCfsParamId: 662`, `svcOperationCfsParam: "vIPConfigure"`, `dataType: "array-map-fixed"`.
|
||||
- Внутри него поле `dataDescriptor` содержит ключ `"count"`:
|
||||
- `svcOperationCfsSubparamId: 40`,
|
||||
- `dataType: "integer > 0"`,
|
||||
- `isRequired: true`,
|
||||
- `defaultValue: ""`,
|
||||
- `descr: "Пример: \`1\`"`.
|
||||
2. **Generator Input:**
|
||||
- Читается в структуру `types.CfsParam` с мапой `DataDescriptor map[string]CfsSubParam` ([TOOLS/yaml-generator/internal/types/types.go](../TOOLS/yaml-generator/internal/types/types.go#L49-L72)).
|
||||
3. **Internal Generator Representation:**
|
||||
- В [TOOLS/yaml-generator/internal/client/client.go](../TOOLS/yaml-generator/internal/client/client.go#L210-L232) мапа `dataDescriptor` разворачивается в `types.ParamSpec.SubParams`. Поле `count` становится элементом среза `SubParams` с `ID: 40`, `Code: "count"`, `DataType: "integer > 0"`.
|
||||
4. **Generator Transformation:**
|
||||
- Сортируется по ID (`sort.Slice(subParams, ...)`).
|
||||
5. **API YAML:**
|
||||
- Записывается в [generated/dev/resources_yaml/19_vc_org.yaml](../generated/dev/resources_yaml/19_vc_org.yaml#L131-L150):
|
||||
```yaml
|
||||
- id: 662
|
||||
code: vIPConfigure
|
||||
data_type: array-map-fixed
|
||||
required: true
|
||||
sort: 10
|
||||
sub_params:
|
||||
- id: 39
|
||||
code: name
|
||||
data_type: string
|
||||
required: true
|
||||
default: ""
|
||||
is_modifiable: false
|
||||
- id: 40
|
||||
code: count
|
||||
data_type: integer > 0
|
||||
required: true
|
||||
default: ""
|
||||
descr: 'Пример: `1`'
|
||||
is_modifiable: false
|
||||
```
|
||||
- **Потерь в YAML нет:** `vIPConfigure` и `count` полностью и без искажений сохранены в каноническом YAML спецификации.
|
||||
|
||||
---
|
||||
|
||||
## 6. Цепочка данных для `vc_nsxt.ipSpaceName` и связанных параметров
|
||||
|
||||
1. **API Model:**
|
||||
- Эндпоинт `/instanceOperations/default/111` возвращает операцию `modify` сервиса 22.
|
||||
- Параметр `ipSpaceName` возвращается с:
|
||||
- `svcOperationCfsParamId: 372`,
|
||||
- `svcOperationCfsParam: "ipSpaceName"`,
|
||||
- `dataType: "string"`,
|
||||
- `isRequired: false`,
|
||||
- `descr: "Имя ip Space для внешнего IP"`,
|
||||
- `man: "Необходимо указывать, если включён параметр \`Выделить VIP для SNAT\`"`,
|
||||
- `sort: 50`.
|
||||
- В HAR-дампах ([HAR/edge_.har](../HAR/edge_.har#L7985)) на живом инстансе в рантайме возвращается `valueList: ["no-needed", ...]`.
|
||||
2. **Generator Input:**
|
||||
- Читается в структуру `types.CfsParam` ([TOOLS/yaml-generator/internal/types/types.go](../TOOLS/yaml-generator/internal/types/types.go#L49-L72)).
|
||||
3. **Internal Generator Representation:**
|
||||
- Преобразуется в `types.ParamSpec` со значениями `ID: 372`, `Code: "ipSpaceName"`, `DataType: "string"`.
|
||||
4. **Generator Transformation:**
|
||||
- Нормализуются defaults и value_list через `normalizeDefault` и `normalizeValueList`.
|
||||
5. **API YAML:**
|
||||
- Записывается в [generated/dev/resources_yaml/22_vc_nsxt.yaml](../generated/dev/resources_yaml/22_vc_nsxt.yaml#L149-L155):
|
||||
```yaml
|
||||
- id: 372
|
||||
code: ipSpaceName
|
||||
data_type: string
|
||||
required: false
|
||||
descr: Имя ip Space для внешнего IP
|
||||
man: Необходимо указывать, если включён параметр `Выделить VIP для SNAT`
|
||||
sort: 50
|
||||
```
|
||||
- Рядом в той же операции сохранены: `needEnableAVI` (ID 368), `virtualServicesCount` (ID 369), `qosProfile` (ID 856), `routedNetConfiguration` (ID 1112 с sub_params).
|
||||
- **Особенность по `value_list`:** в статическом `22_vc_nsxt.yaml` поле `value_list` для `ipSpaceName` отсутствует (`null` в ответе static-дефолтов эндпоинта `/instanceOperations/default/111`), хотя в рантайме на конкретном инстансе `valueList` динамически содержит `["no-needed", ...]`.
|
||||
|
||||
---
|
||||
|
||||
## 7. Что сохраняется
|
||||
|
||||
- Полная идентичность сущностей: `service_id`, `svcOperationId` (как `id` операции), `svcOperationCfsParamId` (как `id` параметра), `svcOperationCfsSubparamId` (как `id` в `sub_params`).
|
||||
- Строковые коды: `operation`, коды параметров (`code`).
|
||||
- Исходные типы платформы: `dataType` (`string`, `boolean`, `integer > 0`, `array-map-fixed`, `map-fixed`).
|
||||
- Вся структура вложенности (`dataDescriptor` → `sub_params`).
|
||||
- Флаги `required`, валидационные regex, min/max, описания (`descr`, `man`), порядок (`sort`).
|
||||
|
||||
---
|
||||
|
||||
## 8. Что преобразуется или нормализуется
|
||||
|
||||
- Имена операций и сервисов приводятся к ASCII snake_case через `normalize.Identifier`.
|
||||
- `dataDescriptor` из мапы ключей преобразуется в упорядоченный срез `sub_params` с сортировкой по числовому `id`.
|
||||
- Значения `valueList` и `default` приводятся к строковым представлениям (убираются пробелы, пустые значения приводятся к `nil`).
|
||||
|
||||
---
|
||||
|
||||
## 9. Что теряется
|
||||
|
||||
- HTTP-метод и путь обращения к API (в YAML отсутствуют; генератор считает их внешним знанием рантайма).
|
||||
- Динамические значения списков выбора (`valueList`): эндпоинт дефолтов `/instanceOperations/default/{id}` возвращает пустой `valueList` для полей, зависящих от конкретного тенанта/инстанса (например, доступные `ipSpaceName` для конкретного VDC/Org).
|
||||
|
||||
---
|
||||
|
||||
## 10. Где происходят потери
|
||||
|
||||
- Потери HTTP-метаданных (метод, URL) происходят на этапе маршалинга структуры `types.ServiceSpec` в YAML ([TOOLS/yaml-generator/main.go](../TOOLS/yaml-generator/main.go#L94-L105)), так как они изначально отсутствуют в контракте [TOOLS/lib/types.go](../TOOLS/lib/types.go).
|
||||
- Отсутствие runtime `valueList` обусловлено вызовом шаблонного эндпоинта `/instanceOperations/default/{id}` вместо запроса контекста живого инстанса.
|
||||
|
||||
---
|
||||
|
||||
## 11. Какие данные доступны в API YAML
|
||||
|
||||
- Полный перечень всех сервисов, операций (`create`, `modify`, `delete`, `suspend`, `resume`, сабресурсов) и их параметров.
|
||||
- Точные типы платформы (`data_type`) и иерархия подполей (`sub_params`).
|
||||
- Идентификаторы `id` (CFS param IDs) и символические коды (`code`).
|
||||
- Метаданные валидации (обязательность, регулярные выражения, ограничения диапазонов).
|
||||
|
||||
---
|
||||
|
||||
## 12. Какие данные доступны только в API-модели
|
||||
|
||||
- Динамические списки допустимых значений (`valueList`), вычисляемые бэкендом для конкретного состояния конкретного инстанса (например, список реально существующих ipSpaces организации при вызове modify на Edge).
|
||||
- Внутренние служебные поля платформы CFS, отфильтрованные моделью генератора (`contractId`, `contragentId`, `nestedRefData`, `config`, `statePath`, `expression`).
|
||||
|
||||
---
|
||||
|
||||
## 13. Неизвестные места
|
||||
|
||||
- Неизвестно, возвращает ли платформа Nubes какую-либо схему валидации для эндпоинтов отката/деаллокации (например, принимает ли `vc_org.modify` пустой массив `vIPConfigure: []` для полного снятия или требует только уменьшения `count: 0`), так как в дефолтной модели операции 207 описан только общий формат `vIPConfigure`.
|
||||
|
||||
---
|
||||
|
||||
## 14. Что необходимо проверить фактическим API
|
||||
|
||||
- Поведение `vc_org` (операция 207) при передаче `vIPConfigure: []` против `vIPConfigure: [{"name": "...", "count": 0}]` при попытке полной деаллокации IP-пространства.
|
||||
- Поведение `vc_nsxt` (операция 111) при передаче `ipSpaceName: "no-needed"` на различных окружениях (Dev/Test/Prod).
|
||||
|
||||
---
|
||||
|
||||
## Мнение Opus: финальное ревью отчёта
|
||||
|
||||
Отчёт признан годным и принят как вход для дальнейшего архитектурного этапа.
|
||||
|
||||
### Достаточно для дальнейшей работы
|
||||
|
||||
- Цепочка `API model → yaml-generator → API YAML` прослежена по коду, а не по догадкам: `classifyOperation`, `params.Merge`, разворачивание `dataDescriptor` в `sub_params`.
|
||||
- Обе обязательные трассировки (`vc_org.vIPConfigure.count`, `vc_nsxt.ipSpaceName`) доведены до YAML с подтверждением «потерь нет».
|
||||
- Зафиксирован ключевой факт: динамический `valueList` есть только в рантайме живого инстанса, а в API YAML его нет. Это прямое ограничение для будущего modifier-слоя.
|
||||
- Неизвестное поведение payload деаллокации (`vIPConfigure: []` против `count: 0`) помечено как `unknown`, а не закрыто предположением.
|
||||
|
||||
### Следствия перед архитектурным этапом
|
||||
|
||||
- Под ярлыком «Ordinary Resource Generator» в отчёте упоминаются два разных инструмента: `yaml-generator` пишет спеки, а `resource-generator` создаёт Go-ресурсы. Для проектирования `ModifierSpec` это две разные точки вмешательства.
|
||||
- `vIPConfigure` и `ipSpaceName` присутствуют только в `modify` и отсутствуют в `create`. В текущей схеме они сливаются в общий ресурс и не имеют отдельного жизненного цикла. Это корень задачи модификаторов.
|
||||
- Runtime-`valueList` придётся получать не из дефолтного эндпоинта, а из контекста инстанса. Это вопрос рантайма провайдера, а не генератора.
|
||||
|
||||
### Итоговая оценка
|
||||
|
||||
Ошибок, которые ломали бы выводы отчёта, не выявлено. Отчёт можно использовать как подтверждённую основу для дальнейшего проектирования.
|
||||
@@ -0,0 +1,69 @@
|
||||
# HAR-разбор: SNAT / ipSpace / модификации (dev)
|
||||
|
||||
Дата: 2026-09-22. Источник: `/home/naeel/TF/tf_provider/HAR/*.har` (записи UI на dev-стенде, 2026-09-20).
|
||||
Релевантные файлы: `edge_.har` (SNAT/Edge), `ipSpace0.har`, `org_enough_.har`, `org_not_enough_.har`, `org0.har`.
|
||||
|
||||
## Поток modify в реальном API
|
||||
|
||||
1. `POST /api/v1/svc/instanceOperations` — `{"instanceUid":"...","operation":"modify"}` → возвращает `instanceOperationUid`.
|
||||
2. `POST /api/v1/svc/instanceOperationCfsParams` — по одному запросу на параметр:
|
||||
`{"paramValue":"...","instanceOperationUid":"...","svcOperationCfsParamId":NNN}`.
|
||||
3. `GET /svc/instanceOperations/{id}/validate-cfs`
|
||||
4. `POST /svc/instanceOperations/{id}/run`
|
||||
5. Поллинг `GET /svc/instanceOperations/{id}`.
|
||||
|
||||
## Найденные payload-и
|
||||
|
||||
| Операция | param id | код | значение из HAR |
|
||||
|---|---|---|---|
|
||||
| edge modify | 368 | `needEnableAVI` | `false` / `true` |
|
||||
| edge modify | 369 | `virtualServicesCount` | `1` / `2` |
|
||||
| edge modify | 856 | `qosProfile` | `QoS-100Mbit` |
|
||||
| edge modify | **372** | **`ipSpaceName`** | **`no-needed`** / `""` |
|
||||
| edge modify | 1112 | `routedNetConfiguration` | `{"mainDns":"81.22.46.22","secondDns":"185.247.187.77","ipAddrPool":"10.10.102.0/24"}` |
|
||||
| org modify | **662** | **`vIPConfigure`** | `[{"name":"internet-ipv4-v1","count":"3"}]` |
|
||||
|
||||
## Ответы на открытые вопросы
|
||||
|
||||
1. **Тумблера «Выделить VIP для SNAT» в API НЕТ.** SNAT управляется целиком через `ipSpaceName` (param 372).
|
||||
Его `valueList` (из метаданных в HAR): `no-needed, internet-antiddos-v1, internet-no-antiddos-v1, ...` — то есть `no-needed` это легальное значение «SNAT не нужен».
|
||||
- Включить SNAT: `ipSpaceName = <имя ipSpace из org>`.
|
||||
- Выключить: `ipSpaceName = "no-needed"`.
|
||||
2. ✅ **Каноническое «SNAT выключен» = `no-needed`.** Подтверждено: в UI (Edge → Modify → поле «ip Space для VIP», параметр `ipSpaceName`) текущее значение показывается как `no-needed`. Reverse для SNAT = `modify` с `ipSpaceName="no-needed"` → delete SNAT-модификатора можно реализовать не как no-op. (`""` из `ipSpace0.har` — не каноническое, а промежуточное состояние.)
|
||||
3. 🟡 **Де-аллокация IP в org — попытка зафиксирована (`org2.har`, 2026-09-22):** UI отправил `modify` с
|
||||
`vIPConfigure=[{"name":"internet-ipv4-v1","count":"2"}]` (count уменьшен с 3 до 2).
|
||||
HTTP-ошибки НЕТ, но операция осталась в `isPending:true` — не выполнилась (согласуется с ограничением ниже).
|
||||
**Вывод:** payload де-аллокации = ТА ЖЕ структура `vIPConfigure`, только меньше `count` (не отдельная операция).
|
||||
Точная семантика «удалить совсем» (`count=0` или опустить элемент) не подтверждена.
|
||||
🔴 **Ограничение (подтверждено):** уменьшить/удалить ipSpace в `vcOrg` **нельзя, пока существуют дочерние инстансы** (VDC/Edge/кластер).
|
||||
Следствие: reverse возможен только ПОСЛЕ уничтожения детей → порядок destroy критичен:
|
||||
`кластер → SNAT-модификатор (no-needed) → org IP de-alloc → edge → vdc → org`.
|
||||
Чтобы снять payload «удалить совсем», нужен чистый org без детей (или плановый teardown).
|
||||
|
||||
## Побочные факты
|
||||
|
||||
- У `ipSpaceName` (372) в API есть `valueList`, но в нашем YAML его **нет** → проверить, тянет ли генератор `valueList` (возможно, он динамический: имена ipSpace конкретной org).
|
||||
- Имя ipSpace в живом примере — `internet-ipv4-v1` (не произвольное).
|
||||
- `qosProfile` (856) UI всегда шлёт как `QoS-100Mbit`.
|
||||
- `routedNetConfiguration` передаётся JSON-строкой.
|
||||
- В состоянии org: `"vip":{"no-needed":{},"internet-ipv4-v1":{"count":4}}` — `no-needed` фигурирует и в стейте.
|
||||
|
||||
## Наблюдения на возможно сломанном Edge (2026-09-22, nsx_WZ03709-saas-wmfop5be)
|
||||
|
||||
⚠️ ВАЖНО: этот Edge, судя по всему, в сломанном состоянии (devops-проблема).
|
||||
Ошибки ниже **НЕ считать универсальными правилами API** — перепроверить на здоровом Edge.
|
||||
|
||||
1. `modify` 14:40:52 → «ipSpace '' не найден на https://sandbox.nubes.ru» — при пустом `ipSpaceName` бэкенд отклонил запрос. ❓ Возможно, следствие сломанного Edge, не правило.
|
||||
2. `modify` 14:43:44 → «Insufficient rule blocks» при попытке снять «Включить ALB». ❓ Возможно, застрявшие VS/SE Group, не правило.
|
||||
3. `delete` (2 раза) → FORBIDDEN «Cannot delete SE Group assignment … since there are Virtual Services». ❓ Возможно, застрявшие VS, не правило.
|
||||
|
||||
**Что остаётся надёжным (из API-метаданных, НЕ из этих ошибок):**
|
||||
- `valueList` у `ipSpaceName` содержит `no-needed` (+ имена ipSpace) — из описания параметра.
|
||||
- UI показывает `no-needed` как текущее значение при выключенном SNAT.
|
||||
|
||||
## Что ещё нужно выяснить из UI (открытые вопросы)
|
||||
|
||||
1. 🔴 **Де-аллокация IP в org — пока НЕ снять:** UI/бэкенд не даёт удалить ipSpace, пока есть дочерние инстансы (подтверждено 2026-09-22). Нужен чистый org или плановый teardown. Гипотеза payload — `vIPConfigure=[]` (unverified).
|
||||
2. ✅ **Имя ipSpace — выбор ИЗ СПИСКА** (подтверждено UI). Свободного ввода нет → список динамический (текущие ipSpace org + `no-needed`).
|
||||
Следствие для провайдера: `ip_space_name` в SNAT-модификаторе должен браться из **computed-вывода org-модификатора**, а не быть свободной строкой.
|
||||
3. 🟡 Полное удаление ipSpace и поведение при destroy Edge с включённым SNAT — на будущее (блокировано п.1).
|
||||
@@ -0,0 +1,122 @@
|
||||
# Ответ Opus: анализ решения IaC-развёртывания Штурвала (модификаторы + скрытые зависимости)
|
||||
|
||||
**Дата:** 2026-09-23
|
||||
**Связанный промпт:** `NOTES/20_prompts/prompt_for_opus_iac_shturval_modify.md`
|
||||
**Связанный анализ:** `NOTES/30_analysis/SHTURVAL_IAC_MODIFY_ANALYSIS_2026-09-23.md`
|
||||
**Статус:** документирование ответа. Конкретный план НЕ составляется.
|
||||
|
||||
> ⚠️ **ВАЖНАЯ ПОПРАВКА (2026-09-23, позже).** Ответ Опуса ниже строился на НЕВЕРНОЙ посылке «`vIPConfigure` — накопительный API». Это опровергнуто тестом `NOTES/30_analysis/ORG_IP_MODIFIER_TEST_2026-09-22.md`: `vIPConfigure` ведёт себя как **replace-состояние** — идемпотентно (1→1), работает в обе стороны (вверх/вниз/до 0), `count` читается из `state.params`. Соответственно «блокеры» (a) Read счётчика и (b) адресное освобождение **сняты как ложные**. Остаётся только (c) Read цепочки `providerVdc → providerGateway → ipSpace`. НЕ использовать прежнюю формулировку «накопительный API несовместим с декларативной моделью» как источник истины.
|
||||
|
||||
---
|
||||
|
||||
## 1. Суть ответа (главный вывод)
|
||||
|
||||
Форма IaC-ресурсов фиксируется **уже сейчас**, потому что она диктуется моделью Terraform (декларативность, идемпотентность, inverse), а не спеками платформы.
|
||||
|
||||
НО есть **три блокера от платформы**, без которых идемпотентность и Delete принципиально недостижимы на стороне провайдера.
|
||||
|
||||
---
|
||||
|
||||
## 2. Ответ Opus по пунктам
|
||||
|
||||
### Пункт 1 — накопительный `vIPConfigure` → идемпотентный ресурс
|
||||
|
||||
- Ресурс отдельный (`nubes_org_vip_allocation`) с `depends_on` на оргу, НЕ операция внутри орги.
|
||||
- **Ключ идемпотентности — желаемое состояние, а не дельта.** Юзер задаёт целевой `count` на `name`; провайдер сам считает `target − current` и модифицирует только разницу.
|
||||
- **Create:** Read текущего числа vIP → выделить `target − current`. Если API не отдаёт «сколько уже есть» — нужен серверный счётчик/тег, иначе идемпотентность недостижима.
|
||||
- **Read:** читать родителя (оргу), извлекать фактическое число IP по `name` в state. Если API не различает «кем/зачем выделено» — Read вернёт общий пул, drift неизбежен.
|
||||
- **Update:** та же дельта-логика (target изменился → доначислить/освободить).
|
||||
- **Delete (inverse):** `modify` с обратным знаком до `count=0` по этому `name`. Требует адресного освобождения конкретных IP. Если освобождение — тоже накопительный modify без адресации, inverse корректно сделать нельзя.
|
||||
|
||||
**Риск (ОПРОВЕРГНУТ позже):** это утверждение строилось на ложной посылке «накопительный API». Факт: `vIPConfigure` — replace-состояние, дельта `target − current` по факту не нужна — достаточно слать целевой `count`, платформа сама выставляет его (идемпотентно). См. `ORG_IP_MODIFIER_TEST_2026-09-22.md`.
|
||||
|
||||
### Пункт 2 — `ipSpaceName` (цепочка providerVdc → providerGateway → ipSpace)
|
||||
|
||||
- Это **выводимое значение из инфраструктуры, НЕ пользовательский ввод** → data-source, а не аргумент ресурса.
|
||||
- Правильно: `data "nubes_ip_space" { org/vdc = ... }`, который проходит цепочку providerVdc → providerGateway → ipSpace и возвращает `name`. Ресурс берёт значение по ссылке.
|
||||
- **Граница «данные vs логика»:** в реестре хранить **тип поля и его источник** (что это computed-from-parent, а не user-input). Сама цепочка обхода — логика data-source, не данные реестра.
|
||||
- НЕ вычислять из state родителя вручную в ресурсе (скрытая связанность, ломается при >1 T0). Data-source явно выражает зависимость в графе tf.
|
||||
- Пока платформа «подкладывает» значение сама — data-source должен уметь то же читать. Если API этой цепочки нет на чтение — **блокер**.
|
||||
|
||||
### Пункт 3 — не завязываться на «один T0»
|
||||
|
||||
- Закладывать **явный селектор шлюза** уже сейчас: `provider_gateway` / `t0_id` как аргумент (или ключ data-source), даже если сегодня один и выводится автоматически (optional + computed default).
|
||||
- vIP-аллокация и SNAT привязывать к **конкретному gateway id**, а не к «дефолтному в орге».
|
||||
- **Что сломается при >1 T0, если не заложить:** `ipSpaceName` станет неоднозначным (несколько ipSpace), vIP-аллокация не будет знать, к какому шлюзу. Придётся менять схему (добавлять обязательный селектор) → breaking change.
|
||||
- **Заложив optional-селектор сейчас:** при росте T0 меняется только default-резолвинг, схема остаётся совместимой.
|
||||
|
||||
### Пункт 4 — ждать спеки или фиксировать форму сейчас
|
||||
|
||||
- **Форму ресурсов можно и нужно фиксировать сейчас** — она диктуется моделью Terraform, а не спеками.
|
||||
- **Не блокер (делаем сейчас):** раздельные ресурсы + `depends_on`; целевое состояние вместо дельты; селектор шлюза; data-source для `ipSpaceName`; inverse через обратный modify.
|
||||
- **Блокер (нужно от платформы) — только один подтверждённый:**
|
||||
- c) API чтения цепочки providerVdc → providerGateway → ipSpace (иначе data-source невозможен).
|
||||
- **Сняты как ложные (опровергнуты тестом 2026-09-22):**
|
||||
- a) чтение текущего числа vIP — УЖЕ работает через `state.params.vIPConfigure`;
|
||||
- b) адресное освобождение IP — УЖЕ работает: `count` меньше/`0` задаётся тем же `modify`, в обе стороны.
|
||||
- **Вывод:** проектируем форму сейчас, блокер только (c). Новые спеки повлияют на **резолвинг значений**, не на форму ресурсов — если форма построена на «целевое состояние + селектор + data-source».
|
||||
|
||||
### Пункт 5 — минимально-инвазивный порядок внедрения
|
||||
|
||||
**От платформы (до кодинга ресурсов) — обязательно:**
|
||||
- Read цепочки → `ipSpaceName` (для data-source).
|
||||
- Подтверждение, что генератор умеет строить схему из объединения `create`+`modify` полей (иначе `vIPConfigure`/`ipSpaceName` вообще не попадут в схему).
|
||||
|
||||
> ⚠️ Read счётчика vIP и адресное освобождение — НЕ блокеры (уже подтверждено тестом). Исключены.
|
||||
|
||||
**На стороне провайдера — можно сейчас, не дожидаясь:**
|
||||
- Раздельные ресурсы vip-allocation / nsxt-snat с `depends_on`.
|
||||
- Логика «target − current = дельта» (заглушка current, пока нет Read).
|
||||
- Data-source-скелет для `ipSpaceName` (с TODO на реальный обход цепочки).
|
||||
- Optional+computed селектор шлюза.
|
||||
- Inverse-контракт (Delete = обратный modify до нуля).
|
||||
|
||||
**Главный неустранимый на нашей стороне блокер:** ❌ СНЯТ — строился на ложной посылке «накопительный API». Реальный остаточный блокер — только (c) чтение цепочки providerVdc → providerGateway → ipSpace (для data-source `ipSpaceName`).
|
||||
|
||||
---
|
||||
|
||||
## 3. Что нового vs то, что уже собирались делать
|
||||
|
||||
### Совпадает со старыми планами (НЕ новое)
|
||||
|
||||
- Отдельный ресурс под модификацию + `depends_on` — было (`PLAN_modifier_redesign.md`, «resource association»).
|
||||
- Inverse через обратный modify (`count→0`) — было (`inverse_rollback_analysis_2026-09-23.md`).
|
||||
- Идемпотентность (skip run, если live уже целевое) — было.
|
||||
- «Не ждать спеки для формы, а фиксировать сейчас» — по сути было.
|
||||
|
||||
### Реально новое у Опуса
|
||||
|
||||
1. **«Целевое состояние, а не дельта»** — строгий принцип: юзер задаёт целевой `count`, провайдер сам считает `target − current`. Старые планы просто «досылали заданные поля», не формализовали желаемое состояние.
|
||||
2. **`ipSpaceName` — data-source, не аргумент ресурса** — сдвиг от «юзер вписывает значение» к «computed-from-parent». Раньше виделось как ввод.
|
||||
3. **Селектор шлюза (`t0_id`/`provider_gateway`) как optional+computed сейчас** — в старых планах про «один T0» вообще не было (пришло только из реплики Виталия).
|
||||
4. **Чёткая граница «данные vs логика»** — в реестре только «тип поля + что computed-from-parent», цепочка обхода — логика data-source.
|
||||
5. **Блокеры от платформы** — из трёх заявленных Опуса два (Read счётчика, адресное освобождение) **ложны** (опровергнуты тестом), остаётся один реальный: Read цепочки providerVdc→providerGateway→ipSpace.
|
||||
|
||||
### Главное отличие одной фразой
|
||||
|
||||
Старые планы отвечали на «**как сделать модификатор в tf**». Опус отвечает на «**как сделать его идемпотентным и IaC-честным**» — но его центральный вывод «ядро проблемы в платформе (накопительный API)» **оказался ошибочным**, т.к. исходная посылка «накопительный» неверна (см. поправку в шапке). Реальный остаток — только `ipSpaceName` (цепочка providerVdc→providerGateway→ipSpace) и селектор шлюза.
|
||||
|
||||
---
|
||||
|
||||
## 4. Спорный/непроверенный момент — РАЗРЕШЁН
|
||||
|
||||
Посылка Опуса «накопительный `vIPConfigure` без Read-счётчика и адресного освобождения несовместим с декларативной моделью» **опровергнута** тестом `NOTES/30_analysis/ORG_IP_MODIFIER_TEST_2026-09-22.md`:
|
||||
|
||||
- повторный `modify` с тем же `count` не аккумулирует IP (1→1) → идемпотентно;
|
||||
- `count` меняется в обе стороны (2→1→0) через тот же `modify` → «адресное освобождение» не нужно, достаточно задать меньший/нулевой `count`;
|
||||
- `count=0` принимается (несмотря на `minvalue:1` в схеме), элемент ipSpace остаётся в `state.params`;
|
||||
- Read уже есть: `state.params.vIPConfigure = [{"name":"internet-ipv4-v1","count":N}]`.
|
||||
|
||||
Единственный реально непроверенный момент: полное удаление ipSpace (`[]` / отсутствие элемента) — тест этого не покрывал. Для IaC-задачи «обнулить» достаточно, полное удаление — опционально.
|
||||
|
||||
---
|
||||
|
||||
## 5. Резюме (с поправкой)
|
||||
|
||||
- Форма ресурсов — проектируем сейчас, она не зависит от спеков.
|
||||
- `vIPConfigure` — **НЕ блокер**: идемпотентно, обе стороны, `count` читается/задаётся из `state.params` (тест 2026-09-22).
|
||||
- Единственный подтверждённый блокер: **Read цепочки `providerVdc → providerGateway → ipSpace`** для data-source `ipSpaceName` (c).
|
||||
- Непроверено: полное удаление ipSpace (`[]`), но для задачи достаточно `count=0`.
|
||||
- Конкретный план внедрения пока НЕ составляется (по решению пользователя).
|
||||
|
||||
**Следующий возможный шаг (только по запросу):** проверить (c) чтение цепочки ipSpace живым API.
|
||||
@@ -0,0 +1,35 @@
|
||||
# Проверка модификатора vc_org ip_space (modify 207) — 2026-09-22
|
||||
|
||||
Организация: `NarodOrg` (`9890a8a0-040b-4d56-8018-c31519c35a30`), realm `sandbox.nubes.ru`.
|
||||
Стенд: `DEV_STAND/FullPipe`, провайдер `nubes-dev` 2.0.9.
|
||||
|
||||
## Что делали
|
||||
|
||||
1. Переименовали `org_ips.tf` → `terraform apply` — ошибок нет (ресурс ушёл из state; `Delete` модификатора — no-op).
|
||||
2. Вернули файл → `apply` — ошибок нет, **число IP осталось 1** (повторный `modify` с тем же `count=1` НЕ задвоил).
|
||||
3. `org_ip_count = 2` → `apply` — стало 2.
|
||||
4. `org_ip_count = 1` → `apply` — стало 1.
|
||||
5. `org_ip_count = 0` → `apply` — стало 0.
|
||||
|
||||
## Подтверждено по API
|
||||
|
||||
`GET /instances/9890a8a0-040b-4d56-8018-c31519c35a30`:
|
||||
|
||||
- `state.params.vIPConfigure = [{"name":"internet-ipv4-v1","count":0}]`
|
||||
- последняя операция `modify` — `isSuccessful: true`, `isPending: false`, `isInProgress: false`.
|
||||
|
||||
## Выводы (обновляют прежние гипотезы)
|
||||
|
||||
1. **modify работает в обе стороны** (count вверх/вниз/до 0) на здоровой орге, даже при живых дочерних инстансах (VDC/Edge).
|
||||
2. **modify идемпотентен** — повторный `modify` с тем же `count` не аккумулирует IP (1 → 1).
|
||||
3. **`count=0` принимается**, хотя в схеме операции 207 у `count` стоит `minvalue: 1` / `integer > 0` — валидация не отвергает 0. `count=0` = ноль выделенных IP (элемент ipSpace остаётся в state).
|
||||
4. **`Delete` модификатора — no-op** подтверждён (шаг 1), но это восполнимо: повторный `apply` с нужным `count` корректно восстанавливает состояние.
|
||||
|
||||
## Опровергнуто
|
||||
|
||||
- Утверждение из `NOTES/30_analysis/HAR_SNAT_MODIFY_FINDINGS.md` «уменьшить/удалить ipSpace нельзя, пока существуют дочерние инстансы» — **не подтвердилось на здоровой орге** (в HAR был сломанный Edge; это и было помечено как неподтверждённое наблюдение).
|
||||
- Опасение из Opus-ревью о «двойном выделении при replace/destroy→apply» — в части повторного `modify` с тем же `count` **не воспроизвелось** (идемпотентно).
|
||||
|
||||
## Открытый вопрос
|
||||
|
||||
- Полное удаление ipSpace (пустой массив `[]` / отсутствие элемента) не тестировалось — `count=0` оставляет элемент `{"name":"internet-ipv4-v1","count":0}` в state.
|
||||
@@ -0,0 +1,38 @@
|
||||
# 30_analysis — анализы, отчёты, разборы
|
||||
|
||||
Фактические материалы: форензика, разборы багов, ответы LLM-ревью, аналитика по кодовой базе.
|
||||
|
||||
> Правило: **факт без источника не факт.** Ниже у каждого файла указано, проверен ли он на стенде.
|
||||
|
||||
## Ядро по задаче «IaC + modify» (актуально)
|
||||
|
||||
| Файл | Что внутри | Статус |
|
||||
|---|---|---|
|
||||
| `SHTURVAL_IAC_MODIFY_ANALYSIS_2026-09-23.md` | Анализ: почему `modify` не выражается, прецедент VCD, скрытые зависимости (`providerVdc → providerGateway → ipSpace`), варианты A–E, мнение | ✅ Актуально |
|
||||
| `OPUS_ANSWER_IAC_SHTURVAL_MODIFY_2026-09-23.md` | Ответ Opus по промпту IaC **+ поправка**: два его «блокера» (Read счётчика, адресное освобождение) ложны | ✅ Актуально (читать с поправкой в шапке) |
|
||||
| `ORG_IP_MODIFIER_TEST_2026-09-22.md` | **Единственная проверка на живом стенде**: `vIPConfigure` идемпотентен (1→1), работает вверх/вниз/до 0, `count=0` принимается, Read из `state.params` | ✅ Актуально, высокое доверие |
|
||||
|
||||
## Форензика и отладка
|
||||
|
||||
| Файл | Что внутри | Статус |
|
||||
|---|---|---|
|
||||
| `FORENSIC_ANALYSIS_BRIEF_2026-09-23.md` | Бриф: что исследовать в цепочке API → YAML, строгие ограничения (не проектировать решения) | ✅ Актуально как рамка |
|
||||
| `FORENSIC_ANALYSIS_REPORT_2026-09-23.md` | Отчёт по брифу: потерь данных API→YAML нет; теряются динамические `valueList` и HTTP-метаданные | ✅ Актуально |
|
||||
| `HAR_SNAT_MODIFY_FINDINGS.md` | Находки по SNAT/`modify` из HAR. ⚠️ Часть утверждений **опровергнута** тестом 2026-09-22 (см. `ORG_IP_MODIFIER_TEST…`) | ⚠️ Частично устарело |
|
||||
| `DEBUG_REPORT_VC_VDC_500.md` | Разбор ошибки 500 при работе с vc/vdc | ⚠️ Историческое |
|
||||
| `DEBUG_REPORT_VM_FIX.md` | Разбор/фикс проблемы с VM | ⚠️ Историческое |
|
||||
| `inverse_rollback_analysis_2026-09-23.md` | Inverse-откат: `delete_params {Code, Mode, Value, zero_fields, off_value}`, порядок destroy. Модель — от отменённого механизма; факты (`count=0`, `no-needed`) верны | ⚠️ Частично устарело (баннер в файле) |
|
||||
|
||||
## Обзоры кодовой базы и архитектуры
|
||||
|
||||
| Файл | Что внутри | Статус |
|
||||
|---|---|---|
|
||||
| `CODEBASE_ANALYSIS_AND_ROADMAP.md` | Обзор репозитория + roadmap | ⚠️ Проверить актуальность по дате |
|
||||
| `ARCHITECTURE_NEW.md` | «Universal Rebuild»: ядро, генераторы, YAML-спеки. Ссылается на пути `universal_rebuild/*` | ⚠️ Историческое (пути не совпадают с текущим репо) |
|
||||
| `YAML_ANALYSIS_43_SERVICES.md` | Анализ 43 сервисов по YAML-спекам | ⚠️ Проверить актуальность по дате |
|
||||
| `opus_review_answer.md` | Ответ Opus на `prompt_for_opus_review.md` (ревью архитектуры, 3 задачи roadmap) | ⚠️ Историческое |
|
||||
|
||||
## Связанное
|
||||
|
||||
- Сырые данные: `../../HAR/` (дампы live-запросов), `../../generated/<стенд>/resources_yaml/` (спеки).
|
||||
- Хендовер по текущему состоянию: `../40_chat_summaries/CHAT_RESUME_IAC_2026-09-24.md`.
|
||||
@@ -0,0 +1,169 @@
|
||||
# Штурвал через IaC: анализ проблемы `modify` и скрытых платформенных зависимостей
|
||||
|
||||
**Дата:** 2026-09-23
|
||||
**Контекст:** дискуссия в Telegram про запуск цепочки Штурвал полностью через Terraform.
|
||||
|
||||
---
|
||||
|
||||
## 1. Исходная задача
|
||||
|
||||
Клиенту нужен IaC (Infrastructure as Code): вся инфраструктура описывается одним конфигом, команда `terraform apply` разворачивает её целиком, `plan`/`destroy` дают полную картину. Никаких обязательных ручных шагов посередине.
|
||||
|
||||
Цепочка для стенда Штурвал:
|
||||
|
||||
```text
|
||||
vcOrg -> create
|
||||
vcVdc -> create
|
||||
vcNsxt -> create
|
||||
------------------------------
|
||||
vcOrg -> modify (аллоцировать внешние IP в организацию)
|
||||
vcNsxt -> modify (включить SNAT, указать внешний IP из vcOrg)
|
||||
------------------------------
|
||||
k8sShturval -> create
|
||||
```
|
||||
|
||||
Ключевой конфликт: `create` у Terraform работает штатно, а операции `modify` в текущем провайдере никак не выражаются — Terraform не умеет «создать ресурс, а через несколько шагов поменять в нём же параметр».
|
||||
|
||||
---
|
||||
|
||||
## 2. Почему `modify` не выражается в текущем провайдере
|
||||
|
||||
### 2.1. Генератор строит схему только из `create`
|
||||
|
||||
Провайдер генерируется из YAML-спеков (`generated/{stand}/resources_yaml/*.yaml`). Схема ресурса (какие поля можно писать в `.tf`) строится **только из операции `create`**. Параметры, которые есть только в `modify`, в схему не попадают.
|
||||
|
||||
Подтверждено по файлам:
|
||||
|
||||
- `generated/dev/resources_yaml/19_vc_org.yaml`:
|
||||
- `create` (id 136) → только `resourceRealm` (418), `organizationType` (556), `orgSuffix` (1125);
|
||||
- `vIPConfigure` (выделение внешних IP) есть **только** в `modify` (id 207), с sub-полями `name` (39) и `count` (40).
|
||||
- `generated/dev/resources_yaml/22_vc_nsxt.yaml`:
|
||||
- `create` (id 10) → `vdcUid`, `needEnableAVI` (340), `virtualServicesCount` (341), `qosProfile` (825), `routedNetConfiguration` (1110) и др.;
|
||||
- `ipSpaceName` (372) есть **только** в `modify` (id 111).
|
||||
|
||||
Вывод: `vIPConfigure` (vc_org) и `ipSpaceName` (vc_nsxt) живут только в `modify`, в схеме tf-ресурсов их нет. Поэтому «прописать параметр в tf и сделать apply» падает ещё на `plan` (атрибут не известен провайдеру).
|
||||
|
||||
### 2.2. Эти параметры — не «настройки», а отложенные действия
|
||||
|
||||
- `vIPConfigure=[{name,count}]` — задаёт желаемое число внешних IP целиком. **Не накопительная** (повторный вызов с тем же `count` не аккумулирует, подтверждено `NOTES/30_analysis/ORG_IP_MODIFIER_TEST_2026-09-22.md`), работает в обе стороны (вверх/вниз/до `count=0`). Это **декларативное значение** в смысле «желаемое количество IP по данному ipSpace».
|
||||
- `ipSpaceName` — включение SNAT на конкретный ipSpace, который возникает **только после** того, как на орге выделены IP.
|
||||
- Эти операции требуют порядка (org.modify → затем nsxt.modify) и зависят от живого состояния инстанса, а не от дефолтов формы.
|
||||
|
||||
### 2.3. Схема в state ≠ реальное состояние
|
||||
|
||||
Вписывать недостающие параметры «насильно» в tfstate нельзя и бесполезно:
|
||||
|
||||
1. Terraform валидирует атрибуты по **схеме провайдера**, а не по state — неизвестный атрибут будет отброшен/вызовет ошибку.
|
||||
2. Записывать в state «SNAT включён», когда этого нет на площадке, — значит получить ложный `plan` (чистый) при сломанной инфраструктуре.
|
||||
3. Ручная правка tfstate/`state push` ломает целостность (серийник, конфликты на следующем apply).
|
||||
|
||||
Работает только косвенно: `terraform_data`/`null_resource` + `local-exec` → в state попадает **факт** «операция выполнена» (маркер с `triggers`), но не **состояние** SNAT/IP. Порядок задаётся через `depends_on`, но дрейф по самим параметрам `plan` не видит.
|
||||
|
||||
---
|
||||
|
||||
## 3. Каноничное решение: отдельный ресурс (resource association)
|
||||
|
||||
Это принятая в Terraform практика — «resource association / separate resource». Классические примеры:
|
||||
|
||||
- `aws_security_group` + `aws_security_group_rule`
|
||||
- `aws_vpc` + `aws_route_table_association`
|
||||
- `google_project` + `google_project_iam_member`
|
||||
|
||||
Базовый ресурс создаётся отдельно, а донастройка/привязка — отдельным ресурсом с `depends_on`. Граф сам выстраивает порядок, `destroy` разворачивает его корректно.
|
||||
|
||||
### 3.1. Прецедент из Cloud Director (VCD)
|
||||
|
||||
В репозитории лежит сторонний шаблон — `/home/naeel/TF/tf_provider/!/` (network.tf.tmpl, vmware_org.tf, vdc.tf), показывающий, как та же цепочка делается провайдером VMware Cloud Director:
|
||||
|
||||
- `resource "vcd_nsxt_alb_settings"` — включение ALB, `count = var.alb_enable ? 1 : 0`, `depends_on = [vcd_nsxt_edgegateway...]`;
|
||||
- `resource "vcd_nsxt_alb_edgegateway_service_engine_group"` — выделение SE, `reserved_virtual_services = var.alb_segroup_count`;
|
||||
- `resource "vcd_network_routed_v2"` — routed-сеть, `edge_gateway_id`, `dns1/dns2/static_ip_pool`;
|
||||
- `resource "vcd_ip_space_custom_quota"` — квота IP на **оргу**, `depends_on = [edge]`.
|
||||
|
||||
Приём «включить/выключить» = `count`. Обратная операция (выключить ALB / снять квоту) получается **удалением ресурса** — inverse логика не нужна.
|
||||
|
||||
Маппинг на наши сервисы:
|
||||
|
||||
| Nubes API | Канон VCD |
|
||||
|---|---|
|
||||
| `needEnableAVI` | `vcd_nsxt_alb_settings` (+ `count`) |
|
||||
| `virtualServicesCount` | `reserved_virtual_services` в SE-группе |
|
||||
| `routedNetConfiguration` (mainDns/secondDns/ipAddrPool) | `vcd_network_routed_v2` (`dns1/dns2/static_ip_pool`) |
|
||||
| `vIPConfigure` (IP на оргу) | `vcd_ip_space_custom_quota` (на оргу) |
|
||||
|
||||
### 3.2. Чем наш случай сложнее канона
|
||||
|
||||
В классическом паттерне ребёнок — **отдельный объект API** со своим CRUD (правило, association, attachment). Его можно создать/прочитать/удалить.
|
||||
|
||||
У нас отдельного объекта нет — есть **операция `modify` над родителем**. Поэтому требуются:
|
||||
|
||||
1. `Read` — не свой объект, а чтение состояния родителя;
|
||||
2. `Delete` — не удаление, а **обратный modify** (inverse);
|
||||
3. `Create/Update` — вызов той же операции с параметрами;
|
||||
4. идемпотентность (не дёргать `run`, если live уже целевое) и live-сверку.
|
||||
|
||||
Именно поэтому «просто завести поля из modify в схему» не работает — нужна полноценная механика, а не одна правка.
|
||||
|
||||
---
|
||||
|
||||
## 4. Более глубокая проблема: скрытые платформенные зависимости
|
||||
|
||||
Это главное из всей дискуссии (реплики Виталия Зайцева, 18:30–18:34).
|
||||
|
||||
### 4.1. `ipSpaceName` нельзя ввести вручную — он выводится
|
||||
|
||||
```text
|
||||
имя ipSpace → зависит от providerGateway
|
||||
providerGateway → зависит от providerVdc
|
||||
providerVdc → никто не знает изначально
|
||||
```
|
||||
|
||||
Пользователь **не может** заполнить `ipSpaceName`, потому что это значение выводится из внутренней топологии (providerVdc → providerGateway → ipSpace), а не из того, что он видел в ЛК. Это не «поле, которое забыли отдать через API», а **вычисляемое от скрытых зависимостей** значение.
|
||||
|
||||
### 4.2. Текущий костыль платформы
|
||||
|
||||
«При создании орги/vdc/edge, если организация ничего не знает про недостающие параметры — они подкладываются». То есть одноразовая подстановка при создании пустой орги, чтобы избавить пользователя от «мучительных приседаний» в ЛК.
|
||||
|
||||
### 4.3. Ограничение модели: один T0
|
||||
|
||||
«Другая проблема — что будет, если в облаке появится больше 1 T0». Пока принято допущение на уровне кода: **в организации всё одно подключение**. Решение осознанно отложено («пара лет спокойствия»), но для IaC это риск: текущее решение завязано на «в орге всегда один провайдер-шлюз».
|
||||
|
||||
### 4.4. Ожидание изменений спеков
|
||||
|
||||
«Мне надо увидеть, как спеки поменяются, чтобы понять, что исправлять… Надеюсь, появится сначала в sandbox.nubes.ru, а не в ngcloud». То есть платформа меняется, форма ресурсов зависит от **новых спеков**, и строить модификатор сейчас = работать по устаревшим спекам.
|
||||
|
||||
---
|
||||
|
||||
## 5. Итог: где правда
|
||||
|
||||
1. **Ручной ЛК и скрипт не подходят** — клиент требует IaC (Георгий прав). Это не «костыль против красоты», это невыполнение требования.
|
||||
|
||||
2. **Отдельный ресурс под модификацию — необходимое, но не достаточное условие.** Он закрывает «как expressить modify», но не закрывает «откуда юзер возьмёт значения».
|
||||
|
||||
3. **Главная блокировка — не Terraform, а платформа.** `ipSpaceName` (и подобные) выводятся из `providerVdc → providerGateway → ipSpace`, которые юзер не знает. Пока платформа не отдаёт эти значения в спеках (или провайдер не резолвит их data-источником), честный IaC не собрать — ни модификаторами, ни скриптом, ни руками.
|
||||
|
||||
4. **«Ломается агностичность» — верно, но это не порок, а цена.** Ресурсы-модификаторы доменные и «ручные», как в VCD. Без них IaC невозможен, прецедент — перед глазами (`!/network.tf.tmpl`).
|
||||
|
||||
5. **Состояние дел:** платформа в движении (ждут новые спеки). Правильная последовательность — дождаться, что придёт в спеках (snandbox), а затем решать форму ресурса; не строить по старым спекам.
|
||||
|
||||
---
|
||||
|
||||
## 6. Возможные пути (по убыванию «честности» перед IaC)
|
||||
|
||||
| Вариант | Что делает | Вердикт |
|
||||
|---|---|---|
|
||||
| **A. Полноценные ресурсы-модификаторы + data-источники** | отдельный tf-ресурс на modify + data-source, резолвящий `ipSpace`. Полный IaC. | правильно, но только после новых спеков |
|
||||
| **B. Data-source через `http`/`external` + `jsondecode`** | DevOps сам дёргает API и подставляет динамические списки, без правки провайдера | рабочая «дожималка», не полный IaC |
|
||||
| **C. `terraform_data`/`null_resource` + `local-exec`** | модификации скриптом, факт в state, порядок через `depends_on` | полумера, состояние SNAT/IP вне state |
|
||||
| **D. Прессеты/дефолтное окружение** | готовый набор компонентов, экспорт через провайдер | снижает боль на старте, IaC не заменяет |
|
||||
| **E. Ручной ЛК / скрипт вне tf** | модификации руками | не подходит (требование клиента) |
|
||||
|
||||
---
|
||||
|
||||
## 7. Моё мнение
|
||||
|
||||
**Коротко:** для настоящего IaC нужны обе вещи одновременно — **отдельный ресурс под `modify`** и **механизм получения динамических значений** (`ipSpace` и пр.). Пока платформа не отдаёт второе через API/спеки, все «быстрые» способы (скрипт, руками, пресеты) закрывают только симптом, а не требование клиента.
|
||||
|
||||
**Рекомендация:** не городить модификатор сейчас по устаревшим спекам. Дождаться изменений спеков (сначала sandbox), параллельно — обсчитать два blockers: (1) как провайдер будет резолвить `providerVdc → providerGateway → ipSpace` без ручного ввода; (2) допущение «один T0». После этого проектировать форму ресурсов.
|
||||
|
||||
**Что точно не делать:** вписывать параметры «насильно» в tfstate; ждать, что «прописал поле в tf → apply» заработает без правки провайдера. (`vIPConfigure` при этом НЕ накопительный — см. §2.2, тест 2026-09-22.)
|
||||
@@ -0,0 +1,58 @@
|
||||
> ⚠️ **ЧАСТИЧНО УСТАРЕЛО (пометка 2026-09-24).** Модель `delete_params`/`inverse`/`off_value` относится
|
||||
> к **отменённому** механизму модификаторов (метки в YAML + реестр в генераторе).
|
||||
> Но факты внутри — верны и переиспользуются: `count=0` канонический inverse, порядок destroy,
|
||||
> `ipSpaceName="no-needed"`, `needEnableAVI=false`. Актуально: `NOTES/40_chat_summaries/CHAT_RESUME_IAC_2026-09-24.md`.
|
||||
|
||||
# Inverse-откат модификаторов: анализ ответа Опуса — 2026-09-23
|
||||
|
||||
Источник: prompt_for_opus_inverse_architecture.md → ответ Опуса (принят, анализ ниже).
|
||||
|
||||
## Принятые решения (по Опусу)
|
||||
|
||||
1. **Модель delete_params** → заменить плоский `{Code, Value}` на `{Code, Mode, Value?}`:
|
||||
- `Mode: static` — значение из `Value` (дефолт, обратная совместимость).
|
||||
- `Mode: zero_count` — обнулить integer-поля в элементах array-map-fixed, взяв live.
|
||||
2. **Баланс данные/логика**: форма преобразования выводится из `dataType`
|
||||
(boolean→"false", array-map-fixed→zero integer); сентинелы-значения — ТОЛЬКО данные в реестре.
|
||||
3. **Маркер поля**: явный `zero_fields:["count"]` (или флаг на sub_param), а НЕ «обнулить все integer»
|
||||
(риск: порт/приоритет/индекс в том же object).
|
||||
4. **Порядок destroy** — обратный порядок создания из `depends_on`.
|
||||
|
||||
## Мои замечания к ответу (что Опуc недоговорил)
|
||||
|
||||
- **A. Граф уже правильный.** Факт: `edge_net` (SNAT) зависит от `org_ips` (IP), `org_ips` — от `edge`.
|
||||
Обратный порядок destroy: `edge_net → org_ips → edge → vdc` уже корректен.
|
||||
Рекомендация Опуса «сделать ip_space зависимым от edge_net» — перепутана направлением; граф уже такой.
|
||||
- **B. Источник live для zero_count в Delete не указан.** `Delete` модификатора имеет только TF `state`,
|
||||
а live `vIPConfigure` надо читать через `GetInstanceStateParams` в рантайме Delete.
|
||||
- **C. Отличие sentinel от имени в valueList не разобрано** (valueList без разметки sentinel в данных API).
|
||||
- **D. Идемпотентность zero_count при повторном destroy не поднята** (count=0 → снова 0: no-op?).
|
||||
|
||||
## Открытые вопросы (второй раунд к Опусу) — ЗАКРЫТЫ
|
||||
|
||||
1. **Источник live для zero_count в Delete** → `GetInstanceStateParams` (не TF state). ✅
|
||||
2. **Sentinel в valueList** → явный `off_value:"no-needed"` в реестре (данные, не логика). ✅
|
||||
3. **Идемпотентность zero_count** → пропускать `run`, если live уже `count=0` (применимо и к static). ✅
|
||||
4. **count=0** → канонический inverse; полное удаление ipSpace = отдельный опциональный `Mode:remove` (не подменять zero_count). ✅
|
||||
|
||||
## ИТОГ — финальная модель inverse
|
||||
|
||||
`delete_params: []{ Code, Mode, Value?, zero_fields?, off_value? }`
|
||||
- `Mode: static` — обратное значение = `Value` (boolean→"false"; string→off_value).
|
||||
- `Mode: zero_count` — взять live array-map-fixed, обнулить поля из `zero_fields:["count"]`.
|
||||
- `Mode: remove` (опц., не для FullPipe) — полное удаление элемента.
|
||||
|
||||
Рантайм: Delete → `GetInstanceStateParams` → построить inverse → если live уже целевое → no-op (skip run) → иначе `modify`.
|
||||
|
||||
Реестр (yaml-generator main.go):
|
||||
- `vc_org.ip_space`: `inverse`, param `vIPConfigure` `Mode:zero_count, zero_fields:[count]`.
|
||||
- `vc_nsxt.network`: `inverse`, params `needEnableAVI`=static `"false"`, `ipSpaceName`=static off_value `"no-needed"`.
|
||||
|
||||
Порядок destroy (уже корректен в .tf): `edge_net → org_ips → edge → vdc`.
|
||||
|
||||
## Факты (не менять, проверено)
|
||||
|
||||
- `count=0` принимается API (minvalue:1 в схеме — не отвергает), идемпотентно (ORG_IP_MODIFIER_TEST_2026-09-22).
|
||||
- SNAT off = `ipSpaceName="no-needed"` (sentinel в valueList, HAR_SNAT_MODIFY_FINDINGS).
|
||||
- `needEnableAVI` boolean → inverse `"false"`.
|
||||
- vc_org.ip_space: `error` → нужен `inverse` (zero_count). vc_nsxt.network: inverse+static `needEnableAVI=false`, добавить `ipSpaceName="no-needed"`.
|
||||
@@ -0,0 +1,264 @@
|
||||
# Ответ Opus 4.8: код-ревью и оценка архитектуры (3 задачи roadmap)
|
||||
|
||||
Дата: 2026-09-22 | Вопрос: `docs/prompt_for_opus_review.md` (коммит `7eab45e`)
|
||||
|
||||
Ответ получен строго по контексту промпта (файлы не открывались).
|
||||
|
||||
---
|
||||
|
||||
Задание — код-ревью по контексту (файлы открывать запрещено). Отвечаю сжато по пунктам.
|
||||
|
||||
## 1. Покрывает ли модель 3 задачи
|
||||
|
||||
| Задача | Вердикт | Причина |
|
||||
|---|---|---|
|
||||
| vcOrg modify (динамич. IP) | 🟡 | Покрыта, если аллокация = modify-параметр (кол-во/список). Проблема — тип массива (array-map-fixed = JSON-строка) и семантика «растёт по мере необходимости» |
|
||||
| vcNsxt modify (SNAT + IP из пула vcOrg) | 🔴 | Текущая модель НЕ покрывает: refSvc ссылается на инстанс/uid, а нужен конкретный элемент списка IP. Нужна новая связка (computed-атрибут + ссылка) |
|
||||
| k8sShturval create | 🟢 | Стандартный CRUD-create, покрыт. Риски — только long-create и типы параметров |
|
||||
|
||||
## 2. Как моделировать пул IP vcOrg
|
||||
|
||||
- **(б) отдельный ресурс/подресурс на IP** — ✗ противоречит архитектуре: «один сервис = один ресурс», подресурсы/data source не генерируются. Потребует ломки генератора.
|
||||
- **(а) атрибут-массив на nubes_vc_org** — ✅ согласуется. Но НЕ через array-map-fixed (JSON-строка: нет семантического diff, ломается порядок, нельзя адресовать элемент).
|
||||
- **Рекомендация:** декларативное желаемое состояние — либо `int64 desired_count` (modify), либо реальный `ListNestedAttribute`; аллоцированные IP отдавать как **типизированный computed** (list объектов), а не плоскую JSON-мапу. Императивная семантика «добавь ещё один» несовместима с декларативным Terraform.
|
||||
|
||||
## 3. Как vcNsxt получает IP из vcOrg
|
||||
|
||||
| Вариант | Оценка |
|
||||
|---|---|
|
||||
| (а) refSvc-параметр | 🔴 не может указать на конкретный элемент списка, только на uid инстанса |
|
||||
| (б) computed-атрибут vcOrg + ссылка `nubes_vc_org.x.<attr>` | 🟢 лучший фит. IP в nsxt — обычная string, пользователь берёт конкретный элемент |
|
||||
| (в) data source | ✗ не генерируется |
|
||||
|
||||
**Рекомендация — (б).** Условие: computed-вывод vcOrg должен быть **индексируемым типизированным** (`allocated_ips[0]`), не спрятанным в JSON-строке `state_out_flat` — иначе граф зависимостей Terraform не построит ребро и придётся ручной `depends_on`.
|
||||
|
||||
## 4. Риски текущего кода для этих потоков
|
||||
|
||||
- 🔴 **modify с массивами:** array-map-fixed = JSON-строка → нет семантического diff, смена порядка API даёт ложные modify, «дорастить пул» = полная замена списка (опасно, возможен ре-аллок).
|
||||
- 🟡 **create-only guard:** если параметры SNAT-IP / аллокации помечены не как `is_modifiable` → ModifyPlan заблокирует задачи 1 и 2. Проверить флаги в YAML.
|
||||
- 🟡 **read-back:** если API возвращает IP в нестабильном порядке → вечный drift на vcOrg.
|
||||
- 🟡 **порядок плана:** зависимость nsxt→vcOrg держится, только если ссылаемый IP — first-class computed (unknown на plan). Внутри JSON-строки Terraform зависимость не отследит → apply-time ошибки.
|
||||
|
||||
## 5. k8sShturval create — что критично
|
||||
|
||||
- refSvc к vdc/org резолвится (display name ИЛИ uid).
|
||||
- 🔴 long-create: create-op должен **ждать готовности** (poll ready-state), а не только «принято»; следить за context timeout.
|
||||
- типы параметров: map-fixed (nested) vs array-map-fixed (JSON), корректность required/default.
|
||||
- read-back state_params после create для computed-полей.
|
||||
|
||||
## 6. Итог — рекомендации по приоритету
|
||||
|
||||
1. 🔴 Добавить в vcOrg **типизированный индексируемый computed** для аллоцированных IP (list объектов, не JSON-мапа) — разблокирует задачу 2 через ссылку.
|
||||
2. 🔴 Моделировать аллокацию как **декларативное желаемое состояние** (int64 count или ListNested) с реконсиляцией в modify; параметры пометить `is_modifiable`.
|
||||
3. 🟡 Заменить array-map-fixed (JSON-строка) на **ListNestedAttribute** — ради адресуемости элементов, семантического diff и стабильного порядка.
|
||||
4. 🟡 Гарантировать граф зависимостей: ссылаемый IP — отдельный computed-атрибут, не поле JSON, чтобы не требовать `depends_on`.
|
||||
5. 🟢 k8sShturval: добавить ожидание готовности (poll) в long-create и проверить резолв refSvc.
|
||||
|
||||
**Ключевой вывод:** задачи 1 и 3 укладываются в текущую модель с доработками генератора; **задача 2 требует новой абстракции** — типизированного computed-вывода на vcOrg (вариант 3б), т.к. refSvc принципиально не адресует элемент списка.
|
||||
|
||||
---
|
||||
|
||||
## Раунд 2 — уточняющие вопросы и пересмотр
|
||||
|
||||
После проверки кода выяснилось, что промпт раунда 1 описывал модель неполно. Opus получил уточнённый контекст и 7 доп. вопросов.
|
||||
|
||||
### Уточнение контекста (передано Opus)
|
||||
|
||||
- Генератор уже поддерживает 4 вида ресурсов: instance (`nubes_<svc>`), subresource, action (только redeploy), modifier (`nubes_<svc>_<modifier>`).
|
||||
- Modifier-ресурсы для задач 1 и 2 УЖЕ СГЕНЕРИРОВАНЫ:
|
||||
- `nubes_vc_org_ip_space` (vc_org, modify 207): `vc_org_id` (required), `v_ip_configure` (required, StringAttribute = JSON-строка). Computed-выводов НЕТ.
|
||||
- `nubes_vc_nsxt_network` (vc_nsxt, modify 111): `vc_nsxt_id` (required), `need_enable_avi` (bool), `virtual_services_count` (int64), `ip_space_name` (string optional), `qos_profile` (string), `routed_net_configuration` (required, JSON-строка). Computed-выводов НЕТ.
|
||||
- Маппинг: `map-fixed` → SingleNestedAttribute; `array-map-fixed` → StringAttribute (JSON-строка).
|
||||
- Завершение операции — поллинг до `dtFinish`. Delete modifier — no-op.
|
||||
- Связка NSXT↔Org идёт **по имени ipSpace**: `vc_org.modify` задаёт `vIPConfigure[].name`, `vc_nsxt.modify` принимает `ipSpaceName` = это имя. Конкретный IP нигде не адресуется.
|
||||
|
||||
### Ответ Opus (пересмотр)
|
||||
|
||||
## 1. Верен ли прошлый вердикт про «новую абстракцию»
|
||||
|
||||
🟢 **Прошлый вывод отменяется.** Он строился на неполной модели. modifier-ресурсы уже существуют → задача 2 реализуема сегодня двумя модификаторами (`nubes_vc_org_ip_space` → `nubes_vc_nsxt_network`), связанными по имени ipSpace. Никакой новой абстракции и ломки генератора не требуется.
|
||||
|
||||
## 2. Минимальная доработка генератора для ссылки вместо строки
|
||||
|
||||
Добавить **один computed-вывод** на `nubes_vc_org_ip_space` — эхо `v_ip_configure[].name` (имя ipSpace). Тогда:
|
||||
```
|
||||
ip_space_name = nubes_vc_org_ip_space.x.ip_space_name
|
||||
```
|
||||
Это даёт ребро графа + защиту от опечатки. Адресовать IP не нужно — связка по имени. Опционально сверх минимума: валидация, что имя существует в org (refSvc-подобная проверка `ip_space_name` по vIPConfigure целевого org).
|
||||
|
||||
## 3. SNAT: свободная строка или computed обязателен
|
||||
|
||||
- Функционально **свободная строка + `depends_on` работает** (связка по имени, IP не адресуется). 🟡 Риски: опечатка, нет автоматического ребра графа без ручного depends_on, нет drift-детекта.
|
||||
- **Экспортировать имя (string), НЕ список IP.** Список IP не нужен — привязка идёт по имени ipSpace, не по конкретному адресу.
|
||||
- Вердикт: не строго обязателен для функции, но настоятельно рекомендован для корректности графа. Минимум — computed `ip_space_name` (string).
|
||||
|
||||
## 4. v_ip_configure как JSON-строка — реально ли бьёт
|
||||
|
||||
🟢 **В этом сценарии практически безвредно.** Значение фактически write-only (computed read-back нет), потребителю нужно только имя, которое пользователь сам задал. Поэлементное сравнение/адресация не требуются → прошлый 🔴 понижается.
|
||||
|
||||
Остаточный 🟡: спурьёзный diff при переформатировании JSON пользователем; если count растёт со временем — семантика «полная замена списка» без гранулярного diff может ре-триггерить modify/ре-аллокацию. Но без read-back и при связке по имени это не блокер.
|
||||
|
||||
## 5. Delete modifier = no-op
|
||||
|
||||
🟡 **No-op допустим** для association/one-shot модификатора, если обратного payload нет — это известный паттерн.
|
||||
|
||||
**Реальные последствия destroy:** Terraform удалит запись из state, но бэкенд НЕ освободит IP и НЕ отключит SNAT → утечка ресурсов + расхождение state↔бэкенд.
|
||||
|
||||
- Если бэкенд поддерживает обратную операцию (де-аллокация / disable SNAT) — no-op это реальный пробел 🔴, надо подключить reverse-op.
|
||||
- НЕДОСТАТОЧНО ДАННЫХ: есть ли у vc_org/vc_nsxt обратный payload. Если его действительно нет — no-op единственный вариант, но обязательно задокументировать поведение destroy.
|
||||
|
||||
## 6. Нужен ли ещё «int64 desired_count на инстансе»
|
||||
|
||||
🟢 **Нет, прошлый п.2 отменяется.** modifier — правильная единица для чередующейся зависимости (org аллоцирует → nsxt потребляет → org доаллоцирует). Счётчик на инстанс-ресурсе связал бы жизненный цикл пула с жизненным циклом инстанса и сломал бы взаимное чередование org↔nsxt. **Отдельный modifier строго лучше.**
|
||||
|
||||
## 7. k8sShturval create — что остаётся критичным
|
||||
|
||||
Помимо резолва refSvc (display name ИЛИ uid) и корректности required/default в map-fixed/array-map-fixed:
|
||||
- 🟡 **read-back computed после create** (kubeconfig/vault-выводы) — что реально возвращается в state.
|
||||
- 🟡 **обработка failure-состояния в поллинге**: dtFinish должен различать «готово успешно» и «завершилось с ошибкой», иначе провал маскируется под успех. НЕДОСТАТОЧНО ДАННЫХ по логике ошибки в poll-цикле.
|
||||
- 🟢 timeout-бюджет long-create vs интервал поллинга.
|
||||
|
||||
---
|
||||
|
||||
**Итог пересмотра:** обе задачи 1 и 2 **укладываются в существующий modifier-паттерн без новых абстракций**. Единственная стоящая доработка — добавить computed-экспорт `ip_space_name` на org-модификатор (п.2/3), чтобы заменить свободную строку ссылкой и построить граф зависимостей. Проблема JSON-строки в этом сценарии не критична (п.4). Главный остаточный риск — no-op delete и утечка IP/SNAT при destroy (п.5), требует проверки наличия обратного payload.
|
||||
|
||||
---
|
||||
|
||||
## Раунд 3 — дрейф сгенерированного кода и защитный механизм
|
||||
|
||||
Отдельная тема (не roadmap): расхождение `generated/{stand}/go` ↔ `provider/internal/resources_gen`, как предотвратить и сделать видимым.
|
||||
|
||||
### Контекст, переданный Opus
|
||||
|
||||
Go-провайдер Terraform. Ресурсы генерируются из YAML-спеков генератором.
|
||||
Цепочка: YAML → generated/{stand}/go/*.go → (копирование) → provider/internal/resources_gen/*.go → сборка бинаря.
|
||||
|
||||
Факты:
|
||||
- generated/ и provider/internal/resources_gen/ внесены в .gitignore → дрейф между ними НЕ виден в git status/diff.
|
||||
- Реальный дрейф уже случился: копия в provider/ устарела (сгенерирована до двух последних фиксов генератора).
|
||||
- Есть два скрипта сборки: (а) правильный — копирует свежий generated во временный каталог provider и собирает оттуда; (б) рисковый — собирает напрямую из provider/ (берёт потенциально устаревшую локальную копию).
|
||||
|
||||
Вопросы: 1) коммитить автоген или .gitignore+проверка; 2) два пути сборки — корень ли; 3) что предотвращает дрейф, а не детектит; 4) убрать вторую копию и генерировать при сборке; 5) как гарантированно «видно сразу при изменении кода».
|
||||
|
||||
### Ответ Opus
|
||||
|
||||
## 1. Коммитить сгенерированный код или .gitignore + проверка дрейфа
|
||||
|
||||
🟢 **Коммитить сгенерированный код** (для соло/малой команды это надёжнее). Обоснование:
|
||||
- Расхождение всплывает в `git diff`/PR-ревью — там, куда уже смотришь, а не в отдельной проверке, которую надо не забыть запустить.
|
||||
- CI-гейт `regenerate && git diff --exit-code` превращает дрейф в жёсткую ошибку сборки.
|
||||
- Минус — шум в диффах от больших автогенов, но это приемлемая цена за видимость.
|
||||
|
||||
.gitignore + внешняя проверка дрейфа 🟡 — работает, только если проверку невозможно обойти; сейчас её обходят вторым скриптом. Скрытый код = скрытый дрейф (уже случилось).
|
||||
|
||||
## 2. Два пути сборки — корень проблемы?
|
||||
|
||||
🔴 **Да, это корень.** Проблема не в дрейфе как таковом, а в том, что **существует путь, собирающий из потенциально устаревшей копии**. Пока рисковый скрипт (б) доступен, дрейф рано или поздно попадёт в бинарь.
|
||||
|
||||
**Оставить ровно один канонический путь.** Скрипт (б) удалить или сделать так, чтобы он физически не мог собрать из несвежей копии (см. п.3–4).
|
||||
|
||||
## 3. Что ПРЕДОТВРАЩАЕТ дрейф, а не только детектит
|
||||
|
||||
| Механизм | Роль |
|
||||
|---|---|
|
||||
| Жёсткий fail на старте сборки | 🟡 **детект** — не даёт собрать плохой бинарь, но дрейф уже есть |
|
||||
| Авто-синхронизация/генерация перед сборкой | 🟢 **предотвращение** — копия физически не может быть устаревшей |
|
||||
| git pre-commit hook | 🟡 **детект** на этапе коммита |
|
||||
|
||||
**Предотвращает только генерация/синк как обязательная часть сборки** (копия всегда свежая). Fail и hook — детекторы. Рекомендуемая комбинация: **генерация-при-сборке (предотвращение) + CI `git diff --exit-code` после регенерации (страховка от обхода)**.
|
||||
|
||||
## 4. Более сильный паттерн: убрать вторую копию, генерировать при сборке
|
||||
|
||||
🟢 **Да, это сильнейший вариант.** Нет постоянной копии → нечему рассинхронизироваться. Единственный источник истины = YAML + генератор (`go generate` / шаг Makefile перед `go build`).
|
||||
|
||||
Минусы:
|
||||
- 🟡 Сборка зависит от наличия и детерминированности генератора (версию генератора надо пинить).
|
||||
- 🟡 Медленнее сборка.
|
||||
- 🔴 Провайдер перестаёт быть чистым Go-модулем: нельзя `go get`/собрать без тулчейна генератора; воспроизводимость завязана на версию генератора.
|
||||
- Сложнее просто «посмотреть» сгенерированный код без сборки.
|
||||
- НЕДОСТАТОЧНО ДАННЫХ: детерминирован ли генератор (стабильный порядок полей/файлов) — без этого diff-гейт будет шуметь.
|
||||
|
||||
**Компромисс:** генерировать при сборке И коммитить результат (генерация переписывает закоммиченную копию, CI ловит расхождение) — совмещает «невозможно устареть» с «видно в диффе».
|
||||
|
||||
## 5. «Видно сразу при изменении кода, не закопано в доках»
|
||||
|
||||
Гарантированно это даёт **только закоммиченный сгенерированный код**: расхождение появляется в `git diff` того же PR, где меняется генератор/YAML. Усилить:
|
||||
- **pre-commit hook** `regenerate` → расхождение всплывает до коммита;
|
||||
- **CI-гейт** `regenerate && git diff --exit-code` → нельзя смёржить с дрейфом.
|
||||
|
||||
Вариант «генерировать при сборке без копии» даёт *другую* гарантию — «устареть невозможно», но там **нет диффа для обзора**. Если требование именно «видно в изменениях кода» — нужен закоммиченный автоген.
|
||||
|
||||
---
|
||||
|
||||
**Итог — рекомендация по приоритету:**
|
||||
1. 🔴 Убрать рисковый путь сборки (б); оставить один канонический.
|
||||
2. 🟢 Сделать генерацию/синк обязательным шагом канонической сборки (предотвращение).
|
||||
3. 🟢 Закоммитить сгенерированный код + CI-гейт `regenerate && git diff --exit-code` (видимость + защита от обхода).
|
||||
4. 🟡 Запинить версию генератора и обеспечить детерминированный вывод, иначе гейт будет ложно срабатывать.
|
||||
|
||||
---
|
||||
|
||||
## Раунд 4 — универсальность и реестр исключений
|
||||
|
||||
Тема: принцип «универсально по умолчанию, исключения — только если API не ложится», и как кодировать исключения, чтобы они были однозначно понимаемы при каждой генерации.
|
||||
|
||||
### Контекст, переданный Opus (кратко)
|
||||
|
||||
Пайплайн: YAML-спека сервиса (из API) → универсальный генератор → Go-ресурсы → сборка. Код/скрипты/логика генерации — одни для всех стендов и сервисов; различается только YAML. Механизм modifier-ресурсов уже универсален (дженерик читает `kind: modifier` + имя). Не универсально сейчас: хардкод в yaml-generator (`svc.ID == 19 → ip_space`, `svc.ID == 22 → network`) и в docs-generator (`ServiceID == 90`, спец-вывод postgres). Отдельно был инцидент дрейфа git-ignored сгенерированного кода (фон).
|
||||
|
||||
Выработанный принцип (просили оценить): «Универсально по умолчанию. Исключение — только если API реально не ложится. Каждое исключение — одна явная таблица/структура, которую КОД ЧИТАЕТ, с полями ЧТО/ЧТО ДЕЛАЕТ/ПОЧЕМУ/ГДЕ. Канонический список — в ARCHITECTURE-доке, код ссылается. Изменение = изменение таблицы → видно в диффе.»
|
||||
|
||||
### Ответ Opus
|
||||
|
||||
## 1. Оценка принципа
|
||||
|
||||
🟢 **Принцип корректен долгосрочно** — «convention over configuration + escape hatch».
|
||||
|
||||
Подводные камни:
|
||||
- 🟡 «API не ложится» субъективно → нужен объективный тест-триггер, иначе exception creep.
|
||||
- 🔴 Исключения не возвращаются в ядро: когда паттерн повторился 2–3 раза, нужен ритуал «промоушена» в ядро.
|
||||
- 🟡 Обратный перекос: обобщать реально одноразовый случай — раздувает ядро.
|
||||
|
||||
## 2. Как кодировать исключения
|
||||
|
||||
| Вариант | Видимость в diff | Нельзя «проспать» | Поддержка | Рассинхрон с YAML |
|
||||
|---|---|---|---|---|
|
||||
| (а) именованная таблица в коде, код её читает | 🟢 | 🟢 (если итерирует и падает на неучтённом) | 🟡 нужна пересборка | 🟢 низкий |
|
||||
| (б) отдельный yaml-конфиг | 🟢 | 🟡 легко забыть подключить | 🟢 без пересборки | 🟡 средний |
|
||||
| (в) аннотации в YAML-спеке | 🔴 | 🔴 | 🔴 | 🔴 фатально: YAML регенерится → аннотации затираются |
|
||||
|
||||
**(в) отклонить.** **Рекомендация: (а)** — именованная структура, которую код итерирует и ассертит.
|
||||
|
||||
## 3. Граница «логика» vs «данные»
|
||||
|
||||
- В ядре — механизм/алгоритм (как модификатор генерится, маппинг схемы). Никогда не per-service.
|
||||
- В реестре — чистые данные («сервис X → имя модификатора Y», «сервис 90 → набор полей Z»).
|
||||
|
||||
Признаки: 1) `if id == N`, меняющий поток исполнения → извлечь данные; 2) убрать пункт → меняются только значения, не поведение → данные; 3) copy-paste кода → механизм (обобщать), разные строки таблицы → данные.
|
||||
|
||||
## 4. Паттерн «override registry» в кодогенераторах
|
||||
|
||||
- tfplugingen-openapi: `generator_config.yml` отдельно от спеки + IR (`terraform-plugin-codegen-spec`).
|
||||
- OpenAPI Generator: vendor extensions `x-*` + template-оверрайды + config-json.
|
||||
- protoc-плагины: custom options (напр. `google.api.http`).
|
||||
|
||||
Что перенять: 1) отдельный версионируемый конфиг оверрайдов; 2) IR-слой; 3) fail на неучтённом; 4) стабильное символьное имя, не сырой ID.
|
||||
|
||||
## 5. Не противоречит ли реестр «YAML — единственный источник»
|
||||
|
||||
Не противоречит — при разделении двух доменов истины:
|
||||
- **API-YAML** = истина про «что есть сервис» (машинно-владеемый, регенерится).
|
||||
- **Реестр оверрайдов** = истина про «наши провайдер-специфичные решения» (человеко-владеемый).
|
||||
|
||||
Теневой источник — только если ОДИН факт лежит в обоих. 🔴 Нельзя аннотировать API-YAML. Конвейер: `API-YAML + overrides → merged IR → codegen`.
|
||||
|
||||
## 6. Риски «таблица + раздел в ARCHITECTURE.md»
|
||||
|
||||
- 🔴 ARCHITECTURE.md дрейфует от таблицы → возврат к `if id==19`, если кто-то добавит ветку в обход.
|
||||
- 🔴 Числовые ID (19/22/90) непрозрачны.
|
||||
|
||||
Как закрыть: 1) 🔴 единая точка маршрутизации + CI-lint/grep-гейт против `svc.ID ==` вне реестра; 2) раздел ARCHITECTURE генерировать ИЗ реестра (golden-test); 3) 4 поля — поля структуры, а не комментарии; 4) стабильные символьные ключи; 5) fail-fast: генератор падает, если спец-обработка без записи в реестре.
|
||||
|
||||
---
|
||||
|
||||
**Итог:** сильнейшая реализация — отдельный человеко-владеемый override-реестр (данные, не логика), стабильные ключи, IR-слой применения, CI-гейт против хардкодов вне реестра, раздел ARCHITECTURE генерируется из реестра. Это устраняет и `if id==N`, и дрейф доки.
|
||||
@@ -0,0 +1,89 @@
|
||||
# Резюме сессии: Диагностика K8s (Штурвал), VDC и архитектура провайдера (2026-09-20)
|
||||
|
||||
## 1. Инфраструктурный контекст
|
||||
- **ВМ 213 (jump-host / dev)**:
|
||||
- SSH: `ssh vps` (`5.172.178.213`, user `naeel`, key `~/.ssh/naeel_vm_id_ed25519`).
|
||||
- kubectl контекст по умолчанию: `tazetdinovn@gmail.com@naeel-test-3`.
|
||||
- **Кластер `naeel-test-3`**:
|
||||
- API: `https://185.247.187.146:6443`.
|
||||
- Узлы: 6 нод (3 control-plane, 3 workers), версия v1.34.1 / платформа Штурвал 2.12.1.
|
||||
- **Кластер `devclustername`**:
|
||||
- API: `https://185.247.187.226:6443`.
|
||||
- Статус: порт 6443 на Edge доступен, но TLS сбрасывается (`connection reset by peer`) — виртуальные машины кластера находятся в `suspend` / выключены.
|
||||
|
||||
---
|
||||
|
||||
## 2. Что было сделано и починено в кластере `naeel-test-3`
|
||||
|
||||
### Проблема:
|
||||
В веб-интерфейсе Штурвала кластер висел в статусе **«Работает с ошибками» (⚠️ 2/4 по NodeConfigItems)**.
|
||||
|
||||
### Причина:
|
||||
1. На активном воркере `naeel-test-3-workers-5p8w7-vxzch` висело ожидание применения конфигураций (`RebootPending` с типом `drainonly`).
|
||||
2. Очередь на применение/drain была заблокирована (`SlotsOccupied`), так как в CRD `nodeconfigs.node.shturval.tech` осталась старая удалённая нода `naeel-test-3-workers-5p8w7-nqkr2` с зависшим флагом `rebootallowed: true`.
|
||||
|
||||
### Решение:
|
||||
1. Вычищены все фантомные объекты `nodeconfigs`, которых уже нет среди реальных K8s-нод (сняты блокирующие finalizers).
|
||||
2. Слот освободился: контроллер `shturval-node-config` корректно выполнил `drainonly` на воркере `vxzch` (под `pythonk8s` переехал на соседний воркер `wwqj2`).
|
||||
3. Применились оставшиеся `NodeConfigItem` (`generic-init-config`, `all-to-nubes-registry`).
|
||||
4. **Текущий статус**:
|
||||
- Все 6 нод в статусе `Ready`.
|
||||
- Все 6 `nodeconfigs` в статусе `READY: true`.
|
||||
- Все 4 `nodeconfigitems` в статусе `ready: true` (**4/4, статус кластера зелёный**).
|
||||
- Под `pythonk8s` (`drhider.pythonk8s.dev.nubes.ru`, ns `20a75175-a58c-49cb-b8fa-e86367b1a8dc`) поднят и работает (1/1 Running).
|
||||
- Потребление ресурсов: среднее ~22m CPU и ~103 MiB RAM на под; суммарно на весь 6-узловой кластер ~1.6 CPU и ~7.5 GiB RAM (минимальный фоновый простой).
|
||||
|
||||
---
|
||||
|
||||
## 3. Блокировка операций в личном кабинете облака (Suspend / Modify)
|
||||
|
||||
### Симптом:
|
||||
При попытке выполнить операцию `suspend` кластера в UI облака возникает ошибка:
|
||||
`Concurrent operations are not supported (job status: cannot obtain job result) There is a started operation on this instance`
|
||||
|
||||
### Диагностика через API Gateway (`lk-api-gateway-dev.ngcloud.ru`):
|
||||
- Инстанс кластера: `e78a40b9-7de1-4c88-af7a-ad7f7efaa7c2`.
|
||||
- Зависшая операция: `modify` (`opUid: 2fb7dd59-732a-4bb9-add2-6bddd7329f72`).
|
||||
- Причина зависания: оркестратор CFS поймал таймаут (`Timeout has been exceeded`), выполнил откат, записал лог ошибки, но **не проставил `dtFinish` и `isSuccessful` в БД**.
|
||||
- Статус операции в API остался незакрытым, из-за чего API Gateway блокирует любые новые операции над инстансом.
|
||||
- **Внимание**: это баг бэкенда платформы облака (CFS/оркестратора). Средствами `kubectl` внутри кластера это не лечится — требуется сброс статуса операции на стороне API платформы или через поддержку.
|
||||
- Ошибка в истории операций: `Не удалось включить кластер. Ошибка: Error not found from 'parseTerraformError'` — вызвана тем, что общий обработчик бэкенда облака по ошибке прогнал текст через парсер ошибок Terraform. Внутри K8s Terraform не используется.
|
||||
|
||||
---
|
||||
|
||||
## 4. Архитектурные правила по VDC и Terraform-провайдеру
|
||||
|
||||
### Почему возникает ошибка дубликата VDC:
|
||||
`VDC с именем 'WZ03709-saas-snb1-i24-vcpu50' уже существует внутри организации 'WZ03709-saas'`
|
||||
- Имя VDC генерируется бэкендом детерминированно: `{org}-{type}-{segment}-{cpu}-vcpu{reservation}`.
|
||||
- В одном сегменте организации существует максимум два класса VDC:
|
||||
- `vcpu50` (50% гарантия vCPU — для dev/test, оверселлинг, дешевле);
|
||||
- `vcpu80` (80% гарантия vCPU — для prod/баз данных, жесткая фиксация ресурсов).
|
||||
- Создавать третий VDC с тем же процентом SLA в рамках одной организации невозможно (конфликт уникальности имен в vCloud) и бессмысленно: изоляция проектов внутри VDC делается через **vApp**, **сети (Org Networks)** и правила фаервола. При нехватке ресурсов VDC масштабируется через `modify`.
|
||||
- **Канон Terraform**: при работе с уже созданными VDC в манифесте указывать:
|
||||
```hcl
|
||||
adopt_existing_on_create = true
|
||||
```
|
||||
(согласно `docs/60_strategy/provider_philosophy.md`).
|
||||
|
||||
### Зачем нужны дочерние ресурсы-действия (Action / Sub-resources):
|
||||
Цепочка развертывания vCloud/NSX-T имеет циклические зависимости:
|
||||
1. `vcOrg -> create`
|
||||
2. `vcVdc -> create`
|
||||
3. `vcNsxt -> create` (Edge Gateway)
|
||||
4. `vcOrg -> modify` (выделение белых IP в организацию, так как появился Edge)
|
||||
5. `vcNsxt -> modify` (настройка SNAT под конкретный выделенный IP)
|
||||
6. `k8sShturval -> create` (нодам нужен интернет через SNAT)
|
||||
|
||||
Чтобы пользователю не приходилось делать несколько ручных прогонов `terraform apply` с ручным редактированием `.tf` файлов между шагами, в провайдере создаются **дочерние ресурсы-действия**.
|
||||
- Под капотом провайдера эти дочерние ресурсы транслируются в точечные вызовы **`modify`** над родительскими объектами.
|
||||
- Это стандартная практика в Terraform (аналог `aws_security_group_rule` для `aws_security_group`).
|
||||
|
||||
---
|
||||
|
||||
## 5. Что делать дальше в новом чате
|
||||
1. Если продолжаем работу с провайдером Nubes:
|
||||
- Проверить статус зависшей операции `2fb7dd59-732a-4bb9-add2-6bddd7329f72` в API Gateway.
|
||||
- Разрабатывать/тестировать логику дочерних ресурсов (action/sub-resources) и `modify`-пайплайна по стандарту `docs/60_strategy/provider_philosophy.md`.
|
||||
2. Если требуется проверить второй кластер (`devclustername` / `185.247.187.226`):
|
||||
- Убедиться, что кластер выведен из suspend в веб-интерфейсе (чтобы поднялся API server на порту 6443).
|
||||
@@ -0,0 +1,155 @@
|
||||
# Резюме сессии: VDC create-flow, диалог с Opus и правка fallback (2026-09-21)
|
||||
|
||||
## 1. Контекст задачи
|
||||
- Цель: довести до рабочего состояния создание `nubes_vc_vdc` в `FullPipe`.
|
||||
- Симптом: при создании VDC провайдер падал на `GET /instanceOperations/{opUid}?fields=cfsParams` с HTTP 500.
|
||||
- В ходе разбора было подтверждено, что обычные сервисы через тот же провайдер работали, а VDC попадал в отдельную проблемную ветку backend-обработки.
|
||||
|
||||
---
|
||||
|
||||
## 2. Диагностика проблемы
|
||||
|
||||
### 2.1. Что ломалось
|
||||
- В `provider/internal/core/client.go` create-flow делал `GET /instanceOperations/{opUid}?fields=cfsParams` сразу после создания операции.
|
||||
- Для VDC этот запрос приводил к ошибке backend’а:
|
||||
- `Invalid call of the function [getResourceRealmConfig]`
|
||||
- `Cannot cast Object type [Struct] to a value of type [string]`
|
||||
- источник ошибки: `/app/api/v1/resources/instance_operation_cfs_param.cfc`
|
||||
- Причина по отчету: для `vc_vdc` вычислялся динамический `descr` у `storageConfig.name`, и backend падал на `resourceRealm`, который в DEV хранится как `Struct`, а не `string`.
|
||||
|
||||
### 2.2. Почему обычные сервисы не ломались
|
||||
- На обычных сервисах этот `GET` либо не попадал в проблемный backend-код, либо не требовал вычисления `resourceRealm`.
|
||||
- Для VDC в YAML есть специфическая зависимость:
|
||||
- `generated/dev/resources_yaml/21_vc_vdc.yaml`
|
||||
- `storageConfig.name` содержит вычисляемый `descr` с `getResourceRealmConfig(...resourceRealm...)`.
|
||||
- Для обычных сервисов, например `vapp` и `postgres`, такого вычисляемого `resourceRealm`-контекста нет.
|
||||
|
||||
### 2.3. Почему `hasUnresolvedParams` мешал
|
||||
- Эвристика проверяла **все** строковые параметры, а не только параметры с `ref_svc_id`.
|
||||
- Для VDC это ломало fallback на обычных литералах вроде:
|
||||
- `providerVdc = fast-2.8`
|
||||
- `networkProvider = default`
|
||||
- Эти значения не UUID и не JSON, но и резолвить их не нужно.
|
||||
- В результате при падении GET провайдер вместо продолжения переходил в ошибку.
|
||||
|
||||
---
|
||||
|
||||
## 3. Диалог с Opus
|
||||
|
||||
### 3.1. Что просили у Opus
|
||||
- Проверить только:
|
||||
- `provider/internal/core/client.go`
|
||||
- `NOTES/30_analysis/DEBUG_REPORT_VC_VDC_500.md`
|
||||
- `generated/dev/resources_yaml/21_vc_vdc.yaml`
|
||||
- `generated/dev/resources_yaml/26_vapp.yaml`
|
||||
- `generated/dev/resources_yaml/90_postgres.yaml`
|
||||
- Вопросы к Opus были узкими:
|
||||
1. почему обычные сервисы работали, а VDC начал падать на `GET ?fields=cfsParams`
|
||||
2. есть ли в VDC специфическая структура или зависимость, которой нет у обычных сервисов
|
||||
3. является ли `hasUnresolvedParams` неверной эвристикой именно в этом месте
|
||||
4. что именно надо исправить
|
||||
|
||||
### 3.2. Что ответил Opus по сути
|
||||
- Root cause — не данные Terraform и не сами строки `fast-2.8` / `default`, а backend-ошибка на `GET /instanceOperations/{opUid}?fields=cfsParams` именно для VDC.
|
||||
- VDC отличается от обычных сервисов тем, что в его YAML есть динамический `descr` для `storageConfig.name`, который тянет `resourceRealm`.
|
||||
- `hasUnresolvedParams` была признана лишней и хрупкой эвристикой: она может ломать fallback на обычных строках.
|
||||
- Итоговое решение Opus: при ошибке GET идти дальше по браузерному flow, без условий по всем строковым параметрам.
|
||||
|
||||
### 3.3. Дополнительные уточнения в диалоге
|
||||
- Был отдельный спор по формулировке про Lucee / ColdFusion backend.
|
||||
- В итоге было зафиксировано, что этот термин — не отдельная гипотеза, а просто обозначение backend-слоя, который уже фигурировал в отчетах и traceback’ах.
|
||||
- Opus также подтвердил, что для VDC этот GET нужен только как вспомогательный шаг для `resolveRefSvcParamValues`, а не как обязательный бизнес-этап.
|
||||
|
||||
---
|
||||
|
||||
## 4. Что изменили в коде
|
||||
|
||||
### 4.1. `provider/internal/core/client.go`
|
||||
- В `CreateGenericInstanceUniversalV6` удалён gate по `hasUnresolvedParams`.
|
||||
- Теперь логика такая:
|
||||
- если `GET /instanceOperations/{opUid}?fields=cfsParams` успешен — парсим и резолвим `ref_svc_id`
|
||||
- если GET падает — просто продолжаем POST’ить параметры, а потом идём в `validate-cfs` и `run`
|
||||
- Функция `hasUnresolvedParams` удалена полностью.
|
||||
- После удаления была убрана осиротевшая документационная строка, оставшаяся над `isHexDigit`.
|
||||
|
||||
### 4.2. `provider/internal/core/client_test.go`
|
||||
- Добавлен тест:
|
||||
- `TestCreateGenericInstanceUniversalV6_ContinuesWhenOpDetailsGETFails`
|
||||
- Тест моделирует:
|
||||
- `POST /instances`
|
||||
- `POST /instanceOperations`
|
||||
- `GET /instanceOperations/{opUid}?fields=cfsParams` → 500
|
||||
- `POST /instanceOperationCfsParams`
|
||||
- `GET /instanceOperations/{opUid}/validate-cfs`
|
||||
- `POST /instanceOperations/{opUid}/run`
|
||||
- финальный `GET /instances/{uid}`
|
||||
- Проверка теста:
|
||||
- create-flow завершился успешно
|
||||
- все 7 параметров были отправлены с ожидаемыми значениями
|
||||
- polling по операции был ровно один раз
|
||||
|
||||
---
|
||||
|
||||
## 5. Проверка после правки
|
||||
- `cd /home/naeel/TF/tf_provider/provider && go test ./internal/core` — успешно.
|
||||
- После ревью был пойман и исправлен только косметический хвост:
|
||||
- старый комментарий над `isHexDigit`, оставшийся после удаления `hasUnresolvedParams`.
|
||||
- После этого пакет `internal/core` снова прошёл тесты.
|
||||
|
||||
---
|
||||
|
||||
## 6. Вывод по итогам сессии
|
||||
- Проблема была не в обычных сервисах как таковых, а в специфике VDC-данных и backend-пути, который срабатывал на `GET ?fields=cfsParams`.
|
||||
- `hasUnresolvedParams` была неверной эвристикой именно в create-flow VDC и ломала рабочий fallback.
|
||||
- Правильное поведение: если GET падает, не гадать по строковым параметрам, а продолжать browser-like flow через POST параметров, validate и run.
|
||||
|
||||
---
|
||||
|
||||
## 7. Что дальше
|
||||
- Следующий этап — уже не правка логики, а публикация и стендовая проверка при необходимости.
|
||||
- DEV-релиз `2.0.3` успешно собран и загружен в registry `nubes-dev` через `TOOLS/scripts/03_build_and_upload_provider.sh --profile TOOLS/config/dev`.
|
||||
- Перед этим уже был подготовлен короткий запрос на ревью для Opus и получен ответ, который подтвердил направление правки.
|
||||
|
||||
---
|
||||
|
||||
## 8. Отдельный диалог про `organization_uid`, refSvcId и универсальное поведение
|
||||
|
||||
### 8.1. Что стало проблемой
|
||||
- В `vc_vdc` поле `organization_uid` можно передавать как display name (`kontora`), так и как UUID организации.
|
||||
- В коде `provider/internal/resources_gen/21_vc_vdc_resource.go` это поле сейчас резолвится через `ResolveRefSvcParamValue(...)` в UUID.
|
||||
- После `apply` Terraform видит расхождение: в конфиге было имя, в state оказался UUID, и появляется ошибка `Provider produced inconsistent result after apply`.
|
||||
- Параллельно в этом же ресурсе остаются ручные `EqualFold`-хаки, которые пытаются сохранить старое значение, но не решают кейс "имя vs UUID".
|
||||
|
||||
### 8.2. Почему это сравнивали с S3
|
||||
- Для `nubes_s3_bucket` похожее поведение уже работает: ref-поле `s3_user_uid` проходит через общий механизм refSvc-резолва и state-refresh.
|
||||
- В S3 есть симметричный путь: UUID можно принимать на вход, а состояние при чтении синхронизируется через общий mapping-слой.
|
||||
- Поэтому S3 не падает на inconsistency, а VDC падает из-за локальных restore-хаков и разного поведения на create/read/update.
|
||||
|
||||
### 8.3. Что выяснили по коду
|
||||
- Ключевой участок VDC:
|
||||
- [provider/internal/resources_gen/21_vc_vdc_resource.go](provider/internal/resources_gen/21_vc_vdc_resource.go#L197-L202)
|
||||
- [provider/internal/resources_gen/21_vc_vdc_resource.go](provider/internal/resources_gen/21_vc_vdc_resource.go#L323-L338)
|
||||
- [provider/internal/resources_gen/21_vc_vdc_resource.go](provider/internal/resources_gen/21_vc_vdc_resource.go#L388-L403)
|
||||
- [provider/internal/resources_gen/21_vc_vdc_resource.go](provider/internal/resources_gen/21_vc_vdc_resource.go#L511-L525)
|
||||
- В S3 аналогичный слой устроен аккуратнее:
|
||||
- [provider/internal/resources_gen/13_s3bucket_resource.go](provider/internal/resources_gen/13_s3bucket_resource.go#L149-L159)
|
||||
- [provider/internal/resources_core/state_refresh.go](provider/internal/resources_core/state_refresh.go#L82-L96)
|
||||
- [provider/internal/resources_core/params_ref_mapping.go](provider/internal/resources_core/params_ref_mapping.go#L124-L147)
|
||||
|
||||
### 8.4. Что решил сделать дальше
|
||||
- Пользователю нужен не частный фикс только для VDC, а универсальная схема для всех refSvcId-полей.
|
||||
- Была сформулирована задача для Opus: определить, какой канон выбрать для state, где делать name→UUID и UUID→display_name, и как убрать ручные `EqualFold`-хаки без поломки S3 и других уже рабочих ресурсов.
|
||||
- Отдельно зафиксировано требование: ответ Opus нужен короткий, но сам вопрос должен быть подробным и однозначным.
|
||||
|
||||
### 8.5. Важный вывод на сейчас
|
||||
- Универсальное решение пока не внедрено.
|
||||
- Текущий безопасный путь — сначала получить короткий архитектурный ответ от Opus, а уже потом править генератор и пересобирать ресурсы.
|
||||
|
||||
---
|
||||
|
||||
## 9. Детерминированная пересборка генератора
|
||||
|
||||
- После отдельного разбора `kind: modifier` выяснилось, что падение генерации было эксплуатационным: запускался устаревший бинарник `resource-generator`, а не текущие исходники.
|
||||
- В `TOOLS/scripts/02_generate_resources_and_docs_v2.sh` убран `mtime`-гард через `find ... -newer`; генераторы теперь всегда собираются заново перед прогоном.
|
||||
- Это сделано специально, чтобы старый бинарник больше не мог скрыть поддержку новых `kind`-веток в YAML-спеках.
|
||||
- Дополнительно `TOOLS/resource-generator/bin/` добавлен в ignore, чтобы локальный stale-артефакт не путал следующий запуск.
|
||||
@@ -0,0 +1,194 @@
|
||||
# Передача контекста: Terraform-провайдер Nubes
|
||||
|
||||
Дата: 2026-09-22 | Версия DEV: 2.0.8 | HEAD: 13beb9c (`release(dev): 2.0.8`)
|
||||
|
||||
Документ для старта новой сессии. Прочитать целиком перед любыми действиями.
|
||||
|
||||
---
|
||||
|
||||
## 1. Правила работы (соблюдать строго)
|
||||
|
||||
- **Никаких действий без прямого разрешения.** Правки, сборки, заливки, коммиты, запуск
|
||||
terraform, запросы в API — только по явной команде оператора.
|
||||
- **Вопрос в любой форме = только ответ.** Не выполнять действий, не предлагать «а ещё могу».
|
||||
- **Коммитить после каждой правки**, разбивая по смыслу. Не копить в рабочем дереве.
|
||||
- **Не расширять область работ.** Формулировка «сделай актуальным везде» не даёт права
|
||||
на дополнительные шаги.
|
||||
- **Не догадываться.** Не уверен — сказать прямо и спросить. Причину бага доказывать
|
||||
фактами (логи, трассировки, содержимое файлов), а не гипотезами.
|
||||
- Язык ответов — русский.
|
||||
|
||||
---
|
||||
|
||||
## 2. Проект
|
||||
|
||||
- Репозиторий: `/home/naeel/TF/tf_provider`
|
||||
- Провайдер: `terraform-provider-nubes`, Go, `terraform-plugin-framework v1.8.0`
|
||||
- **Go-модуль провайдера лежит в `provider/`** (не в корне). `go build ./...` из корня
|
||||
падает с «directory prefix . does not contain main module».
|
||||
- Генераторы в `TOOLS/`:
|
||||
- `yaml-generator` — из API в YAML-спеки;
|
||||
- `resource-generator` — из YAML в Go (шаблоны `text/template` в
|
||||
`TOOLS/resource-generator/internal/templates/`);
|
||||
- `docs-generator` — из YAML в Markdown.
|
||||
- Пайплайн: `TOOLS/scripts/01_generate_yamls.sh` → `02_generate_resources_and_docs_v2.sh`
|
||||
→ `03_build_and_upload_provider.sh` (скрипт `03` сам прогоняет `01` и `02`).
|
||||
|
||||
---
|
||||
|
||||
## 3. Состояние на 2026-09-21 (конец сессии)
|
||||
|
||||
- Ветка `master`, **рабочее дерево чистое**, HEAD = `13beb9c` (`release(dev): 2.0.8`).
|
||||
- Локальные коммиты **в origin не пушились**.
|
||||
- **DEV-версия: 2.0.8**, залита в
|
||||
`tf-registry.containerk8s.services.ngcloud.ru/nubes-dev/nubes`,
|
||||
подпись GPG `CB3A0DF161ECC416` (`tazet@narod.ru`, ключ `secrets/private_key.asc`).
|
||||
- S3: `prod-s3/nubes-terraform-registry/...`, endpoint `https://s3.msk-1.ngcloud.ru`.
|
||||
- Namespace: `nubes-dev`, провайдер `nubes`.
|
||||
- Terraform v1.9.5 локально.
|
||||
|
||||
---
|
||||
|
||||
## 4. Что починено в этой сессии
|
||||
|
||||
| Коммит | Что |
|
||||
|---|---|
|
||||
| `7ecd2aa` | детерминированная пересборка генераторов (удалён устаревший бинарь) |
|
||||
| `2286d34` | destroy-guard в `ModifyPlan` + универсальный refSvc (имя или UUID) |
|
||||
| `1401003` | release 2.0.4 |
|
||||
| `d608fba` | FullPipe: `edge.tf`, `storage_config` fast→SATA |
|
||||
| `92e04da` | TODO-документ по багу docs-generator |
|
||||
| `bffe3d9` | refSvc-поля без `Computed` (unset = null, а не unknown) |
|
||||
| `61c7e20` | HISTORY сессии |
|
||||
| `54b0baa` | release 2.0.5 |
|
||||
| `af2e10b` | docs-generator: `map-fixed` → `= { ... }` (аргумент, не блок) |
|
||||
| `7c2cc67` | docs-generator: `array-map-fixed` → `jsonencode([...])` |
|
||||
| `3ca0752` | docs-generator: строковые дефолты в кавычках |
|
||||
| `8d405ba` | core: lifecycle-aware подсказки + `supportsSuspend` в сигнатурах |
|
||||
| `c6715e8` | генератор: `supportsSuspend` в diagnostics; нет `suspend_on_destroy` без suspend |
|
||||
| `69808bd` | release 2.0.6 |
|
||||
| `67c4d2f` | `TOOLS/scripts/validate_docs_examples.sh` |
|
||||
| `14ada09` | ТЗ для Flash по tainted-replace |
|
||||
| `724f5f7` | **убрана create-time проверка существования из `ModifyPlan`** (ломал tainted-replace и `destroy`) |
|
||||
| `25080b7` | release 2.0.7 |
|
||||
| `3c0157a` | **`ShouldBeOptionalComputed`**: read-back параметры без Default → `Optional+Computed` |
|
||||
| `4b34cc7` | **core: гарантия known** — `unknown → null` для read-back полей |
|
||||
| `13beb9c` | release 2.0.8 |
|
||||
|
||||
---
|
||||
|
||||
## 5. Ключевые архитектурные факты (не переоткрывать заново)
|
||||
|
||||
1. **Две копии сгенерированного кода.**
|
||||
- `provider/internal/resources_gen/` — в `.gitignore`, локальный артефакт;
|
||||
- `generated/dev/go/` — актуальный вывод генератора; именно его компилирует релиз
|
||||
(скрипт `03` копирует его в temp-копию `provider`).
|
||||
Проверять надо **`generated/dev/go`**, не `resources_gen`. Проверка не той копии
|
||||
уже приводила к ложным выводам.
|
||||
|
||||
2. **Рецепт проверки сборки без релиза:**
|
||||
```bash
|
||||
TMP=$(mktemp -d) && cp -R provider "$TMP/provider" && \
|
||||
find "$TMP/provider/internal/resources_gen" -maxdepth 1 -type f -name '*.go' -delete && \
|
||||
cp generated/dev/go/*.go "$TMP/provider/internal/resources_gen/" && \
|
||||
(cd "$TMP/provider" && go build ./...) && echo BUILD_OK && rm -rf "$TMP"
|
||||
```
|
||||
|
||||
3. **Версия правится в 3 файлах:** `TOOLS/config/dev/profile.env` (`VERSION`),
|
||||
`DEV_STAND/FullPipe/versions.tf`, `VERSIONS.md`.
|
||||
Затем коммит `release(dev): X.Y.Z` и
|
||||
`./TOOLS/scripts/03_build_and_upload_provider.sh --profile TOOLS/config/dev X.Y.Z`.
|
||||
|
||||
4. **Перезаливка той же версии бесполезна** — Terraform не перекачает провайдер.
|
||||
Всегда бампать версию.
|
||||
|
||||
5. `*.tfvars` в `.gitignore` (реальный токен в `terraform.tfvars` безопасен от коммита).
|
||||
|
||||
6. **Read-back инвариант.** Провайдер читает параметр обратно из `state_params` инстанса
|
||||
(`RefreshResourceState` + `InputField`). Такой параметр обязан быть `Optional+Computed`,
|
||||
если он не `Required` и без `Default` — правило `helpers.ShouldBeOptionalComputed`.
|
||||
Иначе `plan=null` vs `state=значение` → «Provider produced inconsistent result after
|
||||
apply».
|
||||
|
||||
7. `RefreshResourceState` при отсутствии кода в `state_params` схлопывает `unknown → null`
|
||||
(иначе «Provider produced invalid result object after apply: ... was unknown»).
|
||||
|
||||
8. **`terraform destroy` при tainted-ресурсе** выполняет внутренний обычный plan, который
|
||||
планирует замену (destroy+create); create-узел замены приходит в `ModifyPlan` с prior
|
||||
state = null — отличить замену от создания невозможно. Поэтому create-time проверка
|
||||
существования из `ModifyPlan` убрана (коммит `724f5f7`), проверка осталась в `Create`.
|
||||
|
||||
9. У сервиса без операции `suspend` в YAML: `SupportsSuspendDestroy=false` →
|
||||
`deleteMode := "delete"`, атрибут `suspend_on_destroy` не генерируется, а подсказки в
|
||||
diagnostics не предлагают adopt (он невозможен).
|
||||
|
||||
10. Вложенные параметры (`map-fixed`) — `schema.SingleNestedAttribute`; в HCL это
|
||||
**аргумент** `= { ... }`, не блок. `array-map-fixed` — `schema.StringAttribute`
|
||||
(JSON-строка).
|
||||
|
||||
11. Логи и артефакты отладки: `/tmp/nubes_find_debug.log` (пишет только Plan-диагностика),
|
||||
`/tmp/plan_trace.txt`, `/tmp/plan_destroy.txt`, `/tmp/plan_norefresh.txt`.
|
||||
|
||||
12. Секреты: `secrets/private_key.asc`, `secrets/public_key.asc`, `secrets/dev.token`.
|
||||
Не выводить содержимое в чат.
|
||||
|
||||
---
|
||||
|
||||
## 6. Файлы, которые нужно прочесть
|
||||
|
||||
**Обязательно:**
|
||||
|
||||
1. `.github/copilot-instructions.md` — жёсткие правила оператора.
|
||||
2. `VERSIONS.md` — что и когда залито.
|
||||
3. `HISTORY/2026-09-21_fullpipe_vdc_nsxt_and_refsvc_fixes.md` — журнал предыдущей сессии.
|
||||
4. `TOOLS/resource-generator/internal/templates/instance.go` — главный шаблон ресурса
|
||||
инстанса (ModifyPlan / Create / Read / Update / Delete / Schema).
|
||||
5. `TOOLS/resource-generator/internal/helpers/helpers.go` — `ShouldBeOptionalComputed`,
|
||||
`ParamDefaultExpr`, `IsNested`, nested-хелперы.
|
||||
6. `provider/internal/resources_core/state_refresh.go` — read-back и инвариант known.
|
||||
7. `provider/internal/resources_core/resource_diagnostics_required.go` — create-time
|
||||
проверки существования/усыновления, `runningConflictHint` / `suspendConflictHint`.
|
||||
8. `TOOLS/scripts/03_build_and_upload_provider.sh` и `TOOLS/scripts/build-provider.sh` —
|
||||
релизный пайплайн.
|
||||
9. `DEV_STAND/FullPipe/` — `versions.tf`, `vdc.tf`, `edge.tf`, `variables.tf`, `outputs.tf`.
|
||||
10. `docs/TODO/docs_generator_nested_attr_syntax.md` — описание бага docs-generator
|
||||
(уже исправлен, см. раздел 8).
|
||||
|
||||
**По необходимости:**
|
||||
|
||||
- `TOOLS/resource-generator/internal/templates/{subresource,modifier,action}.go`
|
||||
- `TOOLS/docs-generator/internal/writers/writers.go` — `formatParamOrBlock`, `sampleValue`,
|
||||
`isNestedListParam`
|
||||
- `provider/internal/resources_core/crud.go` — adopt / suspend / delete
|
||||
- `docs/prompt_for_flash_fix_tainted_replace.md` — разбор tainted-replace
|
||||
(реализован в `724f5f7`)
|
||||
- `TOOLS/scripts/validate_docs_examples.sh` — прогон `terraform validate` по примерам из доков
|
||||
- Память репозитория: `/memories/repo/registry-versions.md`
|
||||
|
||||
---
|
||||
|
||||
## 7. Стенд FullPipe (Organization → vDC → Edge)
|
||||
|
||||
- Каталог `DEV_STAND/FullPipe`, провайдер берётся из `versions.tf` (сейчас 2.0.8).
|
||||
- `nubes_vc_vdc.vdc` — `suspend_on_destroy = true`, `adopt_existing_on_create = true`.
|
||||
- `nubes_vc_nsxt.edge` — `routed_net_configuration` задаётся **через `=`** (объект), не блоком.
|
||||
- Подхватить новую версию: `terraform init -upgrade`.
|
||||
- На 2026-09-21 `nubes_vc_nsxt.edge` был **tainted** в state (последствие прошлых
|
||||
неудачных apply). Убирается `terraform untaint nubes_vc_nsxt.edge`.
|
||||
|
||||
---
|
||||
|
||||
## 8. Открытые вопросы
|
||||
|
||||
1. **`docs/TODO/docs_generator_nested_attr_syntax.md` устарел** — баг исправлен
|
||||
(`af2e10b`, `7c2cc67`, `3ca0752`), но в файле статус «не исправлено».
|
||||
2. **Стенд FullPipe не проверен end-to-end на 2.0.8** — нет подтверждённого успешного
|
||||
`apply` (Organization → vDC → Edge) и `destroy` после фиксов.
|
||||
3. Полный прогон `terraform validate` по всем примерам из доков (63 сервиса) не делался —
|
||||
проверен только `vc_nsxt`. Скрипт для прогона готов:
|
||||
`TOOLS/scripts/validate_docs_examples.sh`.
|
||||
4. `TOOLS/resource-generator/internal/templates/modifier.go:65` — та же схема `Computed`
|
||||
без ветки read-back. Для бага «inconsistent result after apply» не критично
|
||||
(Create/Update модификаторов не читают обратно в state), но при работе с `kind: modifier`
|
||||
держать в голове.
|
||||
5. Пуш локальных коммитов в `origin/master` не делался.
|
||||
@@ -0,0 +1,211 @@
|
||||
# CHAT RESUME: IaC-развёртывание Штурвала — состояние на 2026-09-24
|
||||
|
||||
> **Кому:** новый чат / новый участник. Читать целиком, это хендовер.
|
||||
> **Что это:** сводка длинной сессии (2026-09-23/24) по вопросу «как дать клиенту IaC для цепочки Штурвал».
|
||||
> **Статус:** решение НЕ принято, код НЕ написан. Есть анализ, проверенные факты и развилка.
|
||||
|
||||
---
|
||||
|
||||
## 0. TL;DR (одним абзацем)
|
||||
|
||||
Клиенту нужен **настоящий IaC**: один конфиг + `terraform apply` = вся инфраструктура. Цепочка Штурвала
|
||||
(`vcOrg → vcVdc → vcNsxt → [modify оргов/эдж] → k8sShturval`) упирается в две операции `modify`, которые
|
||||
провайдер сейчас выразить не может: в схеме tf-ресурса их параметров нет (схема строится только из `create`).
|
||||
Ручной ЛК и скрипт **отклонены** — это не IaC. Единственный каноничный путь — **отдельные tf-ресурсы под
|
||||
модификации** (как сделано в провайдере VMware Cloud Director, прецедент в папке `!/`),
|
||||
плюс желательно, чтобы платформа отдавала через API выводимые значения (`ipSpaceName`), которые юзер знать не может.
|
||||
Часть прежних «блокеров» оказалась **ложной** — см. §7, это критично.
|
||||
|
||||
---
|
||||
|
||||
## 1. Задача
|
||||
|
||||
Развернуть Штурвал (k8s) целиком через Terraform, с корректным `apply`/`plan`/`destroy`:
|
||||
|
||||
```
|
||||
vcOrg -> create (в нашем случае орг уже создана вручную и НЕ в state)
|
||||
vcVdc -> create
|
||||
vcNsxt -> create (Edge)
|
||||
------------------------------
|
||||
vcOrg -> modify (аллокация внешних IP: vIPConfigure)
|
||||
vcNsxt -> modify (включить SNAT, указать внешний IP из vcOrg: ipSpaceName)
|
||||
------------------------------
|
||||
k8sShturval -> create
|
||||
```
|
||||
|
||||
Операции строго последовательны. Проблема: между `nsxt.create` и `shturval.create` стоят два `modify`,
|
||||
которые в один tf-ресурс не укладываются.
|
||||
|
||||
---
|
||||
|
||||
## 2. Позиции участников (Telegram 2026-09-23)
|
||||
|
||||
| Кто | Позиция |
|
||||
|---|---|
|
||||
| **Владимир (наш)** | Пытается сделать всё терраформом. `create` — ок, `modify` — «просто так не получится, надо создавать дополнительные ресурсы-модификаторы». Сомневался: может, руками в ЛК или скриптом. |
|
||||
| **Георгий** | **Основной запрос клиента — IaC.** Ручной ЛК/скрипт «совсем не подойдёт». Правильно указал порядок: сначала nsxt, потом квота на оргу. |
|
||||
| **Дмитрий** | Хочет «terraform apply одного ямлика со всей инфрой — и всё». Спрашивал, как это реализовано в провайдере Cloud Director из провайдерской УЗ. |
|
||||
| **Виталий** | Дал 3 tf-файла (`vmware_org.tf`, `vdc.tf`, `network.tf.tmpl`) — **официальный провайдер Cloud Director**. Ключевое: `ipSpace → providerGateway → providerVdc` (цепочка, которую юзер не знает), «квоту делаем через API на оргу, т.к. эджей много, а орга одна», «при создании параметры подкладываются», «в организации один T0». |
|
||||
|
||||
---
|
||||
|
||||
## 3. Подтверждённые факты (с источниками)
|
||||
|
||||
1. **Схема tf-ресурса строится ТОЛЬКО из `create`** (генератор `TOOLS/resource-generator`).
|
||||
→ modify-only параметры в схему не попадают.
|
||||
2. **`vIPConfigure`** (vc_org, modify id **207**, param id **662**, `array-map-fixed`, sub: `name`=39, `count`=40)
|
||||
есть **только** в modify. В `create` (id 136) — только `resourceRealm`(418), `organizationType`(556), `orgSuffix`(1125).
|
||||
Файл: `generated/dev/resources_yaml/19_vc_org.yaml`.
|
||||
3. **`ipSpaceName`** (vc_nsxt, modify id **111**, param id **372**, string, required false) есть **только** в modify.
|
||||
В `create` (id 10) — `vdcUid`, `needEnableAVI`(340), `virtualServicesCount`(341), `qosProfile`(825), `routedNetConfiguration`(1110).
|
||||
Файл: `generated/dev/resources_yaml/22_vc_nsxt.yaml`.
|
||||
4. **`vIPConfigure` — replace-семантика, НЕ накопительная.** Повторный modify с тем же count не задваивает (1→1),
|
||||
работает вверх/вниз/до 0, `count=0` принимается (несмотря на `minvalue:1`), live читается из `state.params`.
|
||||
Источник: `NOTES/30_analysis/ORG_IP_MODIFIER_TEST_2026-09-22.md` (проверено на стенде DEV_STAND/FullPipe, провайдер 2.0.9).
|
||||
5. **`ipSpaceName` — выводимое значение:** цепочка `providerVdc → providerGateway → ipSpace`, юзер его не знает.
|
||||
Платформа сейчас «подкладывает» недостающие параметры при создании пустой орги. Список доступных ipSpace
|
||||
в статической выгрузке (`/instanceOperations/default/{id}`) — пустой, виден только в ЛК/живом инстансе. (Виталий + форензика)
|
||||
6. **Допущение платформы: в организации один T0/провайдер-шлюз.** Рост T0 отложен, но при нём схема сломается.
|
||||
7. **Доступ к значению modify-параметра:** live берётся из `GET /instances/{uid}` → `state.params`,
|
||||
а НЕ из `cfsParams.paramValue` (это сохранённый дефолт формы от прошлых прогонов, см. `dtCreated` в HAR).
|
||||
8. **Flow modify (HAR `org_enough_.har`):** `POST /instanceOperations {instanceUid, operation:"modify"}` →
|
||||
`POST /instanceOperationCfsParams {paramValue, instanceOperationUid, svcOperationCfsParamId}` →
|
||||
`POST /instanceOperations/{opUid}/validate-cfs` → `POST /instanceOperations/{opUid}/run`.
|
||||
9. **Прецедент (VCD, папка `!/`):** та же цепочка делается **отдельными ресурсами** с `depends_on`:
|
||||
`vcd_nsxt_alb_settings` (`count = var.alb_enable ? 1 : 0`), `vcd_nsxt_alb_edgegateway_service_engine_group`
|
||||
(`reserved_virtual_services`), `vcd_network_routed_v2`, `vcd_ip_space_custom_quota` (на оргу).
|
||||
Приём «включено/выключено» = существование ресурса; **inverse = удаление ресурса**.
|
||||
10. **Nubes — надстройка над Cloud Director.** Наши `vcOrg`/`vcVdc`/`vcNsxt` создают объекты в VCD.
|
||||
Провайдер Nubes — обёртка над API Nubes, отдельный от официального `terraform-provider-vcd`.
|
||||
|
||||
---
|
||||
|
||||
## 4. Почему «просто добавить поле» / «насильно в state» / «скрипт» — не работает
|
||||
|
||||
- **Добавить поле в .tf** → падает на `plan`: атрибута нет в схеме (см. §3.1).
|
||||
- **Terraform не может внутри одного ресурса** сделать «create → через N шагов modify».
|
||||
Декларативный Update требует **желаемого состояния**; `ipSpaceName` — это включение SNAT, а не значение поля.
|
||||
- **Вписать в tfstate** нельзя: state валидируется по схеме провайдера, а «записанное» состояние ≠ реальность
|
||||
(получишь чистый `plan` при сломанной инфраструктуре). `null_resource`/`terraform_data` + `local-exec` даёт
|
||||
только **факт** выполнения, не состояние.
|
||||
- **Ручной ЛК / скрипт вне tf** — отклонено: это не IaC (нет версионирования, воспроизводимости, дрейфа, отката).
|
||||
- **Правка провайдера «по-старому»** (метки `kind: modifier` в YAML + реестр в `yaml-generator`) — **отменённый заход**,
|
||||
см. §7.
|
||||
|
||||
---
|
||||
|
||||
## 5. Каноничное решение (что делать)
|
||||
|
||||
**Два независимых требования — нужны оба:**
|
||||
|
||||
1. **Отдельные tf-ресурсы под `modify`** (наша сторона): e.g. `nubes_org_ip_allocation` (vIPConfigure),
|
||||
`nubes_nsxt_network` (`needEnableAVI`/`virtualServicesCount`/`ipSpaceName`/`routedNetConfiguration`),
|
||||
привязка через `depends_on` к орге/эджу.
|
||||
- `Read` = читать родителя (`state.params`), `Delete` = обратный modify (`count=0` / `needEnableAVI=false` /
|
||||
`ipSpaceName="no-needed"`), `Create/Update` = `modify` с параметрами.
|
||||
2. **Доступ к выводимым значениям через API** (сторона платформы): динамические `valueList` + цепочка
|
||||
`providerVdc → providerGateway → ipSpace`. Иначе юзер подсматривает в ЛК (ручной ввод как временный долг допустим,
|
||||
но поле надо делать `Optional+Computed`, чтобы позже включить автоподстановку без breaking change).
|
||||
|
||||
**Дизайн-требования, заложить сразу:**
|
||||
- `ip_space_name` → `Optional + Computed`;
|
||||
- optional селектор шлюза (`t0_id` / `provider_gateway`) — чтобы рост T0 не сломал схему;
|
||||
- не тащить доменные метки в универсальный YAML (см. §7).
|
||||
|
||||
---
|
||||
|
||||
## 6. Что можно делать уже сейчас, не дожидаясь платформы
|
||||
|
||||
- **`vIPConfigure` (аллокация IP на оргу) — можно делать начисто**: блокеров нет, replace-семантика подтверждена тестом,
|
||||
Read/Delete выражаются через `state.params` и `count=0`.
|
||||
- **`needEnableAVI` / `virtualServicesCount`** — выразимы (есть и в create, и в modify).
|
||||
- **`ipSpaceName`** — единственное, что упирается в платформу; временно — ввод юзером (значение он и так смотрит в ЛК).
|
||||
- **`routedNetConfiguration`** — есть в create (1110) и modify (1112), выразимо.
|
||||
|
||||
**Не решено (требует решения до кода):**
|
||||
- откуда генератор берёт **список** доменных ресурсов (это не данные API, а доменное знание — то самое место,
|
||||
где раньше был реестр в `yaml-generator`). Форму выбрать осознанно: явный список vs отдельный вход.
|
||||
- какие шаги вообще остаются на **провайдерском** (облачном) уровне, а какие отдаются тенанту
|
||||
(квота ipSpace / ALB — возможно, это уровень облака, и тогда в клиентский tf они не входят).
|
||||
|
||||
---
|
||||
|
||||
## 7. ⚠️ Исправленные ошибки (НЕ повторять!)
|
||||
|
||||
1. **Ложный факт «`vIPConfigure` накопительный».** Был протащен в промпт для Opus как «подтверждённый», из-за чего
|
||||
Opus построил вывод «накопительный API несовместим с декларативной моделью» и объявил два «блокера»
|
||||
(Read счётчика, адресное освобождение). **Оба ложны** — тест `ORG_IP_MODIFIER_TEST_2026-09-22.md` доказывает
|
||||
идемпотентность и работу в обе стороны. Документы исправлены.
|
||||
2. **Прежняя repo-память (`modifier-gotchas.md`) содержала устаревшие утверждения** (эпоха 09-21/22):
|
||||
`kind: modifier`, `delete_strategy`, `nubes_vc_nsxt_network`, «у vc_nsxt нет instance-modify»,
|
||||
«Update = no-op». **Файл перезаписан** актуальными фактами. НЕ использовать старую формулировку.
|
||||
3. **Старые «модификаторы» были написаны и даже работали** (09-22), но заход признан негодным:
|
||||
доменную логику вшили в универсальный генератор (метки в YAML). Соответствующие документы помечены баннером LEGACY.
|
||||
|
||||
---
|
||||
|
||||
## 8. Карта файлов
|
||||
|
||||
**АКТУАЛЬНО (источник истины):**
|
||||
- `NOTES/40_chat_summaries/CHAT_RESUME_IAC_2026-09-24.md` ← этот файл
|
||||
- `NOTES/30_analysis/SHTURVAL_IAC_MODIFY_ANALYSIS_2026-09-23.md` — анализ, варианты A–E, мнение
|
||||
- `NOTES/30_analysis/OPUS_ANSWER_IAC_SHTURVAL_MODIFY_2026-09-23.md` — ответ Opus + поправки (ложные блокеры сняты)
|
||||
- `NOTES/30_analysis/ORG_IP_MODIFIER_TEST_2026-09-22.md` — проверенные факты по vIPConfigure
|
||||
- `NOTES/20_prompts/prompt_for_opus_iac_shturval_modify.md` — промпт (факты исправлены)
|
||||
- `generated/dev/resources_yaml/19_vc_org.yaml`, `22_vc_nsxt.yaml` — спеки (факты по операциям/параметрам)
|
||||
- `HAR/org_enough_.har`, `HAR/org2.har`, `HAR/edge_.har` — live-семантика modify
|
||||
- `!/` — прецедент Cloud Director (3 файла `vcd_*`), НЕ наш код
|
||||
|
||||
**LEGACY — с баннерами (НЕ источник истины, файлы сохранены для истории):**
|
||||
|
||||
⛔ «ЛОЖНЫЙ ПУТЬ — ОТМЕНЕНО. ТАК ДЕЛАТЬ НЕЛЬЗЯ» (метки `kind: modifier` в YAML + реестр в `yaml-generator`):
|
||||
- `PLAN_modifier_redesign.md`
|
||||
- `docs/60_strategy/modifier_resources_ideology_and_specification.md`
|
||||
- `NOTES/20_prompts/prompt_for_opus_modifier_architecture_full.md`, `…_architecture_q3.md`, `…_modifier_null_bug.md`, `…_modifier_plan_review.md`, `…_modifiers_review.md`
|
||||
- `NOTES/20_prompts/prompt_for_opus_modifier_review_2.md`
|
||||
- `HISTORY/OPUS/2026-09-22_modifier_architecture_project.md`, `…_modifier_null_reset_bug.md`, `…_modifier_plan_review.md`, `…_modifiers_code_review.md`
|
||||
|
||||
⚠️ «ЧАСТИЧНО УСТАРЕЛО» (модель отменённого механизма; факты внутри верны и переиспользуются):
|
||||
- `NOTES/30_analysis/inverse_rollback_analysis_2026-09-23.md`
|
||||
|
||||
✅ «АКТУАЛЬНОЕ НАПРАВЛЕНИЕ, НО НЕ РЕАЛИЗОВАНО»:
|
||||
- `NOTES/20_prompts/prompt_for_opus_modifier_global_architecture.md` (требования: YAML без доменных меток; модификаторы — не ветка генератора)
|
||||
|
||||
⚠️ «ПЕРЕКРЫТ»:
|
||||
- `PLAN_regenerate_providers_0.0.1.md` — версия `0.0.1` объявлена легаси в `PLAN_FLASH_reversion_cleanup.md`
|
||||
|
||||
**Статус не определён (не трогал):**
|
||||
- `NOTES/20_prompts/prompt_for_opus_modifiable_architecture.md` (тема: CreateOnly vs Modifiable — не относится напрямую к отменённому заходу)
|
||||
|
||||
> Примечание: `NOTES/30_analysis/ORG_IP_MODIFIER_TEST_2026-09-22.md` — **актуален** (отчёт по проверке, не план).
|
||||
|
||||
**Инфра-контекст:**
|
||||
- `TOOLS/config/<стенд>/profile.env` → `NUBES_API_ENDPOINT`, `TOKEN_FILE`
|
||||
- `VERSIONS.md` — залитые версии (DEV на 09-22 = 2.0.13; в `generated/dev/provider_build/` лежат 2.0.17 — расхождение)
|
||||
|
||||
---
|
||||
|
||||
## 9. Развилка (ждёт решения)
|
||||
|
||||
| Вариант | Суть | Вердикт |
|
||||
|---|---|---|
|
||||
| **A** | Отдельные tf-ресурсы под modify (+ позже data-source для ipSpace) | канон; реализуемо сейчас для `vIPConfigure` |
|
||||
| **B** | То же, но значения вводит юзер вручную | приемлемый временный долг при `Optional+Computed` |
|
||||
| **C** | Ждать новых спеков платформы | часть работ всё равно можно начать сейчас |
|
||||
| **D** | Ручной ЛК / скрипт вне tf | ❌ отклонено (требование IaC) |
|
||||
| **E** | Пресеты/дефолтное окружение | снижает боль на старте, IaC не заменяет |
|
||||
|
||||
**Не принято:** делать ли `modify`-ресурсы доменными «руками» (и как их перечислять в генераторе) —
|
||||
вопрос архитектуры; и что из шагов остаётся за облаком.
|
||||
|
||||
---
|
||||
|
||||
## 10. Открытые вопросы к людям
|
||||
|
||||
1. **Георгию/продукту:** какие шаги цепочки — тенантские, а какие — уровень облака (квота ipSpace, ALB)?
|
||||
От этого зависит объём ресурсов в клиентском tf.
|
||||
2. **Виталию (платформа):** можете отдавать через API (а) динамические списки значений (`ipSpace`),
|
||||
(б) цепочку `providerVdc → providerGateway → ipSpace`?
|
||||
3. **Виталию:** файлы `!/` — ваш инструмент провижининга от провайдерской УЗ или справочный пример?
|
||||
(в диалоге он сказал только «это провайдер от клауд директора»)
|
||||
4. **Команде:** откуда генератор берёт список доменных ресурсов-модификаций (форма решения, без меток в YAML).
|
||||
@@ -0,0 +1,27 @@
|
||||
# 40_chat_summaries — выжимки и хендоверы из чатов
|
||||
|
||||
Резюме длинных сессий: чтобы новый чат/человек вошёл в контекст, не перечитывая переписку.
|
||||
Формат — по конвенции репозитория: `CHAT_RESUME_<тема>_<дата>.md`.
|
||||
|
||||
## Файлы
|
||||
|
||||
| Файл | Тема | Статус |
|
||||
|---|---|---|
|
||||
| `CHAT_RESUME_IAC_2026-09-24.md` | **Текущая линия: IaC + `modify` (Штурвал).** Задача, позиции участников (Георгий/Дмитрий/Виталий), 10 подтверждённых фактов, почему не работает простое, каноничное решение, развилка A–E, **исправленные ошибки**, карта файлов, открытые вопросы | ✅ **Актуально — начинать отсюда** |
|
||||
| `CHAT_RESUME_2026-09-22.md` | Предыдущая сессия (дата в имени) | ⚠️ История |
|
||||
| `CHAT_RESUME_2026-09-21.md` | Предыдущая сессия | ⚠️ История |
|
||||
| `CHAT_RESUME_2026-09-20.md` | Предыдущая сессия | ⚠️ История |
|
||||
| `CHAT_RESUME_NEW.md` | Хендовер «для нового чата» (без даты — сложно датировать) | ⚠️ Проверить дату перед использованием |
|
||||
| `CHAT_RESUME.md` | Первый/базовый хендовер (без даты) | ⚠️ Проверить дату |
|
||||
| `CHAT_RESUME_PLAN_VM.md` | Хендовер по плану работ на ВМ | ⚠️ Проверить дату |
|
||||
|
||||
## Как пользоваться
|
||||
|
||||
1. Открыть `CHAT_RESUME_IAC_2026-09-24.md` — это сводка всей линии по IaC.
|
||||
2. Внутри него есть ссылки на детальные документы (`../30_analysis/*`, `../20_prompts/*`).
|
||||
3. Устаревшие рестюме **не удалять** — они фиксируют состояние на свою дату (полезно для хронологии).
|
||||
|
||||
## Важно про даты
|
||||
|
||||
Файлы без даты в имени (`CHAT_RESUME.md`, `_NEW`, `_PLAN_VM`) — потенциальный источник путаницы:
|
||||
перед использованием проверьте содержание (даты/версии внутри) на актуальность.
|
||||
@@ -0,0 +1,24 @@
|
||||
# 60_reference — справочные материалы
|
||||
|
||||
Справочники и внешние факты: состояния инстансов, матрицы переходов, списки API, обзор
|
||||
универсального ядра. Это не планы и не анализы — это «шпаргалки».
|
||||
|
||||
## Файлы
|
||||
|
||||
| Файл | Что внутри | Статус |
|
||||
|---|---|---|
|
||||
| `INSTANCE_STATES.md` | Матрица состояний ресурсов в облаке ngcloud | ✅ Справочник |
|
||||
| `STATE_TRANSITIONS.md` | Матрица переходов состояний инстансов ngcloud | ✅ Справочник |
|
||||
| `apis.txt` | Список API. **В самом файле предупреждение:** старые API закрываются, не использовать для генерации; актуальные — `lk-api-gateway*.ngcloud.ru/api/v1/svc` | ⚠️ Читать с учётом шапки файла |
|
||||
| `ai_universal_provider_gen.md` | Подробный отчёт по универсальному ядру и генератору ресурсов | ⚠️ Проверить актуальность (описывает общее устройство) |
|
||||
|
||||
## Где искать более точные данные
|
||||
|
||||
| Нужно | Где |
|
||||
|---|---|
|
||||
| Endpoint + токен стенда | `../../TOOLS/config/<стенд>/profile.env` → `NUBES_API_ENDPOINT`, `TOKEN_FILE` |
|
||||
| Спеки сервисов (операции/параметры/ID) | `../../generated/<стенд>/resources_yaml/<id>_<svc>.yaml` |
|
||||
| Live-ответы API | `../../HAR/*.har` |
|
||||
| Залитые версии провайдера | `../../VERSIONS.md` |
|
||||
| Схема нумерации стендов | `../10_plans/PLAN_FLASH_reversion_cleanup.md` |
|
||||
| Состояния после `modify` (примеры) | `../30_analysis/ORG_IP_MODIFIER_TEST_2026-09-22.md` |
|
||||
@@ -0,0 +1,67 @@
|
||||
# NOTES — рабочие материалы по провайдеру Nubes
|
||||
|
||||
Здесь лежат **рабочие материалы**: планы, промпты для LLM, анализы, выжимки из чатов, процессные
|
||||
и справочные заметки. Это НЕ публикуемая документация пользователя (та — в `docs/`, собирается mkdocs).
|
||||
|
||||
> **Главное правило:** файлы с баннером ⛔ в первой строке — **отменённый («ложный») путь**.
|
||||
> Читать можно, опираться нельзя.
|
||||
|
||||
---
|
||||
|
||||
## Карта папок
|
||||
|
||||
| Папка | Что внутри | Когда идти туда |
|
||||
|---|---|---|
|
||||
| [`10_plans/`](10_plans/README.md) | Планы работ: актуальные, перекрытые и отменённые | Понять текущие задачи и их статус |
|
||||
| [`20_prompts/`](20_prompts/README.md) | Промпты для LLM (Opus/Sol/Sonnet/DeepSeek/Codex) | Понять, что и у кого спрашивали; переиспользовать формулировки |
|
||||
| [`30_analysis/`](30_analysis/README.md) | Анализы, форензика, отчёты, ответы LLM-ревью, разборы багов | Найти факты и обоснования решений |
|
||||
| [`40_chat_summaries/`](40_chat_summaries/README.md) | Выжимки-хендоверы из чатов (`CHAT_RESUME_*`) | Быстро войти в контекст прошлых сессий |
|
||||
| [`60_reference/`](60_reference/README.md) | Справочники: состояния, стадии, API, архитектурный обзор | Уточнить терминологию и внешние факты |
|
||||
|
||||
> ❗ **Инструкции (сборка/заливка, добавление сервиса, процессы) вынесены из NOTES в корневую папку
|
||||
> [`HOW_TO/`](../HOW_TO/README.md)** — там индекс «что нужно → какой файл».
|
||||
|
||||
---
|
||||
|
||||
## Что осталось в корне и в других местах (НЕ переносилось)
|
||||
|
||||
| Путь | Что это | Почему оставлено |
|
||||
|---|---|---|
|
||||
| `docs/` | Документация + налоговая структура `00_overview … 90_finance`, `curated/`, `TODO/`, `help/`, `ops/` | Завязана на mkdocs (`mkdocs.yml`, `exclude_docs`) — перенос ломает сборку сайта |
|
||||
| `HISTORY/` | Архив: записи по датам, `HISTORY/OPUS/`, `HISTORY/SONNET/` | Это канонический архив истории; отменённый заход помечен баннерами внутри |
|
||||
| `HAR/` | Сырые HAR-дампы (live-запросы к API стенда) | Исходные данные для анализа, менять нельзя |
|
||||
| `generated/` | Выхлоп генераторов (`resources_yaml`, `go`, `docs`, `provider_build`) | Машинный вывод, не заметки |
|
||||
| `TOOLS/`, `provider/`, `scripts/`, `gateway/`, `apps/`, `charts/`, `tf_examples/`, `secrets/` | Код и артефакты проекта | Не заметки |
|
||||
| `! /` | **Прецедент Cloud Director** (`vmware_org.tf`, `vdc.tf`, `network.tf.tmpl`) — чужой tf-код на официальном vcd-провайдере | Оставлено на месте (папка названа «!» специально, чтобы быть на виду) |
|
||||
| `README.md`, `VERSIONS.md`, `mkdocs.yml` | Точки входа | `README.md` — карта всего проекта; `VERSIONS.md` — источник правды по залитым версиям |
|
||||
| `TMP/`, `test_push.md`, `demo.jpg` | Временное/непонятное | Не трогал (статус не определён) |
|
||||
|
||||
---
|
||||
|
||||
## Актуальное состояние (2026-09-24)
|
||||
|
||||
Если нужен контекст по задаче «IaC + модификаторы (modify)», читать в таком порядке:
|
||||
|
||||
1. [`40_chat_summaries/CHAT_RESUME_IAC_2026-09-24.md`](40_chat_summaries/CHAT_RESUME_IAC_2026-09-24.md) — **начинать отсюда**: полный хендовер (задача, факты, позиции участников, развилка, исправленные ошибки).
|
||||
2. [`30_analysis/SHTURVAL_IAC_MODIFY_ANALYSIS_2026-09-23.md`](30_analysis/SHTURVAL_IAC_MODIFY_ANALYSIS_2026-09-23.md) — анализ проблемы и варианты.
|
||||
3. [`30_analysis/OPUS_ANSWER_IAC_SHTURVAL_MODIFY_2026-09-23.md`](30_analysis/OPUS_ANSWER_IAC_SHTURVAL_MODIFY_2026-09-23.md) — ответ Opus **с поправками** (часть его «блокеров» ложная).
|
||||
4. [`30_analysis/ORG_IP_MODIFIER_TEST_2026-09-22.md`](30_analysis/ORG_IP_MODIFIER_TEST_2026-09-22.md) — единственные проверенные на стенде факты.
|
||||
5. [`20_prompts/prompt_for_opus_iac_shturval_modify.md`](20_prompts/prompt_for_opus_iac_shturval_modify.md) — актуальный промпт.
|
||||
|
||||
---
|
||||
|
||||
## Статусы файлов (что можно использовать)
|
||||
|
||||
- ✅ **Актуально** — можно опираться.
|
||||
- ⚠️ **Перекрыто / частично** — использовать осторожно, есть более новый документ.
|
||||
- ⛔ **Отменённый путь** — только как история (баннер в файле).
|
||||
|
||||
Точные статусы по каждому файлу — в README соответствующей папки.
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Известная проблема: устаревшие ссылки
|
||||
|
||||
Файлы переносились из корня и `docs/`, поэтому **внутри исторических документов могли остаться
|
||||
старые пути** (например `docs/prompts/...` или `docs/CHAT_RESUME_...`). В актуальных документах
|
||||
ссылки обновлены; в архивных — могли не обновляться: ищите файл по имени, а не по пути.
|
||||
@@ -1,135 +1,130 @@
|
||||
# DevOps Runbook: Provider Build Pipeline
|
||||
# Terraform Provider Nubes — карта проекта
|
||||
|
||||
This repo root contains the 4 scripts for the full provider build pipeline.
|
||||
Репозиторий содержит **Terraform-провайдер Nubes Cloud** и всю обвязку вокруг него:
|
||||
генераторы (API → YAML → Go), пайплайн сборки/заливки, документацию, стенды и служебные материалы.
|
||||
|
||||
## Overview
|
||||
Облачная платформа — **VMware Cloud Director**; сервисы Nubes (`vcOrg`, `vcVdc`, `vcNsxt`, `k8s…`)
|
||||
создают объекты в ней. Провайдер генерируется из спецификаций API, а не пишется руками.
|
||||
|
||||
1) Generate YAML specs from API
|
||||
2) Generate Go resources + documentation files from YAML
|
||||
3) Build and upload provider binaries for 3 OS targets
|
||||
4) Build and publish documentation site
|
||||
---
|
||||
|
||||
## Documentation publishing instructions
|
||||
## 🚦 Быстрая навигация
|
||||
|
||||
The verified documentation generation and publishing pipeline is documented in
|
||||
[`HISTORY/2026-09-03_docs_upload_pipeline_verified.md`](HISTORY/2026-09-03_docs_upload_pipeline_verified.md).
|
||||
It covers the generated docs source, MkDocs build, the separate documentation
|
||||
S3 bucket, VM upload and mirror steps, stand-specific URLs, and the legacy
|
||||
script that must not be used.
|
||||
| Что нужно | Куда идти |
|
||||
|---|---|
|
||||
| **Инструкции: сборка, заливка, добавление сервиса** | **[`HOW_TO/`](HOW_TO/README.md)** ← начинать отсюда |
|
||||
| Рабочие материалы: планы, промпты, анализы, выжимки чатов | [`NOTES/`](NOTES/README.md) |
|
||||
| Пользовательская документация (mkdocs) | [`docs/`](docs/README.md) |
|
||||
| Архив по датам и разборам | [`HISTORY/`](HISTORY/) |
|
||||
| Пайплайн публикации документации | [`DOCS_PIPELINE/README.md`](DOCS_PIPELINE/README.md) |
|
||||
| Залитые версии провайдера (источник правды) | [`VERSIONS.md`](VERSIONS.md) |
|
||||
| Правила генерации кода (обязательны для генератора) | [`TOOLS/ARCHITECTURE.md`](TOOLS/ARCHITECTURE.md) |
|
||||
| Правила работы для агента | [`.github/copilot-instructions.md`](.github/copilot-instructions.md) |
|
||||
| Текущая задача (IaC + `modify`) | [`NOTES/40_chat_summaries/CHAT_RESUME_IAC_2026-09-24.md`](NOTES/40_chat_summaries/CHAT_RESUME_IAC_2026-09-24.md) |
|
||||
|
||||
## Prerequisites
|
||||
---
|
||||
|
||||
- Go 1.22+
|
||||
- `python3`
|
||||
- `gpg`
|
||||
- `mc` (MinIO/S3 client)
|
||||
- Docker (for mkdocs build)
|
||||
## Как собрать и залить провайдер (кратко)
|
||||
|
||||
## Shared settings
|
||||
|
||||
S3 environment:
|
||||
- `S3_ENDPOINT` (example: `https://s3.msk-1.ngcloud.ru`)
|
||||
- `S3_ACCESS_KEY`
|
||||
- `S3_SECRET_KEY`
|
||||
|
||||
Provider naming defaults:
|
||||
- `REGISTRY_HOSTNAME`: `tf-registry.containerk8s.services.ngcloud.ru`
|
||||
- `NAMESPACE`: `nubes`
|
||||
- `NAME`: `nubes`
|
||||
|
||||
## Step 1: Generate YAMLs from API
|
||||
|
||||
Script: `01_generate_yamls.sh`
|
||||
|
||||
Input list of services:
|
||||
- `services_list.txt` (service_id only)
|
||||
|
||||
Token options:
|
||||
- `TOKEN_FILE=/home/naeel/terra/HH-MM-SS.token`, or
|
||||
- `NUBES_API_TOKEN` directly
|
||||
|
||||
Example:
|
||||
```bash
|
||||
export TOKEN_FILE=/home/naeel/terra/08-33-41.token
|
||||
./01_generate_yamls.sh
|
||||
cd /home/naeel/TF/tf_provider
|
||||
|
||||
# 1) YAML-спеки из API стенда
|
||||
./TOOLS/scripts/01_generate_yamls.sh --profile TOOLS/config/dev
|
||||
# 2) YAML → Go-ресурсы + документация
|
||||
./TOOLS/scripts/02_generate_resources_and_docs_v2.sh --profile TOOLS/config/dev
|
||||
# 3) Сборка (linux/windows/darwin) + GPG-подпись + заливка в S3
|
||||
./TOOLS/scripts/03_build_and_upload_provider.sh --profile TOOLS/config/dev 2.0.18
|
||||
# 4) (опционально) публикация документации
|
||||
./TOOLS/scripts/04_build_and_publish_docs.sh --profile TOOLS/config/dev 2.0.18
|
||||
```
|
||||
|
||||
## Step 2: Generate Go resources and docs
|
||||
**Подробные инструкции:**
|
||||
[`HOW_TO/HOWTO-UPLOAD.md`](HOW_TO/HOWTO-UPLOAD.md) (сборка/заливка) ·
|
||||
[`HOW_TO/DEVOPS_BUILD_PIPELINE.md`](HOW_TO/DEVOPS_BUILD_PIPELINE.md) (полный ранбук + GPG-bootstrap) ·
|
||||
[`HOW_TO/HOWTO_ADD_NEW_SERVICE.md`](HOW_TO/HOWTO_ADD_NEW_SERVICE.md) (новый сервис).
|
||||
|
||||
Script: `02_generate_resources_and_docs.sh`
|
||||
**Схема версий (жёстко):** `prod = 1.*`, `dev = 2.*`, `test = 3.*`.
|
||||
Легаси (`prod=2.*`, `dev=3.*`, `test=5.*`, `0.0.1`) — не использовать.
|
||||
|
||||
Example:
|
||||
```bash
|
||||
./02_generate_resources_and_docs.sh
|
||||
---
|
||||
|
||||
## Структура репозитория
|
||||
|
||||
| Путь | Что это |
|
||||
|---|---|
|
||||
| **`HOW_TO/`** | **Все общие инструкции:** сборка/заливка, пайплайн, добавление сервиса, миграция, генерация доков. Индекс: `HOW_TO/README.md` |
|
||||
| **`NOTES/`** | Рабочие материалы: `10_plans`, `20_prompts`, `30_analysis`, `40_chat_summaries`, `60_reference`. Карта: `NOTES/README.md` |
|
||||
| **`docs/`** | Документация: публикуемый сайт (mkdocs) + внутренние разделы (см. ниже) |
|
||||
| **`DOCS_PIPELINE/`** | Пайплайн публикации сайта документации (`publish-docs.sh`) |
|
||||
| **`HISTORY/`** | Архив по датам (`HISTORY/OPUS/`, `HISTORY/SONNET/`) — хроника решений и разборов |
|
||||
| **`TOOLS/`** | Генераторы и скрипты: `yaml-generator` (API→YAML), `resource-generator` (YAML→Go), `docs-generator`, `scripts/`, `config/`, `lib/`. Правила: `TOOLS/ARCHITECTURE.md` |
|
||||
| **`provider/`** | Исходники самого провайдера (ядро, CRUD-хелперы, `main.go`) |
|
||||
| **`generated/`** | Машинный выхлоп: `generated/<стенд>/resources_yaml`, `/go`, `/docs`, `/provider_build` |
|
||||
| **`DEV_STAND/`, `TEST_STAND/`, `PROD_STAND/`** | Terraform-манифесты стендов (проверочные конфигурации, `sync.sh`) |
|
||||
| **`HAR/`** | HAR-дампы live-запросов к API (сырые данные для анализа) |
|
||||
| **`secrets/`** | Токены стендов, GPG-ключи, S3-креды. **Не коммитить** |
|
||||
| **`gateway/`, `apps/`, `charts/`** | Вспомогательный сервис/приложения/чарты (вне ядра провайдера) |
|
||||
| **`tf_examples/`, `tfflaskcrud/`, `tfluceecrud/`, `tfnodejscrud/`** | Примеры конфигураций Terraform |
|
||||
| **`! /`** | Прецедент Cloud Director (чужой tf-код на официальном `terraform-provider-vcd`) — образец «как надо» |
|
||||
| **`TMP/`** | Временное/бэкапы |
|
||||
| `mkdocs.yml`, `site/`, `site_test/` | Конфиг и вывод сборки сайта документации |
|
||||
|
||||
---
|
||||
|
||||
## Документация: `docs/`
|
||||
|
||||
Собирается mkdocs (`mkdocs.yml`, nav → `docs/`). Разделы:
|
||||
|
||||
| Раздел | Что внутри |
|
||||
|---|---|
|
||||
| `docs/index.md`, `docs/30_registry/` | **Публикуется**: главная, справочник ресурсов, руководства, ассеты |
|
||||
| `docs/curated/` | Проверенные примеры (напр. `postgres/pg_user_db.md`) |
|
||||
| `docs/90_finance/` | Финансовые шаблоны (акт) |
|
||||
| `docs/ops/` | Операционные runbook'и: `API_TOKENS.md`, `STANDS.md`, `MONITORING.md`, `ROLLBACK.md`, `RUNBOOK.md`, `TESTING.md` |
|
||||
| `docs/help/` | Внутренние справки: `BUILD.md`, `build-and-publish.md`, `architecture-and-methods.md`, `error-knowledge-base.md`, `dev-reference/` |
|
||||
| `docs/00_overview/`, `docs/20_discovery/`, `docs/40_analysis/`, `docs/50_history/`, `docs/60_strategy/`, `docs/70_api/` | Внутренние разделы (исключены из сайта: `exclude_docs` в `mkdocs.yml`) |
|
||||
| `docs/TODO/` | Технические заметки «что не сделано» |
|
||||
|
||||
---
|
||||
|
||||
## Пайплайн (что происходит под капотом)
|
||||
|
||||
```
|
||||
API стенда ──01──▶ generated/<стенд>/resources_yaml/*.yaml (yaml-generator)
|
||||
│
|
||||
├──02──▶ generated/<стенд>/go/*.go (resource-generator + registry)
|
||||
│ generated/<стенд>/docs/*.md (docs-generator)
|
||||
│
|
||||
├──03──▶ сборка linux/windows/darwin → GPG-подпись → S3-реестр
|
||||
└──04──▶ mkdocs build → публикация документации
|
||||
```
|
||||
|
||||
Outputs:
|
||||
- Go files in `universal_rebuild/internal/resources_gen`
|
||||
- Docs in `docs/30_registry/resources`
|
||||
Ключевое: **схема tf-ресурса строится из операции `create`** в YAML, а ID операций/параметров
|
||||
сохраняются из API. Нюансы и известные ограничения — в `NOTES/30_analysis/` и `NOTES/README.md`.
|
||||
|
||||
## Step 3: Build and upload provider
|
||||
---
|
||||
|
||||
Script: `03_build_and_upload_provider.sh`
|
||||
## Стенды и реестр
|
||||
|
||||
Uses `registry-server-build/build-provider.sh` and signs with:
|
||||
- `secrets/private_key.asc` (ignored by git)
|
||||
| Стенд | Namespace | Диапазон версий | Профиль |
|
||||
|---|---|---|---|
|
||||
| PROD | `nubes` | `1.*` | `TOOLS/config/prod` |
|
||||
| DEV | `nubes-dev` | `2.*` | `TOOLS/config/dev` |
|
||||
| TEST | `nubes-test` | `3.*` | `TOOLS/config/test` |
|
||||
|
||||
Example:
|
||||
```bash
|
||||
export S3_ENDPOINT=https://s3.msk-1.ngcloud.ru
|
||||
export S3_ACCESS_KEY=...
|
||||
export S3_SECRET_KEY=...
|
||||
./03_build_and_upload_provider.sh 2.0.2
|
||||
```
|
||||
- Реестр: `tf-registry.containerk8s.services.ngcloud.ru`; бакет бинарников `nubes-terraform-registry`.
|
||||
- Общий конфиг реестра: `TOOLS/config/registry.env`; стенд-специфика: `TOOLS/config/<стенд>/profile.env`
|
||||
(`NUBES_API_ENDPOINT`, `TOKEN_FILE`, `NAMESPACE`, `VERSION`).
|
||||
- Список сервисов для генерации: `TOOLS/config/services_list.txt`.
|
||||
|
||||
## Step 4: Build and publish docs
|
||||
---
|
||||
|
||||
Script: `04_build_and_publish_docs.sh`
|
||||
## ⛔ Чего не делать
|
||||
|
||||
Example:
|
||||
```bash
|
||||
export S3_ENDPOINT=https://s3.msk-1.ngcloud.ru
|
||||
export S3_ACCESS_KEY=...
|
||||
export S3_SECRET_KEY=...
|
||||
./04_build_and_publish_docs.sh 2.0.2
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- The GPG private key must remain stable across releases. Do not regenerate per build.
|
||||
- If the key is regenerated, the registry server must be updated to serve the new public key.
|
||||
- Terraform will fail with `authentication signature from unknown issuer` if the registry public key does not match the signing key.
|
||||
- `services_list.txt` is the source of truth for which services are generated.
|
||||
- If the provider version changes, update `universal_rebuild/main.go`.
|
||||
|
||||
## One-time GPG bootstrap (do this once, keep the key stable)
|
||||
|
||||
1) Generate and export keys (no passphrase):
|
||||
```bash
|
||||
GPG_DIR=${ROOT_DIR}/secrets
|
||||
GNUPGHOME=$(mktemp -d)
|
||||
cat > /tmp/gpg_batch <<'EOF'
|
||||
%no-protection
|
||||
Key-Type: RSA
|
||||
Key-Length: 4096
|
||||
Subkey-Type: RSA
|
||||
Subkey-Length: 4096
|
||||
Name-Real: tazet@narod.ru
|
||||
Name-Email: tazet@narod.ru
|
||||
Expire-Date: 0
|
||||
EOF
|
||||
gpg --batch --homedir "$GNUPGHOME" --gen-key /tmp/gpg_batch
|
||||
gpg --batch --homedir "$GNUPGHOME" --armor --export-secret-keys > "$GPG_DIR/private_key.asc"
|
||||
gpg --batch --homedir "$GNUPGHOME" --armor --export > "$GPG_DIR/public_key.asc"
|
||||
rm -rf "$GNUPGHOME" /tmp/gpg_batch
|
||||
```
|
||||
|
||||
2) Update registry server public key (ASCII Armor) in:
|
||||
- `registry-server-build/main.go`
|
||||
- `operator/cmd/registry/main.go`
|
||||
|
||||
3) Rebuild and redeploy the registry server (see `docs/50_history/00_system_mechanics.md`).
|
||||
|
||||
4) Build and upload provider artifacts as usual.
|
||||
|
||||
# check string
|
||||
- **Не использовать** легаси-схемы версий (`prod=2.*`, `dev=3.*`, `test=5.*`, `0.0.1`).
|
||||
- **Не использовать** закрытые API и хосты: `index.cfm`, `registry.kube5s.ru`, `deck-api.ngcloud.ru`.
|
||||
- **Не вызывать** старые бинарники из `TOOLS/*/bin/` — скрипты пересобирают генераторы сами.
|
||||
- **Не перегенерировать GPG-ключ** подписи — сломается `terraform init` у пользователей.
|
||||
- **Не путать бакеты:** бинарники `nubes-terraform-registry`, документация `terraform-registry`.
|
||||
- **Не опираться** на файлы с баннером ⛔ в `NOTES/` и `HISTORY/` — это отменённые («ложные») пути.
|
||||
|
||||
@@ -212,3 +212,34 @@ From the unified YAML, generate:
|
||||
- No manual edits to generated YAML or generated Go code.
|
||||
- Any change must come from API or generator logic updates.
|
||||
- The generator must enforce these rules and fail fast on drift.
|
||||
|
||||
## Exception Registry (service-specific DATA, never logic)
|
||||
|
||||
Principle: provider core and generator logic are universal for all stands and
|
||||
services. The ONLY allowed deviations are DATA entries, and they MUST live in
|
||||
exactly two named registries:
|
||||
|
||||
| Registry | File | Declares |
|
||||
|---|---|---|
|
||||
| `serviceSpecificModifiers` | `TOOLS/yaml-generator/main.go` | which service `modify` op becomes a modifier resource and its name (key = normalized service name) |
|
||||
| `serviceSpecificDocExamples` | `TOOLS/docs-generator/internal/writers/writers.go` | per-service doc examples, gated on service name + required state/vault keys |
|
||||
|
||||
Rules:
|
||||
|
||||
- Key by stable service NAME (slug), never by raw numeric ID.
|
||||
- Each entry answers WHAT / WHAT IT DOES / WHY / WHERE (see code comments).
|
||||
- Adding an exception = editing one of these two registries → visible in diff.
|
||||
- Never annotate API-YAML: it is machine-regenerated and edits would be lost.
|
||||
|
||||
Enforced by scripts (run before build/commit):
|
||||
|
||||
- `TOOLS/scripts/check_generated_drift.sh <stand>` — generated Go vs provider copy.
|
||||
- `TOOLS/scripts/check_hardcoded_service_ids.sh` — forbids `svc.ID == N` /
|
||||
`ServiceID == N` outside the registries.
|
||||
|
||||
Build rule: `provider/internal/resources_gen` and `provider/resources_yaml` are
|
||||
ephemeral by design and never a build source. Canonical build is
|
||||
`03_build_and_upload_provider.sh` (temp copy from `generated/<stand>/go`);
|
||||
`build-provider.sh` refuses direct build from `provider/`. For local IDE,
|
||||
`go build` and `go test`, materialize one stand first:
|
||||
`TOOLS/scripts/dev-materialize.sh <stand>` (output is git-ignored).
|
||||
|
||||
@@ -24,6 +24,25 @@ cd resource-generator && go build -o ../bin/resource-generator .
|
||||
|
||||
Структура: `main.go` + `internal/{helpers,loader,params,templates,types,writers}`.
|
||||
|
||||
### Как запускать правильно
|
||||
|
||||
Не запускайте `TOOLS/resource-generator/bin/resource-generator` вручную и не полагайтесь на старый бинарник из `TOOLS/resource-generator/bin/`.
|
||||
|
||||
Используйте канонический скрипт из корня репозитория, он **всегда** пересобирает генераторы из текущих исходников перед запуском:
|
||||
|
||||
```bash
|
||||
./TOOLS/scripts/02_generate_resources_and_docs_v2.sh --profile TOOLS/config/dev
|
||||
```
|
||||
|
||||
Для других стендов подставляйте нужный профиль:
|
||||
|
||||
```bash
|
||||
./TOOLS/scripts/02_generate_resources_and_docs_v2.sh --profile TOOLS/config/test
|
||||
./TOOLS/scripts/02_generate_resources_and_docs_v2.sh --profile TOOLS/config/prod
|
||||
```
|
||||
|
||||
Это устраняет случайный запуск устаревшего бинаря и гарантирует, что новые `kind` из YAML, включая `modifier`, будут обработаны текущим кодом генератора.
|
||||
|
||||
## docs-generator
|
||||
|
||||
`resources_yaml/*.yaml` → Markdown-документация в `docs/30_registry/resources/`
|
||||
|
||||
@@ -4,7 +4,7 @@ TOKEN_FILE="secrets/dev.token"
|
||||
|
||||
# Release versions
|
||||
# Version
|
||||
VERSION="2.0.0"
|
||||
VERSION="2.0.17"
|
||||
|
||||
NAMESPACE="nubes-dev"
|
||||
PROVIDER_NAME="nubes"
|
||||
|
||||
@@ -6,17 +6,12 @@
|
||||
12 s3 # S3 Object Storage
|
||||
13 s3bucket # S3 бакет
|
||||
19 vcOrg # Организация в Cloud Director
|
||||
# 20 vcOrgSaas # Организация [DEPRECATED] — нет в UI
|
||||
21 vc_vdc # Виртуальный датацентр (vDC)
|
||||
22 vc_nsxt # Сетевой шлюз периметра (Edge)
|
||||
# 23 vc_vm # VM в Cloud Director (старый формат) (vc_vm) — нет в UI
|
||||
# 24 vcNat # DEPRECATED Правила маршрутизации для VM (vc_nat)
|
||||
25 vcexternalip # Публичные IP адреса
|
||||
26 vapp # Виртуальный каталог ВМ (vApp)
|
||||
# 27 vc_vm_v2 # VM в Cloud Director (vc_vmV2) — нет в DEV UI
|
||||
28 vc_vm_v3 # Виртуальная машина
|
||||
29 vcVdcGroup # Группа датацентров
|
||||
# 32 vmpostgre # vc_vm_postgresql_std
|
||||
50 nextcloud # Nextcloud
|
||||
81 superset # Apache Superset
|
||||
82 harbor # Container Registry
|
||||
@@ -34,13 +29,8 @@
|
||||
97 nodered # NodeRed
|
||||
98 http # Простой HTTP контейнер
|
||||
99 gitea # Gitea
|
||||
# 100 openwhisk # Serverless Openwhisk — нет в UI
|
||||
109 zonesV2 # Управление DNS
|
||||
# 110 dnszone # DNS зона — нет в UI
|
||||
111 dnsrecord # DNS запись
|
||||
# 112 tenant # Тенант в Grafana — нет в UI
|
||||
# 113 vcComplex # Быстрый старт — нет в UI
|
||||
# 114 GiteaComplex # Комплексная услуга по созданию gitea — нет в UI
|
||||
115 mariadb # Mariadb
|
||||
116 kafka # ApacheKafka
|
||||
# 117 nifi # Nifi
|
||||
@@ -50,6 +40,5 @@
|
||||
149 valoTenant # VALO Cloud
|
||||
150 k8sSthutrvalCluster # Kubernetes кластер Штурвал
|
||||
151 k8sOpenbao # Vault
|
||||
# 153 nifi # Nifi (DEV)
|
||||
153 nifi # Nifi (DEV)
|
||||
163 llmAi # LLM
|
||||
# 175 k8sGo # Go — нет в UI
|
||||
|
||||
@@ -376,7 +376,7 @@ func buildCreateParamsPage(spec types.ServiceSpec, nav string, version string) s
|
||||
// Якорь-ссылка для map-fixed/array-map-fixed с вложенными параметрами
|
||||
if p.HasSubParams && len(p.SubParams) > 0 && desc == "" {
|
||||
anchor := strings.ToLower(p.Code)
|
||||
if p.IsJson {
|
||||
if isNestedListParam(p) {
|
||||
desc = fmt.Sprintf("[Развернуть ↓](#%s-array-map-fixed)", anchor)
|
||||
} else {
|
||||
desc = fmt.Sprintf("[Развернуть ↓](#%s-map-fixed)", anchor)
|
||||
@@ -476,18 +476,46 @@ func buildOutputsPage(spec types.ServiceSpec, nav string, version string) string
|
||||
b.WriteString("\n")
|
||||
}
|
||||
|
||||
if spec.ServiceID == 90 && containsString(snapshot.OutPaths, "internalConnect.master") && containsString(snapshot.VaultFields, "adminUser") && containsString(snapshot.VaultFields, "adminPass") {
|
||||
b.WriteString("### Пример для связки с Lucee/NodeJS\n\n")
|
||||
b.WriteString("```hcl\n")
|
||||
b.WriteString("testds_connectionString = \"jdbc:postgresql://${nubes_postgres.db2.state_out_flat[\"internalConnect.master\"]}:5432/postgres?sslmode=require\"\n")
|
||||
b.WriteString("testds_username = nubes_postgres.db2.vault_secrets[\"adminUser\"]\n")
|
||||
b.WriteString("testds_password = nubes_postgres.db2.vault_secrets[\"adminPass\"]\n")
|
||||
b.WriteString("```\n")
|
||||
for _, ex := range serviceSpecificDocExamples {
|
||||
if spec.Name == ex.serviceName && hasAllKeys(snapshot.OutPaths, ex.outKeys) && hasAllKeys(snapshot.VaultFields, ex.vaultKeys) {
|
||||
b.WriteString("### Пример для связки с Lucee/NodeJS\n\n")
|
||||
b.WriteString("```hcl\n")
|
||||
b.WriteString("testds_connectionString = \"jdbc:postgresql://${nubes_postgres.db2.state_out_flat[\"internalConnect.master\"]}:5432/postgres?sslmode=require\"\n")
|
||||
b.WriteString("testds_username = nubes_postgres.db2.vault_secrets[\"adminUser\"]\n")
|
||||
b.WriteString("testds_password = nubes_postgres.db2.vault_secrets[\"adminPass\"]\n")
|
||||
b.WriteString("```\n")
|
||||
}
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// serviceSpecificDocExamples — реестр исключений документации (ДАННЫЕ, не логика).
|
||||
//
|
||||
// ЧТО: postgres → дополнительный HCL-пример связки с Lucee/NodeJS.
|
||||
// ЧТО ДЕЛАЕТ: если сервис и требуемые ключи state_out_flat/vault_secrets совпали — рендерит пример.
|
||||
// ПОЧЕМУ: специфичная интеграция postgres↔Lucee/NodeJS, универсальный рендер её не покрывает.
|
||||
// ГДЕ: TOOLS/ARCHITECTURE.md, раздел «Реестр исключений».
|
||||
//
|
||||
// Добавлять только здесь. Grep-гейт TOOLS/scripts/check_hardcoded_service_ids.sh
|
||||
// запрещает сравнения ServiceID == N вне этого файла.
|
||||
var serviceSpecificDocExamples = []struct {
|
||||
serviceName string
|
||||
outKeys []string
|
||||
vaultKeys []string
|
||||
}{
|
||||
{serviceName: "postgres", outKeys: []string{"internalConnect.master"}, vaultKeys: []string{"adminUser", "adminPass"}},
|
||||
}
|
||||
|
||||
func hasAllKeys(haystack []string, needles []string) bool {
|
||||
for _, n := range needles {
|
||||
if !containsString(haystack, n) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func containsString(values []string, target string) bool {
|
||||
for _, value := range values {
|
||||
if value == target {
|
||||
@@ -708,11 +736,15 @@ func formatParamOrBlock(p types.ParamSpec, indent string, requiredOnly bool) str
|
||||
if requiredOnly && !p.Required {
|
||||
return ""
|
||||
}
|
||||
// Для map-fixed генерируем вложенный HCL-блок
|
||||
if p.HasSubParams && len(p.SubParams) > 0 && !p.IsJson {
|
||||
// Для map-fixed генерируем HCL-АРГУМЕНТ объекта: `param = { ... }`.
|
||||
// ВАЖНО: resource-generator объявляет такие поля как schema.SingleNestedAttribute,
|
||||
// а SingleNestedAttribute в HCL — это аргумент (через `=`), НЕ блок.
|
||||
// Блочный синтаксис (`param { ... }`) ломает terraform validate:
|
||||
// "Unsupported block type ... Did you mean to define argument?".
|
||||
if p.HasSubParams && len(p.SubParams) > 0 && !isNestedListParam(p) {
|
||||
paramCode := ToSnake(p.Code)
|
||||
var b strings.Builder
|
||||
b.WriteString(fmt.Sprintf("%s%s {\n", indent, paramCode))
|
||||
b.WriteString(fmt.Sprintf("%s%s = {\n", indent, paramCode))
|
||||
for _, sp := range p.SubParams {
|
||||
spVal := sampleValue(sp)
|
||||
if isJsonType(sp) {
|
||||
@@ -732,12 +764,14 @@ func formatParamOrBlock(p types.ParamSpec, indent string, requiredOnly bool) str
|
||||
b.WriteString(fmt.Sprintf("%s}\n", indent))
|
||||
return b.String()
|
||||
}
|
||||
// Для array-map-fixed генерируем dynamic блок
|
||||
if p.HasSubParams && len(p.SubParams) > 0 && p.IsJson {
|
||||
// array-map-fixed: resource-generator объявляет такие поля как schema.StringAttribute
|
||||
// (JSON-строка), поэтому в доке это тоже аргумент-строка через jsonencode([...]),
|
||||
// а НЕ блок и НЕ dynamic-блок.
|
||||
if p.HasSubParams && len(p.SubParams) > 0 && isNestedListParam(p) {
|
||||
paramCode := ToSnake(p.Code)
|
||||
var b strings.Builder
|
||||
b.WriteString(fmt.Sprintf("%s%s {\n", indent, paramCode))
|
||||
b.WriteString(fmt.Sprintf("%s # Каждый элемент массива — объект с полями:\n", indent))
|
||||
b.WriteString(fmt.Sprintf("%s%s = jsonencode([\n", indent, paramCode))
|
||||
b.WriteString(fmt.Sprintf("%s {\n", indent))
|
||||
for _, sp := range p.SubParams {
|
||||
spVal := sampleValue(sp)
|
||||
if isJsonType(sp) {
|
||||
@@ -749,12 +783,13 @@ func formatParamOrBlock(p types.ParamSpec, indent string, requiredOnly bool) str
|
||||
spComment = strings.TrimSpace(stripHTML(sp.Man))
|
||||
}
|
||||
if spComment != "" {
|
||||
b.WriteString(fmt.Sprintf("%s %s = %s # %s\n", indent, spCode, spVal, spComment))
|
||||
b.WriteString(fmt.Sprintf("%s %s = %s # %s\n", indent, spCode, spVal, spComment))
|
||||
} else {
|
||||
b.WriteString(fmt.Sprintf("%s %s = %s\n", indent, spCode, spVal))
|
||||
b.WriteString(fmt.Sprintf("%s %s = %s\n", indent, spCode, spVal))
|
||||
}
|
||||
}
|
||||
b.WriteString(fmt.Sprintf("%s}\n", indent))
|
||||
b.WriteString(fmt.Sprintf("%s },\n", indent))
|
||||
b.WriteString(fmt.Sprintf("%s])\n", indent))
|
||||
return b.String()
|
||||
}
|
||||
return formatParamLine(p, indent, requiredOnly)
|
||||
@@ -762,6 +797,14 @@ func formatParamOrBlock(p types.ParamSpec, indent string, requiredOnly bool) str
|
||||
|
||||
func sampleValue(p types.ParamSpec) string {
|
||||
if hasDefault(p.Default) {
|
||||
// ВАЖНО: тип атрибута в схеме важнее «вида» значения в YAML.
|
||||
// YAML-парсер отдаёт `default: 81.22.46.22` как float64, и formatLiteral
|
||||
// печатает его без кавычек — а HCL не может распарсить такой литерал:
|
||||
// Error: Invalid number literal (Failed to recognize the value of this number literal)
|
||||
// Для строковых параметров значение обязано быть в кавычках.
|
||||
if isStringParam(p) {
|
||||
return strconv.Quote(fmt.Sprintf("%v", p.Default))
|
||||
}
|
||||
return formatLiteral(p.Default)
|
||||
}
|
||||
if len(p.ValueList) > 0 {
|
||||
@@ -779,6 +822,11 @@ func sampleValue(p types.ParamSpec) string {
|
||||
return "\"TODO\""
|
||||
}
|
||||
|
||||
// isStringParam — параметр является строкой по схеме (data_type: string / varchar).
|
||||
func isStringParam(p types.ParamSpec) bool {
|
||||
return strings.Contains(strings.ToLower(firstType(p)), "string")
|
||||
}
|
||||
|
||||
func formatLiteral(value interface{}) string {
|
||||
switch v := value.(type) {
|
||||
case bool:
|
||||
@@ -860,6 +908,22 @@ func isJsonType(p types.ParamSpec) bool {
|
||||
return strings.Contains(strings.ToLower(firstType(p)), "json")
|
||||
}
|
||||
|
||||
// isNestedListParam — аналог resource-generator helpers.IsNestedList.
|
||||
//
|
||||
// Параметр с подполями и типом-массивом (data_type: array-map-fixed).
|
||||
// resource-generator объявляет такие поля как schema.StringAttribute (JSON-строка),
|
||||
// а НЕ как SingleNestedAttribute, поэтому и в доке это строка:
|
||||
//
|
||||
// storage_config = jsonencode([{ ... }])
|
||||
//
|
||||
// Историческая ошибка: условие проверяло p.IsJson, но array-map-fixed приходит
|
||||
// с data_type=array-map-fixed (is_json не выставлен), поэтому поле попадало
|
||||
// в ветку map-fixed и рендерилось объектом — несовместимо со схемой.
|
||||
func isNestedListParam(p types.ParamSpec) bool {
|
||||
dtype := strings.ToLower(strings.TrimSpace(p.DataType) + " " + strings.TrimSpace(p.Type))
|
||||
return strings.Contains(dtype, "array")
|
||||
}
|
||||
|
||||
func defaultCell(value interface{}) string {
|
||||
if !hasDefault(value) {
|
||||
return ""
|
||||
@@ -1088,7 +1152,7 @@ func renderNestedParams(p types.ParamSpec) string {
|
||||
}
|
||||
var b strings.Builder
|
||||
label := p.Code
|
||||
if p.IsJson {
|
||||
if isNestedListParam(p) {
|
||||
label += " (array-map-fixed) — элемент"
|
||||
} else {
|
||||
label += " (map-fixed)"
|
||||
|
||||
+17
-1
@@ -41,9 +41,25 @@ type OperationSpec struct {
|
||||
ID int `yaml:"id"`
|
||||
Kind string `yaml:"kind"`
|
||||
Action string `yaml:"action"`
|
||||
Modifier string `yaml:"modifier,omitempty"`
|
||||
Subresource string `yaml:"subresource,omitempty"`
|
||||
Man string `yaml:"man,omitempty"`
|
||||
Params []ParamSpec `yaml:"params"`
|
||||
// DeleteStrategy — стратегия Delete для modifier-операций: noop_warn | inverse | error.
|
||||
// Пусто → noop_warn (remove из state + AddWarning).
|
||||
DeleteStrategy string `yaml:"delete_strategy,omitempty"`
|
||||
// Idempotency — pre-check перед run для modifier: none | check_before_run.
|
||||
// Пусто → none.
|
||||
Idempotency string `yaml:"idempotency,omitempty"`
|
||||
// DeleteParams — обратные значения (wire-строки) только при delete_strategy: inverse.
|
||||
DeleteParams []DeleteParam `yaml:"delete_params,omitempty"`
|
||||
Params []ParamSpec `yaml:"params"`
|
||||
}
|
||||
|
||||
// DeleteParam — обратное значение параметра для inverse-Delete.
|
||||
// Value — финальная wire-строка (для bool "false", для json готовый JSON).
|
||||
type DeleteParam struct {
|
||||
Code string `yaml:"code"`
|
||||
Value string `yaml:"value"`
|
||||
}
|
||||
|
||||
// ParamSpec — параметр операции.
|
||||
|
||||
Binary file not shown.
@@ -74,6 +74,55 @@ func ParamDefaultExpr(p types.Param) string {
|
||||
}
|
||||
}
|
||||
|
||||
// ShouldBeOptionalComputed — обязан ли параметр быть объявлен в схеме как
|
||||
// `Optional: true, Computed: true` (при отсутствии явного Default).
|
||||
//
|
||||
// ОБЩИЙ ИНВАРИАНТ ПРОВАЙДЕРА (не привязан к конкретному сервису или параметру):
|
||||
// каждый скалярный параметр, который провайдер читает ОБРАТНО из state_params
|
||||
// инстанса и записывает в state (см. RefreshResourceState + InputField в шаблоне
|
||||
// instance.go), обязан быть Optional+Computed, если он не Required и без Default.
|
||||
//
|
||||
// ПОЧЕМУ ИНАЧЕ ЛОМАЕТСЯ:
|
||||
// - Пользователь не задал опциональный параметр → в плане он = null.
|
||||
// - Платформа подставляет своё значение (например дефолтный QoS-профиль
|
||||
// "QoS-100Mbit" для vc_nsxt) → провайдер кладёт его в state при apply.
|
||||
// - Terraform видит расхождение plan(null) ≠ state(значение) и падает:
|
||||
// "Provider produced inconsistent result after apply: .qos_profile:
|
||||
// was null, but now cty.StringVal("QoS-100Mbit")".
|
||||
//
|
||||
// ПОЧЕМУ Computed ЭТО ЧИНИТ:
|
||||
// - С `Computed: true` незаданный параметр в плане = unknown (не null),
|
||||
// и провайдер имеет право заполнить его значением из API — это штатная
|
||||
// семантика Optional+Computed, а не обходной путь.
|
||||
//
|
||||
// УСЛОВИЯ, ПРИ КОТОРЫХ Computed НЕ НУЖЕН:
|
||||
// - Required: значение всегда задано пользователем → план уже известен;
|
||||
// - RefSvcId != 0: refSvc-параметры НЕ читаются обратно в модель — они
|
||||
// резолвятся только для API-вызова, а в state остаётся ровно то, что
|
||||
// написал пользователь (иначе получим plan(display name) ≠ state(UUID));
|
||||
// - Default != "": атрибут уже объявлен Computed+Default, значение известно.
|
||||
func ShouldBeOptionalComputed(p types.Param) bool {
|
||||
return !p.Required && p.RefSvcId == 0 && p.Default == ""
|
||||
}
|
||||
|
||||
// ShouldUseStateForUnknown решает, нужен ли UseStateForUnknown() plan-modifier
|
||||
// для скалярного параметра.
|
||||
//
|
||||
// Зачем: Optional+Computed параметр БЕЗ Default при незаданном значении в плане
|
||||
// = unknown. Если Update не перезаписывает его (в т.ч. ветка no-op с ранним
|
||||
// return, см. instance.go), unknown протекает в state и Terraform падает с
|
||||
// "Provider produced invalid result object after apply: ... unknown".
|
||||
// UseStateForUnknown схлопывает unknown в предыдущее known-значение из state,
|
||||
// не требуя сетевого вызова.
|
||||
//
|
||||
// НЕ применяем к JSON (у них свой JsonNormalize) и к полям с Default/Required.
|
||||
func ShouldUseStateForUnknown(p types.Param) bool {
|
||||
if p.IsJson {
|
||||
return false
|
||||
}
|
||||
return ShouldBeOptionalComputed(p)
|
||||
}
|
||||
|
||||
// ParamFormat возвращает вызов Format* для форматирования значения параметра.
|
||||
func ParamFormat(p types.Param, varName string) string {
|
||||
switch strings.ToLower(p.Type) {
|
||||
|
||||
@@ -22,13 +22,15 @@ import (
|
||||
|
||||
"resource-generator/internal/params"
|
||||
"resource-generator/internal/types"
|
||||
"tf-tools/lib"
|
||||
)
|
||||
|
||||
// LoadSpecs загружает все YAML-спеки из директории и строит GenResource/GenSubresource/GenAction.
|
||||
func LoadSpecs(dir string) ([]types.GenResource, []types.GenSubresource, []types.GenAction, error) {
|
||||
// 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 {
|
||||
@@ -56,6 +58,32 @@ func LoadSpecs(dir string) ([]types.GenResource, []types.GenSubresource, []types
|
||||
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
|
||||
}
|
||||
@@ -104,12 +132,16 @@ func LoadSpecs(dir string) ([]types.GenResource, []types.GenSubresource, []types
|
||||
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)
|
||||
gr.NeedsStringsImport = params.HasRestoreCasingParams(gr.SchemaParams) || params.AnalyzeNeedsStrings(gr.ModifyParams) || params.AnalyzeNeedsStrings(gr.CreateOnlyRequiredParams) || params.AnalyzeNeedsStrings(gr.CreateOnlyParams)
|
||||
// Строковый 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 попал в слайс) ---
|
||||
@@ -192,7 +224,7 @@ func LoadSpecs(dir string) ([]types.GenResource, []types.GenSubresource, []types
|
||||
})
|
||||
|
||||
if walkErr != nil {
|
||||
return nil, nil, nil, walkErr
|
||||
return nil, nil, nil, nil, walkErr
|
||||
}
|
||||
|
||||
domainServiceIDs := make([]int, 0, len(domainServiceIDsSet))
|
||||
@@ -221,8 +253,14 @@ func LoadSpecs(dir string) ([]types.GenResource, []types.GenSubresource, []types
|
||||
}
|
||||
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, nil
|
||||
return services, subs, actions, modifiers, nil
|
||||
}
|
||||
|
||||
// ConvertParams конвертирует []ParamSpec → []Param с нормализацией типов.
|
||||
@@ -257,6 +295,7 @@ func ConvertParams(params []types.ParamSpec) []types.Param {
|
||||
IsJson: strings.EqualFold(strings.TrimSpace(p.DataType), "json"),
|
||||
HasSubParams: isNested,
|
||||
SubParams: subParams,
|
||||
IsModifiable: p.IsModifiable,
|
||||
})
|
||||
}
|
||||
return out
|
||||
@@ -313,6 +352,7 @@ var KnownKinds = map[string]bool{
|
||||
"instance": true,
|
||||
"subresource": true,
|
||||
"action": true,
|
||||
"modifier": true,
|
||||
}
|
||||
|
||||
// ValidateSpec проверяет YAML-спек на обязательные поля и неизвестные kinds.
|
||||
@@ -329,7 +369,7 @@ func ValidateSpec(path string, spec *types.ServiceSpec) error {
|
||||
return fmt.Errorf("operation[%d] %q: missing required field: kind", i, op.Name)
|
||||
}
|
||||
if !KnownKinds[op.Kind] {
|
||||
return fmt.Errorf("operation[%d] %q: unknown kind %q (valid: instance, subresource, action)", i, op.Name, op.Kind)
|
||||
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)
|
||||
@@ -337,6 +377,97 @@ func ValidateSpec(path string, spec *types.ServiceSpec) error {
|
||||
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,59 @@
|
||||
package loader
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"resource-generator/internal/types"
|
||||
"tf-tools/lib"
|
||||
)
|
||||
|
||||
func TestValidateSpecModifier(t *testing.T) {
|
||||
mkModifier := func(ds string, idem string, dps []lib.DeleteParam, params []lib.ParamSpec) *types.ServiceSpec {
|
||||
return &types.ServiceSpec{
|
||||
Name: "vc_nsxt",
|
||||
ServiceID: 22,
|
||||
Operations: []lib.OperationSpec{
|
||||
{
|
||||
Name: "modify",
|
||||
ID: 111,
|
||||
Kind: "modifier",
|
||||
Action: "modify",
|
||||
Modifier: "network",
|
||||
DeleteStrategy: ds,
|
||||
Idempotency: idem,
|
||||
DeleteParams: dps,
|
||||
Params: params,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
spec *types.ServiceSpec
|
||||
want bool // want error (true) or OK (false)
|
||||
}{
|
||||
{"валидный inverse", mkModifier("inverse", "none",
|
||||
[]lib.DeleteParam{{Code: "needEnableAVI", Value: "false"}},
|
||||
[]lib.ParamSpec{{Code: "needEnableAVI", Required: false}}), false},
|
||||
{"неизвестный delete_strategy", mkModifier("bogus", "none", nil, nil), true},
|
||||
{"inverse без delete_params", mkModifier("inverse", "none", nil,
|
||||
[]lib.ParamSpec{{Code: "needEnableAVI"}}), true},
|
||||
{"delete_params.code вне params", mkModifier("inverse", "none",
|
||||
[]lib.DeleteParam{{Code: "missing", Value: "false"}},
|
||||
[]lib.ParamSpec{{Code: "needEnableAVI"}}), true},
|
||||
{"неизвестный idempotency", mkModifier("noop_warn", "bogus", nil, nil), true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := ValidateSpec("test.yaml", tt.spec)
|
||||
if tt.want && err == nil {
|
||||
t.Errorf("ожидали ошибку, получили nil")
|
||||
}
|
||||
if !tt.want && err != nil {
|
||||
t.Errorf("ожидали OK, получили: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -111,6 +111,8 @@ func AlignParamTypes(params []types.Param, schema []types.Param) []types.Param {
|
||||
}
|
||||
|
||||
// ComputeCreateOnly вычисляет набор полей, которые есть только в create (не в modify).
|
||||
// Канон: параметр изменяемый (is_modifiable == true) → НЕ create-only,
|
||||
// потому что «можно менять в UI → можно менять в Terraform».
|
||||
func ComputeCreateOnly(createParams []types.Param, modifyParams []types.Param) map[string]struct{} {
|
||||
modifyCodes := make(map[string]struct{}, len(modifyParams))
|
||||
for _, p := range modifyParams {
|
||||
@@ -127,6 +129,10 @@ func ComputeCreateOnly(createParams []types.Param, modifyParams []types.Param) m
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
// is_modifiable:true → параметр можно менять → не create-only.
|
||||
if p.IsModifiable != nil && *p.IsModifiable {
|
||||
continue
|
||||
}
|
||||
if _, ok := modifyCodes[key]; !ok {
|
||||
createOnly[key] = struct{}{}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,12 @@ import (
|
||||
"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"
|
||||
)
|
||||
@@ -70,7 +76,9 @@ type {{ToCamel .Name}}Model struct {
|
||||
{{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}}"` + "`" + `
|
||||
@@ -104,10 +112,10 @@ func (r *{{ToCamel .Name}}Resource) Schema(ctx context.Context, req resource.Sch
|
||||
{{- 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 (gt .RefSvcId 0) }}Computed: 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()},{{- 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 }}
|
||||
@@ -117,7 +125,9 @@ func (r *{{ToCamel .Name}}Resource) Schema(ctx context.Context, req resource.Sch
|
||||
// Если не задано — 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 .) }}
|
||||
@@ -150,7 +160,9 @@ func (r *{{ToCamel .Name}}Resource) ModifyPlan(ctx context.Context, req resource
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
if req.State.Raw.IsNull() && req.Plan.Raw.IsNull() {
|
||||
// Destroy-план (plan == null): create-time проверку «уже существует / adopt»
|
||||
// запускать нельзя — удаление не валидируется через существование инстанса.
|
||||
if req.Plan.Raw.IsNull() {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -212,50 +224,16 @@ func (r *{{ToCamel .Name}}Resource) ModifyPlan(ctx context.Context, req resource
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
|
||||
return
|
||||
}
|
||||
adoptExistingOnCreate := false
|
||||
if !config.AdoptExistingOnCreate.IsNull() && !config.AdoptExistingOnCreate.IsUnknown() {
|
||||
adoptExistingOnCreate = config.AdoptExistingOnCreate.ValueBool()
|
||||
}
|
||||
{{- if .HasRefSvcParams }}
|
||||
{{- range .SchemaParams }}
|
||||
{{- if and (gt .RefSvcId 0) (eq (ParamType .) "types.String") }}
|
||||
if !config.{{ToCamel .Code}}.IsNull() && !config.{{ToCamel .Code}}.IsUnknown() {
|
||||
resolved{{ToCamel .Code}}, err := r.client.ResolveRefSvcParamValue(ctx, {{.RefSvcId}}, config.{{ToCamel .Code}}.ValueString())
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddWarning("Failed to resolve {{ToSnake .Code}}", err.Error())
|
||||
} else if resolved{{ToCamel .Code}} != "" && resolved{{ToCamel .Code}} != config.{{ToCamel .Code}}.ValueString() {
|
||||
config.{{ToCamel .Code}} = types.StringValue(resolved{{ToCamel .Code}})
|
||||
}
|
||||
}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
params := map[int]string{
|
||||
{{- range .CreateParams }}
|
||||
{{- if not (IsNested .) }}
|
||||
{{.ID}}: {{ParamFormat . (printf "config.%s" (ToCamel .Code))}},
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
}
|
||||
{{- range .CreateParams }}
|
||||
{{- if (IsNested .) }}
|
||||
if config.{{ToCamel .Code}} != nil {
|
||||
params[{{.ID}}] = {{NestedJSONExpr . "config"}}
|
||||
}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
desiredDomain := ""
|
||||
{{- if .HasDomainParam }}
|
||||
if !config.Domain.IsNull() && !config.Domain.IsUnknown() {
|
||||
desiredDomain = config.Domain.ValueString()
|
||||
}
|
||||
{{- end }}
|
||||
domainServiceIDs := []int{ {{- range .DomainServiceIDs }}{{.}}, {{- end }} }
|
||||
|
||||
resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, {{.ServiceID}}, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs)...)
|
||||
// ⛔ 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) {
|
||||
@@ -273,46 +251,23 @@ func (r *{{ToCamel .Name}}Resource) Create(ctx context.Context, req resource.Cre
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- if .HasRefSvcParams }}
|
||||
{{- range .SchemaParams }}
|
||||
{{- if and (gt .RefSvcId 0) (ne .RefSvcId 12) (eq (ParamType .) "types.String") }}
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
// ПРАВИЛО TERRAFORM (официальная документация):
|
||||
// «If an attribute value is configured, it is NEVER valid to change that
|
||||
// value in the plan.» — то есть plan ОБЯЗАН равняться config (= тому что
|
||||
// написал пользователь). Менять plan запрещено на уровне фреймворка.
|
||||
//
|
||||
// ПРОБЛЕМА: API нашего облака возвращает UUID в нижнем регистре.
|
||||
// Пользователь пишет: vapp_uid = "6214BA32-..." (верхний или смешанный).
|
||||
// После apply API вернул: "6214ba32-..." → state != plan → Terraform кричит:
|
||||
// «Provider produced inconsistent result after apply».
|
||||
//
|
||||
// РЕШЕНИЕ: корректировать STATE под PLAN, а не наоборот.
|
||||
// Шаг 1 (здесь): сохраняем оригинальное значение из plan ДО того как
|
||||
// ResolveRefSvcParamValue переведёт UUID в нижний регистр (нужен для API).
|
||||
// Шаг 2 (ниже, после RefreshResourceState): восстанавливаем оригинальный
|
||||
// регистр в state через strings.EqualFold (сравниваем без учёта регистра).
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
original{{ToCamel .Code}} := data.{{ToCamel .Code}}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- if .HasRefSvcParams }}
|
||||
{{- range .SchemaParams }}
|
||||
{{- if and (gt .RefSvcId 0) (ne .RefSvcId 12) (eq (ParamType .) "types.String") }}
|
||||
{{- 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() {
|
||||
resolved{{ToCamel .Code}}, err := r.client.ResolveRefSvcParamValue(ctx, {{.RefSvcId}}, data.{{ToCamel .Code}}.ValueString())
|
||||
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
|
||||
}
|
||||
if resolved{{ToCamel .Code}} != "" && resolved{{ToCamel .Code}} != data.{{ToCamel .Code}}.ValueString() {
|
||||
data.{{ToCamel .Code}} = types.StringValue(resolved{{ToCamel .Code}})
|
||||
}
|
||||
}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
resourceName := data.ResourceName.ValueString()
|
||||
desiredDomain := ""
|
||||
{{- if .HasDomainParam }}
|
||||
@@ -321,7 +276,7 @@ func (r *{{ToCamel .Name}}Resource) Create(ctx context.Context, req resource.Cre
|
||||
}
|
||||
{{- end }}
|
||||
domainServiceIDs := []int{ {{- range .DomainServiceIDs }}{{.}}, {{- end }} }
|
||||
resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, {{.ServiceID}}, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs)...)
|
||||
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() {
|
||||
@@ -331,9 +286,13 @@ func (r *{{ToCamel .Name}}Resource) Create(ctx context.Context, req resource.Cre
|
||||
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 .) }}
|
||||
@@ -363,7 +322,7 @@ func (r *{{ToCamel .Name}}Resource) Create(ctx context.Context, req resource.Cre
|
||||
{{- end }}
|
||||
}, []resources_core.InputField{
|
||||
{{- range .SchemaParams }}
|
||||
{{- if or (le .RefSvcId 0) (ne .RefSvcId 12) }}
|
||||
{{- if eq .RefSvcId 0 }}
|
||||
{Code: "{{.Code}}", Field: "{{ToCamel .Code}}", Type: "{{.Type}}"},
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -372,34 +331,6 @@ func (r *{{ToCamel .Name}}Resource) Create(ctx context.Context, req resource.Cre
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
{{- if .HasRefSvcParams }}
|
||||
{{- range .SchemaParams }}
|
||||
{{- if and (gt .RefSvcId 0) (ne .RefSvcId 12) (eq (ParamType .) "types.String") }}
|
||||
if !state.{{ToCamel .Code}}.IsNull() && !state.{{ToCamel .Code}}.IsUnknown() {
|
||||
resolved{{ToCamel .Code}}, err := r.client.ResolveRefSvcParamValue(ctx, {{.RefSvcId}}, state.{{ToCamel .Code}}.ValueString())
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddWarning("Failed to resolve {{ToSnake .Code}}", err.Error())
|
||||
} else if resolved{{ToCamel .Code}} != "" && resolved{{ToCamel .Code}} != state.{{ToCamel .Code}}.ValueString() {
|
||||
state.{{ToCamel .Code}} = types.StringValue(resolved{{ToCamel .Code}})
|
||||
}
|
||||
}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
// Restore user-provided casing в state (Create).
|
||||
// EqualFold = «совпадают ли значения без учёта регистра?»
|
||||
// Если да — значит API вернул «туже» строку, только в другом регистре.
|
||||
// Заменяем state на original (то что было в plan/config пользователя).
|
||||
// Итог: plan=="6214BA32-..." и state=="6214BA32-..." → нет diff → нет taint.
|
||||
{{- range .SchemaParams }}
|
||||
{{- if and (gt .RefSvcId 0) (ne .RefSvcId 12) (eq (ParamType .) "types.String") }}
|
||||
if !original{{ToCamel .Code}}.IsNull() && !original{{ToCamel .Code}}.IsUnknown() && !state.{{ToCamel .Code}}.IsNull() && !state.{{ToCamel .Code}}.IsUnknown() {
|
||||
if strings.EqualFold(state.{{ToCamel .Code}}.ValueString(), original{{ToCamel .Code}}.ValueString()) {
|
||||
state.{{ToCamel .Code}} = original{{ToCamel .Code}}
|
||||
}
|
||||
}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
|
||||
}
|
||||
@@ -431,7 +362,7 @@ func (r *{{ToCamel .Name}}Resource) Read(ctx context.Context, req resource.ReadR
|
||||
{{- end }}
|
||||
}, []resources_core.InputField{
|
||||
{{- range .SchemaParams }}
|
||||
{{- if or (le .RefSvcId 0) (ne .RefSvcId 12) }}
|
||||
{{- if eq .RefSvcId 0 }}
|
||||
{Code: "{{.Code}}", Field: "{{ToCamel .Code}}", Type: "{{.Type}}"},
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -440,34 +371,6 @@ func (r *{{ToCamel .Name}}Resource) Read(ctx context.Context, req resource.ReadR
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
{{- if .HasRefSvcParams }}
|
||||
{{- range .SchemaParams }}
|
||||
{{- if and (gt .RefSvcId 0) (ne .RefSvcId 12) (eq (ParamType .) "types.String") }}
|
||||
if !newState.{{ToCamel .Code}}.IsNull() && !newState.{{ToCamel .Code}}.IsUnknown() {
|
||||
resolved{{ToCamel .Code}}, err := r.client.ResolveRefSvcParamValue(ctx, {{.RefSvcId}}, newState.{{ToCamel .Code}}.ValueString())
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddWarning("Failed to resolve {{ToSnake .Code}}", err.Error())
|
||||
} else if resolved{{ToCamel .Code}} != "" && resolved{{ToCamel .Code}} != newState.{{ToCamel .Code}}.ValueString() {
|
||||
newState.{{ToCamel .Code}} = types.StringValue(resolved{{ToCamel .Code}})
|
||||
}
|
||||
}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
// Restore user-provided casing в state (Read).
|
||||
// При чтении у нас нет plan — но предыдущий state уже хранит значение
|
||||
// в регистре пользователя (после первого Create оно было восстановлено).
|
||||
// Берём prior state (переменная state) как эталон регистра.
|
||||
// Если API вернул то же UUID только строчными буквами — восстанавливаем.
|
||||
{{- range .SchemaParams }}
|
||||
{{- if and (gt .RefSvcId 0) (ne .RefSvcId 12) (eq (ParamType .) "types.String") }}
|
||||
if !state.{{ToCamel .Code}}.IsNull() && !state.{{ToCamel .Code}}.IsUnknown() && !newState.{{ToCamel .Code}}.IsNull() && !newState.{{ToCamel .Code}}.IsUnknown() {
|
||||
if strings.EqualFold(newState.{{ToCamel .Code}}.ValueString(), state.{{ToCamel .Code}}.ValueString()) {
|
||||
newState.{{ToCamel .Code}} = state.{{ToCamel .Code}}
|
||||
}
|
||||
}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
|
||||
}
|
||||
@@ -529,11 +432,31 @@ func (r *{{ToCamel .Name}}Resource) Update(ctx context.Context, req resource.Upd
|
||||
{{- 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
|
||||
{{- range .OutputParams }}
|
||||
plan.{{ToCamel .Code}} = state.{{ToCamel .Code}}
|
||||
{{- end }}
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
|
||||
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
|
||||
}
|
||||
|
||||
@@ -597,7 +520,7 @@ func (r *{{ToCamel .Name}}Resource) Update(ctx context.Context, req resource.Upd
|
||||
{{- end }}
|
||||
}, []resources_core.InputField{
|
||||
{{- range .SchemaParams }}
|
||||
{{- if or (le .RefSvcId 0) (ne .RefSvcId 12) }}
|
||||
{{- if eq .RefSvcId 0 }}
|
||||
{Code: "{{.Code}}", Field: "{{ToCamel .Code}}", Type: "{{.Type}}"},
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -606,33 +529,6 @@ func (r *{{ToCamel .Name}}Resource) Update(ctx context.Context, req resource.Upd
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
{{- if .HasRefSvcParams }}
|
||||
{{- range .SchemaParams }}
|
||||
{{- if and (gt .RefSvcId 0) (ne .RefSvcId 12) (eq (ParamType .) "types.String") }}
|
||||
if !state.{{ToCamel .Code}}.IsNull() && !state.{{ToCamel .Code}}.IsUnknown() {
|
||||
resolved{{ToCamel .Code}}, err := r.client.ResolveRefSvcParamValue(ctx, {{.RefSvcId}}, state.{{ToCamel .Code}}.ValueString())
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddWarning("Failed to resolve {{ToSnake .Code}}", err.Error())
|
||||
} else if resolved{{ToCamel .Code}} != "" && resolved{{ToCamel .Code}} != state.{{ToCamel .Code}}.ValueString() {
|
||||
state.{{ToCamel .Code}} = types.StringValue(resolved{{ToCamel .Code}})
|
||||
}
|
||||
}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
// Restore user-provided casing в state (Update).
|
||||
// После API-вызова modify state содержит значения в lower-case от API.
|
||||
// Plan == config == то что написал пользователь (регистр неизменён).
|
||||
// EqualFold: если UUID совпадает без учёта регистра — берём из plan.
|
||||
{{- range .SchemaParams }}
|
||||
{{- if and (gt .RefSvcId 0) (ne .RefSvcId 12) (eq (ParamType .) "types.String") }}
|
||||
if !plan.{{ToCamel .Code}}.IsNull() && !plan.{{ToCamel .Code}}.IsUnknown() && !state.{{ToCamel .Code}}.IsNull() && !state.{{ToCamel .Code}}.IsUnknown() {
|
||||
if strings.EqualFold(state.{{ToCamel .Code}}.ValueString(), plan.{{ToCamel .Code}}.ValueString()) {
|
||||
state.{{ToCamel .Code}} = plan.{{ToCamel .Code}}
|
||||
}
|
||||
}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
package templates
|
||||
|
||||
const Modifier = `package resources_gen
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"terraform-provider-nubes/internal/core"
|
||||
"terraform-provider-nubes/internal/resources_core"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
|
||||
{{- if .NeedsBoolDefault }}
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
|
||||
{{- end }}
|
||||
{{- if .NeedsInt64Default }}
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
|
||||
{{- end }}
|
||||
{{- if .NeedsStringDefault }}
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
|
||||
{{- end }}
|
||||
{{- if .NeedsJsonPlanMod }}
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
|
||||
{{- end }}
|
||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||
)
|
||||
|
||||
// Code generated by TOOLS/resource-generator. DO NOT EDIT.
|
||||
// Modifier: {{.ServiceName}}.{{.ModifierName}}
|
||||
|
||||
var _ resource.Resource = &{{ToCamel (printf "%s_%s" .ServiceName .ModifierName)}}Resource{}
|
||||
|
||||
type {{ToCamel (printf "%s_%s" .ServiceName .ModifierName)}}Resource struct {
|
||||
client *core.UniversalClient
|
||||
}
|
||||
|
||||
type {{ToCamel (printf "%s_%s" .ServiceName .ModifierName)}}Model struct {
|
||||
ID types.String {{bt}}tfsdk:"id"{{bt}}
|
||||
{{ToCamel .ServiceName}}ID types.String {{bt}}tfsdk:"{{ToSnake .ServiceName}}_id"{{bt}}
|
||||
OperationTimeout types.String {{bt}}tfsdk:"operation_timeout"{{bt}}
|
||||
LogLevel types.String {{bt}}tfsdk:"log_level"{{bt}}
|
||||
{{- range .SchemaParams }}
|
||||
{{ToCamel .Code}} {{ParamType .}} {{bt}}tfsdk:"{{ToSnake .Code}}"{{bt}}
|
||||
{{- end }}
|
||||
}
|
||||
|
||||
func New{{ToCamel (printf "%s_%s" .ServiceName .ModifierName)}}Resource() resource.Resource {
|
||||
return &{{ToCamel (printf "%s_%s" .ServiceName .ModifierName)}}Resource{}
|
||||
}
|
||||
|
||||
func (r *{{ToCamel (printf "%s_%s" .ServiceName .ModifierName)}}Resource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
|
||||
resp.TypeName = req.ProviderTypeName + "_{{.ServiceName}}_{{.ModifierName}}"
|
||||
}
|
||||
|
||||
func (r *{{ToCamel (printf "%s_%s" .ServiceName .ModifierName)}}Resource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
|
||||
attrs := map[string]schema.Attribute{
|
||||
"id": schema.StringAttribute{Computed: true},
|
||||
"{{ToSnake .ServiceName}}_id": schema.StringAttribute{Required: true},
|
||||
"operation_timeout": schema.StringAttribute{Optional: true},
|
||||
"log_level": schema.StringAttribute{Optional: true},
|
||||
{{- range .SchemaParams }}
|
||||
"{{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 .) "") }}Required: true,{{else}}Optional: true,{{end}}
|
||||
{{- if ne (ParamDefaultExpr .) "" }}Computed: true, Default: {{ParamDefaultExpr .}},{{- end }}
|
||||
{{- if ne (ParamDescription .) "" }}MarkdownDescription: {{ParamDescription .}},{{end}}
|
||||
{{- if .Sensitive }}Sensitive: true,{{end}}
|
||||
{{- if .IsJson }}PlanModifiers: []planmodifier.String{resources_core.JsonNormalize()},{{end}}
|
||||
},
|
||||
{{- end }}
|
||||
}
|
||||
resp.Schema = schema.Schema{Attributes: attrs}
|
||||
}
|
||||
|
||||
func (r *{{ToCamel (printf "%s_%s" .ServiceName .ModifierName)}}Resource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
|
||||
var plan {{ToCamel (printf "%s_%s" .ServiceName .ModifierName)}}Model
|
||||
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
|
||||
if resp.Diagnostics.HasError() { return }
|
||||
if err := r.reconcile(ctx, &plan, nil); err != nil {
|
||||
resp.Diagnostics.AddError("Ошибка клиента", err.Error()); return
|
||||
}
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
|
||||
}
|
||||
|
||||
func (r *{{ToCamel (printf "%s_%s" .ServiceName .ModifierName)}}Resource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
|
||||
var state {{ToCamel (printf "%s_%s" .ServiceName .ModifierName)}}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.{{ToCamel .ServiceName}}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.{{ToCamel .ServiceName}}ID.ValueString(), {{.ServiceID}}, state, nil, []resources_core.InputField{
|
||||
{{- range .SchemaParams }}
|
||||
{Code: "{{.Code}}", Field: "{{ToCamel .Code}}", Type: "{{.Type}}"},
|
||||
{{- end }}
|
||||
})
|
||||
resp.Diagnostics.Append(diags...)
|
||||
if resp.Diagnostics.HasError() { return }
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
|
||||
}
|
||||
|
||||
func (r *{{ToCamel (printf "%s_%s" .ServiceName .ModifierName)}}Resource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
|
||||
var plan {{ToCamel (printf "%s_%s" .ServiceName .ModifierName)}}Model
|
||||
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
|
||||
if resp.Diagnostics.HasError() { return }
|
||||
if err := r.reconcile(ctx, &plan, nil); err != nil {
|
||||
resp.Diagnostics.AddError("Ошибка клиента", err.Error()); return
|
||||
}
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
|
||||
}
|
||||
|
||||
// reconcile — единый путь apply для модификатора (полный payload, досылка в core).
|
||||
// override — обратные значения (wire-строки), используются только Delete=inverse.
|
||||
func (r *{{ToCamel (printf "%s_%s" .ServiceName .ModifierName)}}Resource) reconcile(ctx context.Context, model *{{ToCamel (printf "%s_%s" .ServiceName .ModifierName)}}Model, override map[string]string) error {
|
||||
instanceUID := strings.TrimSpace(model.{{ToCamel .ServiceName}}ID.ValueString())
|
||||
if instanceUID == "" { return fmt.Errorf("отсутствует идентификатор экземпляра") }
|
||||
params := map[string]string{}
|
||||
{{- range .SchemaParams }}
|
||||
{{- if .Required }}
|
||||
if model.{{ToCamel .Code}}.IsNull() || model.{{ToCamel .Code}}.IsUnknown() {
|
||||
return fmt.Errorf("{{ToSnake .Code}} is required")
|
||||
}
|
||||
params["{{.Code}}"] = {{ParamFormat . (printf "model.%s" (ToCamel .Code))}}
|
||||
{{- else }}
|
||||
if !model.{{ToCamel .Code}}.IsNull() && !model.{{ToCamel .Code}}.IsUnknown() {
|
||||
params["{{.Code}}"] = {{ParamFormat . (printf "model.%s" (ToCamel .Code))}}
|
||||
}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
for k, v := range override {
|
||||
params[k] = v
|
||||
}
|
||||
operationTimeout := ""
|
||||
if !model.OperationTimeout.IsNull() && !model.OperationTimeout.IsUnknown() { operationTimeout = model.OperationTimeout.ValueString() }
|
||||
if !model.LogLevel.IsNull() && !model.LogLevel.IsUnknown() { ctx = core.CtxWithLogLevel(ctx, model.LogLevel.ValueString()) }
|
||||
{{- if eq .Idempotency "check_before_run" }}
|
||||
if err := resources_core.RunOperationByCodeIdempotent(ctx, r.client, instanceUID, "{{.OperationName}}", params, operationTimeout); err != nil {
|
||||
return err
|
||||
}
|
||||
{{- else }}
|
||||
if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "{{.OperationName}}", params, operationTimeout); err != nil {
|
||||
return err
|
||||
}
|
||||
{{- end }}
|
||||
model.ID = types.StringValue(resources_core.BuildActionID(instanceUID, "{{.OperationName}}", "{{.ModifierName}}"))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *{{ToCamel (printf "%s_%s" .ServiceName .ModifierName)}}Resource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
|
||||
var state {{ToCamel (printf "%s_%s" .ServiceName .ModifierName)}}Model
|
||||
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
||||
if resp.Diagnostics.HasError() { return }
|
||||
{{- if eq .DeleteStrategy "error" }}
|
||||
resp.Diagnostics.AddError("Запрещено удалять", "Модификатор {{.ServiceName}}.{{.ModifierName}} нельзя удалять: эффект необратим. Обратитесь в техническую поддержку.")
|
||||
return
|
||||
{{- else if eq .DeleteStrategy "inverse" }}
|
||||
override := map[string]string{
|
||||
{{- range .DeleteParams }}
|
||||
"{{.Code}}": {{printf "%q" .Value}},
|
||||
{{- end }}
|
||||
}
|
||||
if err := r.reconcile(ctx, &state, override); err != nil {
|
||||
resp.Diagnostics.AddError("Ошибка клиента", err.Error()); return
|
||||
}
|
||||
{{- else }}
|
||||
resp.Diagnostics.AddWarning("Эффект остаётся на платформе", "Удаление модификатора {{.ServiceName}}.{{.ModifierName}} снимает его из state, но его эффект на платформе остаётся. Откатите вручную при необходимости.")
|
||||
{{- end }}
|
||||
}
|
||||
|
||||
func (r *{{ToCamel (printf "%s_%s" .ServiceName .ModifierName)}}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
|
||||
}
|
||||
`
|
||||
@@ -74,7 +74,7 @@ func (r *{{ToCamel (printf "%s_%s" .ServiceName .SubName)}}Resource) Schema(ctx
|
||||
{{- range .SchemaParams }}
|
||||
"{{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 (gt .RefSvcId 0) }}Computed: true,{{- end }}
|
||||
{{- if ne (ParamDefaultExpr .) "" }}Computed: true, Default: {{ParamDefaultExpr .}},{{- else if .IsJson }}Computed: true,{{- end }}
|
||||
{{- if ne (ParamDescription .) "" }}MarkdownDescription: {{ParamDescription .}},{{end}}
|
||||
{{- if .Sensitive }}Sensitive: true,{{end}}
|
||||
{{- if .ForceNew }}PlanModifiers: []planmodifier.{{PlanModifierType .}}{ {{PlanModifierExpr .}} },{{else if .IsJson}}PlanModifiers: []planmodifier.String{resources_core.JsonNormalize()},{{end}}
|
||||
@@ -99,17 +99,19 @@ func (r *{{ToCamel (printf "%s_%s" .ServiceName .SubName)}}Resource) Create(ctx
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- if .HasRefSvcParams }}
|
||||
{{- range .SchemaParams }}
|
||||
{{- range .CreateParams }}
|
||||
{{- if and (gt .RefSvcId 0) (eq (ParamType .) "types.String") }}
|
||||
// Подресурсный refSvc-параметр резолвим только в локальную переменную.
|
||||
// Сам plan не трогаем: UUID нужен лишь для backend-вызова, а в state
|
||||
// Terraform должен видеть исходное пользовательское значение.
|
||||
resolved{{ToCamel .Code}} := plan.{{ToCamel .Code}}.ValueString()
|
||||
if !plan.{{ToCamel .Code}}.IsNull() && !plan.{{ToCamel .Code}}.IsUnknown() {
|
||||
resolved{{ToCamel .Code}}, err := r.client.ResolveRefSvcParamValue(ctx, {{.RefSvcId}}, plan.{{ToCamel .Code}}.ValueString())
|
||||
var err error
|
||||
resolved{{ToCamel .Code}}, err = r.client.ResolveRefSvcParamValue(ctx, {{.RefSvcId}}, plan.{{ToCamel .Code}}.ValueString())
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Ошибка клиента", err.Error())
|
||||
return
|
||||
}
|
||||
if resolved{{ToCamel .Code}} != "" && resolved{{ToCamel .Code}} != plan.{{ToCamel .Code}}.ValueString() {
|
||||
plan.{{ToCamel .Code}} = types.StringValue(resolved{{ToCamel .Code}})
|
||||
}
|
||||
}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -166,8 +168,12 @@ func (r *{{ToCamel (printf "%s_%s" .ServiceName .SubName)}}Resource) Create(ctx
|
||||
|
||||
params := resources_core.CompactParams(map[string]string{
|
||||
{{- range .CreateParams }}
|
||||
{{- if and (gt .RefSvcId 0) (eq (ParamType .) "types.String") }}
|
||||
"{{.Code}}": resolved{{ToCamel .Code}},
|
||||
{{- else }}
|
||||
"{{.Code}}": {{ParamFormat . (printf "plan.%s" (ToCamel .Code))}},
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
})
|
||||
|
||||
operationTimeout := ""
|
||||
@@ -247,17 +253,18 @@ func (r *{{ToCamel (printf "%s_%s" .ServiceName .SubName)}}Resource) Update(ctx
|
||||
return
|
||||
}
|
||||
{{- if .HasRefSvcParams }}
|
||||
{{- range .SchemaParams }}
|
||||
{{- range .ModifyParams }}
|
||||
{{- if and (gt .RefSvcId 0) (eq (ParamType .) "types.String") }}
|
||||
// Update для подресурса следует тому же правилу, что и Create:
|
||||
// резолвим значение только в локальную переменную и не мутируем plan.
|
||||
resolved{{ToCamel .Code}} := plan.{{ToCamel .Code}}.ValueString()
|
||||
if !plan.{{ToCamel .Code}}.IsNull() && !plan.{{ToCamel .Code}}.IsUnknown() {
|
||||
resolved{{ToCamel .Code}}, err := r.client.ResolveRefSvcParamValue(ctx, {{.RefSvcId}}, plan.{{ToCamel .Code}}.ValueString())
|
||||
var err error
|
||||
resolved{{ToCamel .Code}}, err = r.client.ResolveRefSvcParamValue(ctx, {{.RefSvcId}}, plan.{{ToCamel .Code}}.ValueString())
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Ошибка клиента", err.Error())
|
||||
return
|
||||
}
|
||||
if resolved{{ToCamel .Code}} != "" && resolved{{ToCamel .Code}} != plan.{{ToCamel .Code}}.ValueString() {
|
||||
plan.{{ToCamel .Code}} = types.StringValue(resolved{{ToCamel .Code}})
|
||||
}
|
||||
}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -284,8 +291,12 @@ func (r *{{ToCamel (printf "%s_%s" .ServiceName .SubName)}}Resource) Update(ctx
|
||||
|
||||
params := resources_core.CompactParams(map[string]string{
|
||||
{{- range .ModifyParams }}
|
||||
{{- if and (gt .RefSvcId 0) (eq (ParamType .) "types.String") }}
|
||||
"{{.Code}}": resolved{{ToCamel .Code}},
|
||||
{{- else }}
|
||||
"{{.Code}}": {{ParamFormat . (printf "plan.%s" (ToCamel .Code))}},
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
})
|
||||
|
||||
idParams := map[string]string{
|
||||
|
||||
@@ -49,6 +49,9 @@ type Param struct {
|
||||
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) всегда совпадали.
|
||||
@@ -80,6 +83,8 @@ type GenResource struct {
|
||||
NeedsBoolDefault bool
|
||||
NeedsInt64Default bool
|
||||
NeedsStringDefault bool
|
||||
NeedsBoolUseStateForUnknown bool
|
||||
NeedsInt64UseStateForUnknown bool
|
||||
HasDomainParam bool
|
||||
DomainServiceIDs []int
|
||||
// HasRedeploy: сервис поддерживает redeploy (пересборка из git).
|
||||
@@ -145,3 +150,34 @@ type GenAction struct {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -27,28 +27,33 @@ func WriteInstanceResource(outDir string, svc types.GenResource) error {
|
||||
filePath := filepath.Join(outDir, fileName)
|
||||
|
||||
tpl, err := template.New("resource").Funcs(template.FuncMap{
|
||||
"ToCamel": helpers.ToCamel,
|
||||
"ToSnake": helpers.ToSnake,
|
||||
"ParamType": helpers.ParamType,
|
||||
"ParamDefaultExpr": helpers.ParamDefaultExpr,
|
||||
"ParamFormat": helpers.ParamFormat,
|
||||
"ParamParse": helpers.ParamParse,
|
||||
"ParamDescription": helpers.ParamDescription,
|
||||
"OutputType": helpers.OutputParamType,
|
||||
"OutputIsMap": helpers.OutputParamIsMap,
|
||||
"OutputIsList": helpers.OutputParamIsList,
|
||||
"OutputSensitive": helpers.OutputParamSensitive,
|
||||
"IsNested": helpers.IsNested,
|
||||
"IsNestedList": helpers.IsNestedList,
|
||||
"NestedModelName": helpers.NestedModelName,
|
||||
"NestedTfType": helpers.NestedTfType,
|
||||
"NestedSchemaType": helpers.NestedSchemaType,
|
||||
"NestedSchemaBlock": helpers.NestedSchemaBlock,
|
||||
"NestedSchemaEnd": helpers.NestedSchemaEnd,
|
||||
"NestedJSONExpr": helpers.NestedJSONExpr,
|
||||
"SubSchemaType": helpers.SubSchemaType,
|
||||
"SubDefaultExpr": helpers.SubDefaultExpr,
|
||||
"bt": func() string { return "`" },
|
||||
"ToCamel": helpers.ToCamel,
|
||||
"ToSnake": helpers.ToSnake,
|
||||
"ParamType": helpers.ParamType,
|
||||
"ParamDefaultExpr": helpers.ParamDefaultExpr,
|
||||
// ShouldBeOptionalComputed — единое правило: параметр, который провайдер
|
||||
// читает обратно из state_params, обязан быть Optional+Computed, если он
|
||||
// не Required и без Default. См. подробное объяснение в helpers.go.
|
||||
"ShouldBeOptionalComputed": helpers.ShouldBeOptionalComputed,
|
||||
"ShouldUseStateForUnknown": helpers.ShouldUseStateForUnknown,
|
||||
"ParamFormat": helpers.ParamFormat,
|
||||
"ParamParse": helpers.ParamParse,
|
||||
"ParamDescription": helpers.ParamDescription,
|
||||
"OutputType": helpers.OutputParamType,
|
||||
"OutputIsMap": helpers.OutputParamIsMap,
|
||||
"OutputIsList": helpers.OutputParamIsList,
|
||||
"OutputSensitive": helpers.OutputParamSensitive,
|
||||
"IsNested": helpers.IsNested,
|
||||
"IsNestedList": helpers.IsNestedList,
|
||||
"NestedModelName": helpers.NestedModelName,
|
||||
"NestedTfType": helpers.NestedTfType,
|
||||
"NestedSchemaType": helpers.NestedSchemaType,
|
||||
"NestedSchemaBlock": helpers.NestedSchemaBlock,
|
||||
"NestedSchemaEnd": helpers.NestedSchemaEnd,
|
||||
"NestedJSONExpr": helpers.NestedJSONExpr,
|
||||
"SubSchemaType": helpers.SubSchemaType,
|
||||
"SubDefaultExpr": helpers.SubDefaultExpr,
|
||||
"bt": func() string { return "`" },
|
||||
}).Parse(templates.Instance)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -135,8 +140,38 @@ func WriteActionResource(outDir string, act types.GenAction) error {
|
||||
return os.WriteFile(filePath, formatted, 0644)
|
||||
}
|
||||
|
||||
// WriteModifierResource генерирует отдельный ресурс для parent-level modify.
|
||||
func WriteModifierResource(outDir string, modifier types.GenModifier) error {
|
||||
name := fmt.Sprintf("%s_%s", modifier.ServiceName, modifier.ModifierName)
|
||||
fileName := fmt.Sprintf("%d_%s_modifier.go", modifier.ServiceID, name)
|
||||
filePath := filepath.Join(outDir, fileName)
|
||||
|
||||
tpl, err := template.New("modifier").Funcs(template.FuncMap{
|
||||
"ToCamel": helpers.ToCamel,
|
||||
"ToSnake": helpers.ToSnake,
|
||||
"ParamType": helpers.ParamType,
|
||||
"ParamDefaultExpr": helpers.ParamDefaultExpr,
|
||||
"ParamFormat": helpers.ParamFormat,
|
||||
"ParamDescription": helpers.ParamDescription,
|
||||
"bt": func() string { return "`" },
|
||||
}).Parse(templates.Modifier)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := tpl.Execute(&buf, modifier); err != nil {
|
||||
return err
|
||||
}
|
||||
formatted, err := helpers.FormatSourceOrWarn(filePath, buf.Bytes())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(filePath, formatted, 0644)
|
||||
}
|
||||
|
||||
// WriteRegistry генерирует registry.go со списком всех ресурсов.
|
||||
func WriteRegistry(outDir string, services []types.GenResource, subs []types.GenSubresource, actions []types.GenAction) error {
|
||||
func WriteRegistry(outDir string, services []types.GenResource, subs []types.GenSubresource, actions []types.GenAction, modifiers []types.GenModifier) error {
|
||||
var buf bytes.Buffer
|
||||
buf.WriteString("package resources_gen\n\n")
|
||||
buf.WriteString("import \"github.com/hashicorp/terraform-plugin-framework/resource\"\n\n")
|
||||
@@ -152,6 +187,9 @@ func WriteRegistry(outDir string, services []types.GenResource, subs []types.Gen
|
||||
for _, act := range actions {
|
||||
buf.WriteString(fmt.Sprintf("\t\tNew%[1]sResource,\n", helpers.ToCamel(act.ServiceName+"_"+act.ActionName)))
|
||||
}
|
||||
for _, modifier := range modifiers {
|
||||
buf.WriteString(fmt.Sprintf("\t\tNew%[1]sResource,\n", helpers.ToCamel(modifier.ServiceName+"_"+modifier.ModifierName)))
|
||||
}
|
||||
buf.WriteString("\t}\n")
|
||||
buf.WriteString("}\n")
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ func run() error {
|
||||
return fmt.Errorf("NUBES_RESOURCES_GEN_DIR is required — must point to generated/{stand}/go/")
|
||||
}
|
||||
|
||||
instanceResources, subresources, actions, err := loader.LoadSpecs(resourcesDir)
|
||||
instanceResources, subresources, actions, modifiers, err := loader.LoadSpecs(resourcesDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -72,6 +72,12 @@ func run() error {
|
||||
writeErrs = append(writeErrs, serviceWriteErr{kind: "action", name: name, err: err})
|
||||
}
|
||||
}
|
||||
for _, modifier := range modifiers {
|
||||
if err := writers.WriteModifierResource(outDir, modifier); err != nil {
|
||||
name := fmt.Sprintf("%s_%s", modifier.ServiceName, modifier.ModifierName)
|
||||
writeErrs = append(writeErrs, serviceWriteErr{kind: "modifier", name: name, err: err})
|
||||
}
|
||||
}
|
||||
|
||||
if len(writeErrs) > 0 {
|
||||
var b strings.Builder
|
||||
@@ -82,7 +88,7 @@ func run() error {
|
||||
return errors.New(strings.TrimSpace(b.String()))
|
||||
}
|
||||
|
||||
if err := writers.WriteRegistry(outDir, instanceResources, subresources, actions); err != nil {
|
||||
if err := writers.WriteRegistry(outDir, instanceResources, subresources, actions, modifiers); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -141,7 +141,7 @@ rm -f "$FAILURES_FILE"
|
||||
# Auto-rebuild yaml-generator if sources are newer than binary
|
||||
BIN="${ROOT_DIR}/TOOLS/bin/yaml-generator"
|
||||
SRC="${ROOT_DIR}/TOOLS/yaml-generator/"
|
||||
if [[ ! -x "$BIN" ]] || [[ "$SRC" -nt "$BIN" ]]; then
|
||||
if [[ ! -x "$BIN" ]] || find "$SRC" -type f -newer "$BIN" -print -quit | grep -q .; then
|
||||
echo "Building yaml-generator..."
|
||||
(cd "${ROOT_DIR}/TOOLS/yaml-generator" && go build -o "$BIN" .)
|
||||
fi
|
||||
|
||||
@@ -3,6 +3,15 @@ set -euo pipefail
|
||||
|
||||
# Canonical resources+docs generator (template format).
|
||||
# Restores instruction-style docs structure (manual/example/params_create/params_modify/outputs/ops/params landing).
|
||||
#
|
||||
# Usage:
|
||||
# ./TOOLS/scripts/02_generate_resources_and_docs_v2.sh --profile TOOLS/config/dev
|
||||
# ./TOOLS/scripts/02_generate_resources_and_docs_v2.sh --profile TOOLS/config/test
|
||||
# ./TOOLS/scripts/02_generate_resources_and_docs_v2.sh --profile TOOLS/config/prod
|
||||
#
|
||||
# The script always rebuilds resource-generator and docs-generator from source
|
||||
# before running. This prevents stale binaries from hiding support for new YAML
|
||||
# kinds such as modifier.
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ROOT_DIR="${ROOT_DIR:-$(cd "${SCRIPT_DIR}/../.." && pwd)}"
|
||||
@@ -93,9 +102,20 @@ echo "Generating resources from unified YAML specs..."
|
||||
TMP_GEN_DIR="$(mktemp -d)"
|
||||
trap 'rm -rf "$TMP_GEN_DIR"' EXIT
|
||||
|
||||
RESOURCE_GENERATOR_BIN="${ROOT_DIR}/TOOLS/bin/resource-generator"
|
||||
RESOURCE_GENERATOR_SRC="${ROOT_DIR}/TOOLS/resource-generator"
|
||||
DOCS_GENERATOR_BIN="${ROOT_DIR}/TOOLS/bin/docs-generator"
|
||||
DOCS_GENERATOR_SRC="${ROOT_DIR}/TOOLS/docs-generator"
|
||||
|
||||
# Всегда пересобираем генераторы из текущих исходников.
|
||||
# Это убирает зависимость от mtime и исключает запуск устаревшего бинаря,
|
||||
# который может не знать про новые kinds/spec-ветки в YAML.
|
||||
(cd "$RESOURCE_GENERATOR_SRC" && go build -o "$RESOURCE_GENERATOR_BIN" .)
|
||||
(cd "$DOCS_GENERATOR_SRC" && go build -o "$DOCS_GENERATOR_BIN" .)
|
||||
|
||||
NUBES_RESOURCES_DIR="$RESOURCES_YAML_DIR" \
|
||||
NUBES_RESOURCES_GEN_DIR="$TMP_GEN_DIR" \
|
||||
${ROOT_DIR}/TOOLS/bin/resource-generator
|
||||
"$RESOURCE_GENERATOR_BIN"
|
||||
|
||||
mkdir -p "$GO_OUTPUT_DIR"
|
||||
rm -rf "${GO_OUTPUT_DIR}"/*
|
||||
@@ -113,7 +133,7 @@ if [[ "$DOCS_API_ENDPOINT" != *"/index.cfm"* ]] && [[ "$DOCS_API_ENDPOINT" != *"
|
||||
DOCS_API_ENDPOINT="${DOCS_API_ENDPOINT%/}/index.cfm"
|
||||
fi
|
||||
|
||||
${ROOT_DIR}/TOOLS/bin/docs-generator \
|
||||
"$DOCS_GENERATOR_BIN" \
|
||||
-resources "$RESOURCES_YAML_DIR" \
|
||||
-docs "$DOCS_DIR" \
|
||||
-services "$SERVICES_LIST_PATH" \
|
||||
|
||||
@@ -38,6 +38,9 @@ if [[ -z "$PROFILE_DIR" ]]; then
|
||||
exit 2
|
||||
fi
|
||||
|
||||
"${ROOT_DIR}/TOOLS/scripts/01_generate_yamls.sh" --profile "$PROFILE_DIR"
|
||||
"${ROOT_DIR}/TOOLS/scripts/02_generate_resources_and_docs_v2.sh" --profile "$PROFILE_DIR"
|
||||
|
||||
resolve_root_path() {
|
||||
local path_value="$1"
|
||||
if [[ -z "$path_value" ]]; then
|
||||
@@ -51,7 +54,13 @@ resolve_root_path() {
|
||||
echo "${ROOT_DIR}/${path_value}"
|
||||
}
|
||||
|
||||
S3CFG_REGISTRY="${S3CFG_REGISTRY:-${ROOT_DIR}/secrets/.s3cfg_registry}"
|
||||
if [[ -z "${S3CFG_REGISTRY:-}" ]]; then
|
||||
if [[ -f "${ROOT_DIR}/secrets/.s3cfg_provider" ]]; then
|
||||
S3CFG_REGISTRY="${ROOT_DIR}/secrets/.s3cfg_provider"
|
||||
else
|
||||
S3CFG_REGISTRY="${ROOT_DIR}/secrets/.s3cfg_registry"
|
||||
fi
|
||||
fi
|
||||
TIMEOUTS_SOURCE="${TIMEOUTS_SOURCE:-${PROFILE_DIR}/operation_timeouts.json}"
|
||||
PROFILE_RESOURCES_YAML_DIR="${ROOT_DIR}/generated/$(basename "$PROFILE_DIR")/resources_yaml"
|
||||
PROFILE_GENERATED_GO_DIR="${ROOT_DIR}/generated/$(basename "$PROFILE_DIR")/go"
|
||||
@@ -153,7 +162,7 @@ validate_registry_completeness() {
|
||||
tmp_registered="$(mktemp)"
|
||||
tmp_missing="$(mktemp)"
|
||||
|
||||
find "$resources_gen_dir" -maxdepth 1 -type f -name '*_resource.go' -print0 \
|
||||
find "$resources_gen_dir" -maxdepth 1 -type f \( -name '*_resource.go' -o -name '*_modifier.go' -o -name '*_action.go' \) -print0 \
|
||||
| xargs -0 grep -hE 'func[[:space:]]+New[[:alnum:]_]+Resource[[:space:]]*\(' \
|
||||
| sed -E 's/.*(New[[:alnum:]_]+Resource).*/\1/' \
|
||||
| sort -u > "$tmp_expected"
|
||||
|
||||
@@ -33,7 +33,19 @@ PROVIDER_DIR="${PROVIDER_DIR:-${ROOT_DIR}/provider}"
|
||||
BUILD_DIR="${BUILD_DIR:-${ROOT_DIR}/artifacts/provider_build}"
|
||||
GPG_KEY_FILE="${GPG_KEY_FILE:-${ROOT_DIR}/secrets/private_key.asc}"
|
||||
|
||||
VERSION="${1:-2.0.2}"
|
||||
# Гейт дрейфа: прямая сборка из общего provider/ запрещена.
|
||||
# В provider/internal/resources_gen может лежать устаревшая копия (каталог
|
||||
# в .gitignore, дрейф не виден в git). Канонический путь — через
|
||||
# 03_build_and_upload_provider.sh: он собирает из временной копии со
|
||||
# свежесгенерированным кодом. Детект дрейфа: TOOLS/scripts/check_generated_drift.sh.
|
||||
if [[ "$PROVIDER_DIR" == "${ROOT_DIR}/provider" ]]; then
|
||||
echo "Error: refused to build directly from the shared provider dir." >&2
|
||||
echo " It may contain a stale generated copy of another stand." >&2
|
||||
echo " Use: TOOLS/scripts/03_build_and_upload_provider.sh --profile <dev|test|prod>" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
VERSION="${1:-2.0.3}"
|
||||
REGISTRY_HOSTNAME="${REGISTRY_HOSTNAME:-tf-registry.containerk8s.services.ngcloud.ru}"
|
||||
NAMESPACE="${NAMESPACE:-nubes}"
|
||||
PROVIDER_NAME="${PROVIDER_NAME:-nubes}"
|
||||
|
||||
Executable
+60
@@ -0,0 +1,60 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# check_generated_drift.sh — детект дрейфа сгенерированного Go-кода.
|
||||
#
|
||||
# Сравнивает ПО СОДЕРЖИМОМУ каталог свежей генерации стенда с рабочей копией
|
||||
# провайдера, из которой возможна прямая сборка:
|
||||
#
|
||||
# generated/<stand>/go/ <-> provider/internal/resources_gen/
|
||||
#
|
||||
# Каноническая сборка идёт через 03_build_and_upload_provider.sh (временная
|
||||
# копия со свежесгенерированным кодом). Этот скрипт — детектор: он не даёт
|
||||
# provider/internal/resources_gen незаметно протухнуть (каталог в .gitignore,
|
||||
# поэтому в git status дрейф не виден).
|
||||
#
|
||||
# Использование:
|
||||
# ./TOOLS/scripts/check_generated_drift.sh dev # dev | test | prod
|
||||
#
|
||||
# Выход: 0 — дрейфа нет; 1 — дрейф есть; 2 — ошибка аргументов.
|
||||
|
||||
STAND="${1:-}"
|
||||
if [[ -z "$STAND" ]]; then
|
||||
echo "Usage: $0 <stand> # dev | test | prod" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ROOT_DIR="${ROOT_DIR:-$(cd "${SCRIPT_DIR}/../.." && pwd)}"
|
||||
|
||||
GEN_GO_DIR="${ROOT_DIR}/generated/${STAND}/go"
|
||||
PROV_GO_DIR="${ROOT_DIR}/provider/internal/resources_gen"
|
||||
|
||||
if [[ ! -d "$GEN_GO_DIR" ]]; then
|
||||
echo "ERROR: no generated go for stand '${STAND}': $GEN_GO_DIR" >&2
|
||||
echo " Run: TOOLS/scripts/02_generate_resources_and_docs_v2.sh --profile TOOLS/config/${STAND}" >&2
|
||||
exit 2
|
||||
fi
|
||||
if [[ ! -d "$PROV_GO_DIR" ]]; then
|
||||
echo "DRIFT: provider resources_gen dir missing: $PROV_GO_DIR" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Сравнение по содержимому, включая файлы, присутствующие только с одной стороны.
|
||||
# .stand — маркер стенда, который кладёт dev-materialize.sh; в сравнении не участвует.
|
||||
if diff -rq --exclude='.stand' "$GEN_GO_DIR" "$PROV_GO_DIR" >/dev/null; then
|
||||
echo "OK: no drift for stand '${STAND}' (generated go == provider copy)."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "DRIFT DETECTED for stand '${STAND}':" >&2
|
||||
echo " source: $GEN_GO_DIR" >&2
|
||||
echo " target: $PROV_GO_DIR" >&2
|
||||
echo "" >&2
|
||||
echo "Сгенерированный код стенда расходится с рабочей копией провайдера." >&2
|
||||
echo "Релиз собирается через 03_build_and_upload_provider.sh (temp-копия)." >&2
|
||||
echo "Для локальной разработки перематериализуй стенд:" >&2
|
||||
echo " TOOLS/scripts/dev-materialize.sh ${STAND}" >&2
|
||||
echo "Различия:" >&2
|
||||
diff -rq --exclude='.stand' "$GEN_GO_DIR" "$PROV_GO_DIR" >&2 || true
|
||||
exit 1
|
||||
Executable
+29
@@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# check_hardcoded_service_ids.sh — запрет сервис-специфичных хардкодов по числовому ID.
|
||||
#
|
||||
# Ищет сравнения вида svc.ID == N / ServiceID == N / spec.ServiceID == N (N > 0)
|
||||
# в Go-коде TOOLS/. Исключения должны жить ТОЛЬКО в именованных реестрах (данные):
|
||||
# - TOOLS/yaml-generator/main.go (serviceSpecificModifiers)
|
||||
# - TOOLS/docs-generator/internal/writers/writers.go (serviceSpecificDocExamples)
|
||||
#
|
||||
# Выход: 0 — хардкодов нет; 1 — найдены.
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ROOT_DIR="${ROOT_DIR:-$(cd "${SCRIPT_DIR}/../.." && pwd)}"
|
||||
|
||||
matches="$(grep -rnE '\.(ServiceID|ID)[[:space:]]*==[[:space:]]*[1-9][0-9]*' "$ROOT_DIR/TOOLS" --include='*.go' || true)"
|
||||
|
||||
if [[ -n "$matches" ]]; then
|
||||
echo "HARDCODED SERVICE ID FOUND (service-specific logic must live in a registry):" >&2
|
||||
echo "$matches" >&2
|
||||
echo "" >&2
|
||||
echo "Вынеси исключение в один из реестров:" >&2
|
||||
echo " - TOOLS/yaml-generator/main.go (serviceSpecificModifiers)" >&2
|
||||
echo " - TOOLS/docs-generator/internal/writers/writers.go (serviceSpecificDocExamples)" >&2
|
||||
echo "См. TOOLS/ARCHITECTURE.md, раздел «Реестр исключений»." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "OK: no hardcoded service IDs in TOOLS/."
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user