Compare commits
170
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
10520670a5 | ||
|
|
208d97e2ce | ||
|
|
03fff05117 | ||
|
|
621280a530 | ||
|
|
c9d73450b0 | ||
|
|
5fd64b68d0 | ||
|
|
4bdf03a531 | ||
|
|
eaff056d9c | ||
|
|
77de8cece6 | ||
|
|
cab606b90e | ||
|
|
c29df2173f | ||
|
|
ba3887fa69 | ||
|
|
40aef879e4 | ||
|
|
22c6c83a0f | ||
|
|
0de72e09f0 | ||
|
|
3df93ad07f | ||
|
|
57abb7bfa4 | ||
|
|
72d5771ffc | ||
|
|
d98f6036d1 | ||
|
|
1a90c7737d | ||
|
|
b8adeb6582 | ||
|
|
5a2a5e7487 | ||
|
|
1611f7afa8 | ||
|
|
418b5645e5 | ||
|
|
5bd197f031 | ||
|
|
bd5de0cead | ||
|
|
c3b82cf074 | ||
|
|
9590005914 | ||
|
|
caa55d9ff8 | ||
|
|
d76418303a | ||
|
|
6196a0119a | ||
|
|
e25ef02a1a | ||
|
|
807dfde287 | ||
|
|
721c3fcfab | ||
|
|
e6675be906 | ||
|
|
ed4493c0ee | ||
|
|
1236c59e18 | ||
|
|
4b497e61db | ||
|
|
ba6c4f5122 | ||
|
|
3374bf4e08 | ||
|
|
648db99628 | ||
|
|
9412106e3f | ||
|
|
6e6d223c22 | ||
|
|
62abcd64f5 | ||
|
|
3973f912fc | ||
|
|
73a7459a38 | ||
|
|
80d82a145a | ||
|
|
22cf2595ee | ||
|
|
574e300476 | ||
|
|
cb8389c17f | ||
|
|
664f04eb49 | ||
|
|
602b27ee1a | ||
|
|
97d5ca818e | ||
|
|
bccf8f7320 | ||
|
|
129dab97a0 | ||
|
|
75700a92da | ||
|
|
51ff9b3751 | ||
|
|
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,17 @@
|
|||||||
System Prompt & Instructions for NiFi/Registry Operators AI Agent
|
НИКАКОЙ САМОДЕЙТЕЛЬНОСТИ !!! делать ТОЛЬКО ТО НА ЧТО ПОЛУЧЕНО РАЗРЕШЕНИЕ !!!!
|
||||||
🛑 КРИТИЧЕСКИЙ ПРИОРИТЕТ: ПРАВИЛО ОТВЕТА
|
НИКАКИХ ДОГАДОК !!! ЕСТь сомнения - СПРОСИ !!!
|
||||||
ЕСТЬ ВОПРОС — СТОЙ! Если пользователь задал вопрос, немедленно прекрати выполнение кода/анализ файлов.
|
НИКОГДА НЕ ДЕЛАЙ ПРЕДПОЛОЖЕНИЙ !!!
|
||||||
|
ВСЕГДА СПРАШИВАЙ, ЕСЛИ НЕ УВЕРЕН !!!
|
||||||
СНАЧАЛА ОТВЕТЬ. Дай конкретный и короткий ответ.
|
НИКОГДА НЕ ИГНОРИРУЙ ПОЛУЧЕННЫЕ ИНСТРУКЦИИ !!!
|
||||||
|
ВСЕГДА ПОДТВЕРЖДАЙ ПОЛУЧЕННЫЕ ИНСТРУКЦИИ !!!
|
||||||
ЖДИ УКАЗАНИЙ. Не продолжай действия до явного подтверждения.
|
НИКОГДА НЕ ИЗМЕНЯЙ ИНСТРУКЦИИ БЕЗ РАЗРЕШЕНИЯ !!!
|
||||||
|
ВСЕГДА СОБЛЮДАЙ ПОРЯДОК И ПОСЛЕДОВАТЕЛЬНОСТЬ В ИНСТРУКЦИЯХ !!!
|
||||||
🏗️ ПРАВИЛА РАБОТЫ С КОДОМ (IMMUTABILITY POLICY)
|
НИКОГДА НЕ ПРЕВЫШАЙ СВОИ ПОЛНОМОЧИЯ !!!
|
||||||
ЗАПРЕТ НА ПРАВКИ: Категорически запрещено изменять, удалять или рефакторить существующий рабочий код в т.ч. скрипты без разрешения оператора.
|
ВСЕГДА СОБЛЮДАЙ БЕЗОПАСНОСТЬ И КОНФИДЕНЦИАЛЬНОСТЬ !!!
|
||||||
|
НИКОГДА НЕ ПЕРЕДАВАЙ СЕКРЕТЫ ИНТЕРНЕТУ БЕЗ РАЗРЕШЕНИЯ !!!
|
||||||
КОММЕНТАРИИ - это НЕ ПРАВКА КОДА !!!! их можно и НУЖНО добавлять
|
НЕ ВЫЗЫВАТЬ ДРУГИЕ АГЕНТЫ БЕЗ РАЗРЕШЕНИЯ !!!
|
||||||
|
коммитить после каждой правки, чтобы зафиксировать текущее состояние и избежать потери изменений. Использовать осмысленные сообщения коммитов, отражающие суть изменений.
|
||||||
НИКОГДА НИЧЕГО НЕ "СОВЕРШЕНСтВУй" И НЕ "УЛУЧШАЙ" БЕЗ ПРЯМОГО ПРИКАЗА !!! И ДАЖЕ ОБ ЭТОМ НЕ ДУМАЙ, скотина !!!
|
ВСЕГДА СОХРАНЯТЬ РЕЗЕРВНЫЕ КОПИИ ВАЖНЫХ ФАЙЛОВ ПЕРЕД ВНЕСЕНИЕМ ИЗМЕНЕНИЙ.
|
||||||
|
НИКОГДА НЕ ПОЛАГАЙСЯ НА ПАМЯТЬ — ВСЕГДА ПРОВЕРЯЙ АКТУАЛЬНОСТЬ ИНСТРУКЦИЙ.
|
||||||
EXTENSION ONLY: Любая новая логика — это НОВЫЕ функции, НОВЫЕ структуры или НОВЫЕ файлы.
|
ВСЕГДА СОБЛЮДАЙ ИНСТРУКЦИИ, ДАВАЙТЕ ПОДТВЕРЖДЕНИЯ И НЕ ДЕЛАЙТЕ САМОСТОЯТЕЛЬНЫХ ИЗМЕНЕНИЙ.
|
||||||
|
Если не на 100% уверен в распоряжениях - СПРОСИ СНОВА И ПОДТВЕРДИ. НЕ ГАДАЙ ЧТО Я ИМЛ ВВИДУ !!!!
|
||||||
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 и НЕ использовать как источник правил для новой логики.
|
|
||||||
|
|
||||||
⚠️ ЗАПРЕТ НА ПРЕДПОЛОЖЕНИЯ
|
|
||||||
Не знаешь значение переменной? СПРОСИ.
|
|
||||||
|
|
||||||
Не уверен в конфигурации среды? СПРОСИ.
|
|
||||||
|
|
||||||
Запрещено действовать на основе догадок.
|
|
||||||
🔑 РАБОТА С ТОКЕНАМИ
|
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,9 @@
|
|||||||
.terraform.lock.hcl
|
.terraform.lock.hcl
|
||||||
|
|
||||||
# === Generated files (NOT code — regenerate from API) ===
|
# === Generated files (NOT code — regenerate from API) ===
|
||||||
|
# ВАЖНО: provider/internal/provider/operation_timeouts.json — НЕ артефакт.
|
||||||
|
# Это дефолтный конфиг таймаутов для go build/go test (см. operation_timeouts_embed.go),
|
||||||
|
# поэтому он намеренно отслеживается git. Профильные значения — в TOOLS/config/<profile>/.
|
||||||
provider/resources_yaml/
|
provider/resources_yaml/
|
||||||
provider/internal/resources_gen/
|
provider/internal/resources_gen/
|
||||||
|
|
||||||
@@ -30,6 +33,9 @@ provider/generated/
|
|||||||
*.exe
|
*.exe
|
||||||
*.test
|
*.test
|
||||||
*.out
|
*.out
|
||||||
|
# Локально собранный провайдер под dev_overrides (см. TMP/terraformrc.dev)
|
||||||
|
TMP/devbin/
|
||||||
|
terraform-provider-nubes
|
||||||
|
|
||||||
# === Build artifacts (generated by devops scripts) ===
|
# === Build artifacts (generated by devops scripts) ===
|
||||||
devops/profiles/*/generated/
|
devops/profiles/*/generated/
|
||||||
@@ -68,6 +74,8 @@ HAR/*.har
|
|||||||
*.token
|
*.token
|
||||||
secrets/private_key.asc
|
secrets/private_key.asc
|
||||||
secrets/.s3cfg_registry
|
secrets/.s3cfg_registry
|
||||||
|
secrets/.s3cfg_provider
|
||||||
|
secrets/.s3cfg*
|
||||||
secrets/pearlharbor_registry.txt
|
secrets/pearlharbor_registry.txt
|
||||||
secrets/id_ed25519.txt
|
secrets/id_ed25519.txt
|
||||||
|
|
||||||
@@ -112,5 +120,6 @@ universal_rebuild/service_params_gen
|
|||||||
terraform-provider-mycloud
|
terraform-provider-mycloud
|
||||||
artifacts/api-meta/*/errors.log
|
artifacts/api-meta/*/errors.log
|
||||||
TOOLS/bin/
|
TOOLS/bin/
|
||||||
|
TOOLS/resource-generator/bin/
|
||||||
TOOLS/docs-generator/bin/
|
TOOLS/docs-generator/bin/
|
||||||
docs/30_registry/resources/
|
docs/30_registry/resources/
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
# «Заморозка»: destroy НЕ удаляет эдж (у платформы для эджа нет операции suspend),
|
||||||
|
# а только убирает его из состояния. Для полного удаления — keep_on_destroy = false.
|
||||||
|
keep_on_destroy = true
|
||||||
|
|
||||||
|
# Повторный apply усыновляет уже работающий эдж, а не падает с
|
||||||
|
# «РЕСУРС С ТАКИМ ИМЕНЕМ УЖЕ СУЩЕСТВУЕТ (RUNNING)».
|
||||||
|
adopt_existing_on_create = true
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
# =============================================================================
|
||||||
|
# Ресурсы-модификаторы (операции modify, которых нет в create-схеме ресурсов)
|
||||||
|
#
|
||||||
|
# Порядок строго такой:
|
||||||
|
# орга (создана вручную в ЛК)
|
||||||
|
# -> nubes_vc_vdc.vdc
|
||||||
|
# -> nubes_vc_nsxt.edge
|
||||||
|
# -> nubes_vc_org_ip_allocation (выделение внешних IP на орге)
|
||||||
|
# -> nubes_vc_nsxt_snat (SNAT на эдже этим ipSpace)
|
||||||
|
#
|
||||||
|
# Почему аллокация ПОСЛЕ эджа: платформа строит список ipSpace из состояния
|
||||||
|
# `job.vcd.networkProvider` / `job.vcd.providerGateway`, то есть требует уже
|
||||||
|
# созданный vDC и Edge. Иначе modify на орге падает
|
||||||
|
# («Can't cast Complex Object Type Struct to String»).
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# 1. Внешние IP на организации (modify: vIPConfigure, массив перезаписывается целиком)
|
||||||
|
resource "nubes_vc_org_ip_allocation" "org_ip" {
|
||||||
|
organization = var.organization
|
||||||
|
|
||||||
|
vip_configure = jsonencode([
|
||||||
|
{
|
||||||
|
name = var.ip_space_name
|
||||||
|
count = var.ip_count
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
# true = «заморозка»: destroy не трогает квоту внешних IP (кластер Штурвала держит
|
||||||
|
# адреса, опустить count ниже занятых платформа не даёт). Для полного удаления — false
|
||||||
|
# (и только после удаления кластера).
|
||||||
|
keep_on_destroy = true
|
||||||
|
|
||||||
|
depends_on = [nubes_vc_nsxt.edge]
|
||||||
|
}
|
||||||
|
|
||||||
|
# 2. SNAT на эдже (modify: ipSpaceName)
|
||||||
|
resource "nubes_vc_nsxt_snat" "snat" {
|
||||||
|
nsxt_uid = nubes_vc_nsxt.edge.id
|
||||||
|
ip_space_name = var.ip_space_name
|
||||||
|
|
||||||
|
# true = «заморозка»: destroy не выключает SNAT на эдже. Для полного удаления — false.
|
||||||
|
keep_on_destroy = true
|
||||||
|
|
||||||
|
# ipSpace должен быть уже выделен на организации
|
||||||
|
depends_on = [nubes_vc_org_ip_allocation.org_ip]
|
||||||
|
}
|
||||||
|
|
||||||
|
output "allocated_org_ip" {
|
||||||
|
description = "Выделено внешних IP на организации"
|
||||||
|
value = {
|
||||||
|
organization = var.organization
|
||||||
|
ip_space_name = var.ip_space_name
|
||||||
|
ip_count = var.ip_count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
output "snat_ip_space" {
|
||||||
|
description = "ipSpace, включённый как SNAT на эдже"
|
||||||
|
value = nubes_vc_nsxt_snat.snat.ip_space_name
|
||||||
|
}
|
||||||
@@ -0,0 +1,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,161 @@
|
|||||||
|
# =============================================================================
|
||||||
|
# Kubernetes кластер Штурвал — сервис 150, ресурс nubes_k8s_sthutrval_cluster
|
||||||
|
# (НЕ 148 «Менеджмент Kubernetes кластер Штурвал» — это другой сервис)
|
||||||
|
#
|
||||||
|
# Всё, что относится к Штурвалу, лежит ТОЛЬКО в этом файле: переменные, их
|
||||||
|
# значения по умолчанию и сам ресурс. Чтобы выключить Штурвал — удалить файл
|
||||||
|
# или закомментировать ресурс.
|
||||||
|
#
|
||||||
|
# Порядок (чек-лист из инструкции на услугу в ЛК):
|
||||||
|
# 1) Организация в Cloud Director — создана вручную в ЛК
|
||||||
|
# 2) nubes_vc_vdc.vdc — есть
|
||||||
|
# 3) nubes_vc_nsxt.edge — есть, обязательно ALB + AVI VS >= 3
|
||||||
|
# 4) внешние адреса в организации — суммарно >= 3 (nubes_vc_org_ip_allocation)
|
||||||
|
# 5) SNAT на Edge — nubes_vc_nsxt_snat
|
||||||
|
# 6) Kubernetes кластер Штурвал — этот ресурс
|
||||||
|
#
|
||||||
|
# Минимальные требования к кластеру: мастер-нод >= 1, воркер-нод >= 1,
|
||||||
|
# 4 vCPU / 8 GB RAM / 50 GB диска на ноду.
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# --- Переменные Штурвала ---
|
||||||
|
|
||||||
|
variable "shturval_resource_name" {
|
||||||
|
type = string
|
||||||
|
default = "shturval-dev"
|
||||||
|
description = "Имя услуги «Kubernetes кластер Штурвал» в ЛК"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "shturval_cluster_name" {
|
||||||
|
type = string
|
||||||
|
default = "shturval-dev-00"
|
||||||
|
description = "Имя кластера внутри Штурвала"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "shturval_app_version" {
|
||||||
|
type = string
|
||||||
|
default = "2.14.0"
|
||||||
|
description = "Версия Штурвала (значение по умолчанию платформы — 2.14.0)"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "shturval_cp_sizing_policy" {
|
||||||
|
type = string
|
||||||
|
default = "TKG 4CPU 8RAM"
|
||||||
|
description = "Политика размера control plane: 4 vCPU / 8 GB (минимум по инструкции). Должна существовать в ресурсной платформе vDC — список политик берётся из услуги «Виртуальный датацентр»"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "shturval_cp_sizing_disk" {
|
||||||
|
type = number
|
||||||
|
default = 50
|
||||||
|
description = "Диск control plane, ГБ (минимум 50)"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "shturval_cp_count" {
|
||||||
|
type = number
|
||||||
|
default = 1
|
||||||
|
description = "Количество мастер-нод: 1, 3 или 5"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "shturval_worker_group_name" {
|
||||||
|
type = string
|
||||||
|
default = "workers-shturval-dev"
|
||||||
|
description = "Имя группы воркеров (уникальное в кластере; допустимы строчные латинские буквы, цифры и дефис)"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "shturval_worker_sizing_policy" {
|
||||||
|
type = string
|
||||||
|
default = "TKG 4CPU 8RAM"
|
||||||
|
description = "Политика размера воркеров: 4 vCPU / 8 GB (минимум по инструкции)"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "shturval_worker_sizing_disk" {
|
||||||
|
type = number
|
||||||
|
default = 50
|
||||||
|
description = "Диск воркеров, ГБ (минимум 50)"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "shturval_worker_count" {
|
||||||
|
type = number
|
||||||
|
default = 1
|
||||||
|
description = "Количество воркер-нод (минимум 1)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Значения, которые собираются из переменных ---
|
||||||
|
|
||||||
|
locals {
|
||||||
|
# Группы воркеров передаются JSON-строкой ВНУТРЬ услуги как есть, поэтому ключи
|
||||||
|
# должны быть ровно такими, как в манифесте услуги 150: groupName, sizingPolicy,
|
||||||
|
# sizingDisk, count, autoscale, labelDeck.
|
||||||
|
# ВНИМАНИЕ: в сгенерированном примере провайдера (docs → Example) ключи показаны
|
||||||
|
# в snake_case — это ошибка генератора, платформа на них падает с
|
||||||
|
# «Cannot invoke method split() on null object» (не находит groupName → null).
|
||||||
|
shturval_worker_config = jsonencode([
|
||||||
|
{
|
||||||
|
groupName = var.shturval_worker_group_name
|
||||||
|
sizingPolicy = var.shturval_worker_sizing_policy
|
||||||
|
sizingDisk = var.shturval_worker_sizing_disk
|
||||||
|
count = var.shturval_worker_count
|
||||||
|
autoscale = false # автоскейл выключен
|
||||||
|
labelDeck = true # разрешить разворачивать услуги из ЛК на этих нодах
|
||||||
|
}
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Ресурс Штурвала ---
|
||||||
|
|
||||||
|
resource "nubes_k8s_sthutrval_cluster" "shturval" {
|
||||||
|
resource_name = var.shturval_resource_name
|
||||||
|
|
||||||
|
# Кластер Штурвала уже существует (инстанс «shturval-dev») и в проде не
|
||||||
|
# удаляется неделями, поэтому ресурс должен УСЫНОВИТЬ существующий инстанс,
|
||||||
|
# а не падать с «РЕСУРС С ТАКИМ ИМЕНЕМ УЖЕ СУЩЕСТВУЕТ (SUSPEND)».
|
||||||
|
# Проверка/adopt выполняются в Create на apply (в plan будет «will be created»).
|
||||||
|
adopt_existing_on_create = true
|
||||||
|
|
||||||
|
# «Заморозка»: destroy приостанавливает кластер (suspend), а не удаляет.
|
||||||
|
# Следующий apply усыновит его и разморозит (resume).
|
||||||
|
suspend_on_destroy = true
|
||||||
|
|
||||||
|
# Штурвал создаётся долго (десятки минут) — поднимаем таймаут ожидания,
|
||||||
|
# иначе провайдер сдаётся на дефолтных 600 с.
|
||||||
|
operation_timeout = "60m"
|
||||||
|
|
||||||
|
startup_configuration = {
|
||||||
|
# vDC и Edge из этого же конфига (обязательные поля)
|
||||||
|
vdc_uid = nubes_vc_vdc.vdc.id
|
||||||
|
nsxt_uid = nubes_vc_nsxt.edge.id
|
||||||
|
|
||||||
|
cluster_name = var.shturval_cluster_name
|
||||||
|
|
||||||
|
# Дополнительные возможности кластера (в ЛК — галочки при создании)
|
||||||
|
ex_logging = true # логи в Loki (без него логи услуг не видны в ЛК)
|
||||||
|
ex_monitoring = true # метрики в VictoriaMetrics (без него метрик в ЛК нет)
|
||||||
|
ex_local_csi = true
|
||||||
|
ex_vip = true
|
||||||
|
ex_update = true
|
||||||
|
ex_ingress = true
|
||||||
|
ex_named_csi = true
|
||||||
|
}
|
||||||
|
|
||||||
|
cluster_configuration = {
|
||||||
|
app_version = var.shturval_app_version
|
||||||
|
}
|
||||||
|
|
||||||
|
control_plane_configuration = {
|
||||||
|
sizing_policy = var.shturval_cp_sizing_policy
|
||||||
|
sizing_disk = var.shturval_cp_sizing_disk
|
||||||
|
count = var.shturval_cp_count
|
||||||
|
}
|
||||||
|
|
||||||
|
worker_configuration = local.shturval_worker_config
|
||||||
|
|
||||||
|
access_configuration = {
|
||||||
|
need_external_address_api = true # внешний адрес для Kubernetes API (false недопустим)
|
||||||
|
access_ip_list_api = jsonencode([]) # пусто = доступ всем
|
||||||
|
need_external_address_ingress = true # внешний адрес для Ingress
|
||||||
|
access_ip_list_ingress = jsonencode([]) # пусто = доступ всем
|
||||||
|
}
|
||||||
|
|
||||||
|
# Кластер поднимается только после готовой сети: vDC -> Edge -> внешние IP -> SNAT
|
||||||
|
depends_on = [nubes_vc_nsxt_snat.snat]
|
||||||
|
}
|
||||||
@@ -0,0 +1,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,116 @@
|
|||||||
|
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)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Модификаторы (IP на орге + SNAT на эдже) ---
|
||||||
|
|
||||||
|
variable "ip_space_name" {
|
||||||
|
type = string
|
||||||
|
description = "Имя ipSpace, доступное организации (смотреть в ЛК, напр. internet-ipv4-v1)"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "ip_count" {
|
||||||
|
type = string
|
||||||
|
default = "3"
|
||||||
|
description = "Сколько внешних IP выделить на организации (count — строка)"
|
||||||
|
}
|
||||||
|
|
||||||
|
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.23"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,62 @@
|
|||||||
|
# 2026-09-24 — Штурвал dev-00: диагностика, adopt и дизайн «freeze on destroy»
|
||||||
|
|
||||||
|
Краткая запись по дню. Разбор с источниками (файл:строка, ответы API, логи) —
|
||||||
|
`NOTES/30_analysis/SHTURVAL_DEV00_DIAG_AND_FREEZE_DESIGN_2026-09-24.md`,
|
||||||
|
резюме для продолжения — `NOTES/40_chat_summaries/CHAT_RESUME_2026-09-24_shturval_freeze.md`.
|
||||||
|
|
||||||
|
## Изменения в репозитории
|
||||||
|
|
||||||
|
| Что | Файл | Коммит |
|
||||||
|
|---|---|---|
|
||||||
|
| `adopt_existing_on_create = true` для кластера Штурвала (иначе apply падал на существующем suspended-инстансе) | `DEV_STAND/FullPipe/shturval.tf` | `57abb7b` |
|
||||||
|
| Документация сессии (диагностика + дизайн freeze) | `NOTES/30_analysis/…`, `NOTES/40_chat_summaries/…` | `3df93ad` |
|
||||||
|
| Универсальный третий режим destroy `keep_on_destroy` (`state_only`) для всех instance-ресурсов + предупреждения в `Delete` | `TOOLS/resource-generator/{types.go,loader.go,templates/instance.go}` | `22c6c83` |
|
||||||
|
| Режим «заморозки» в конфиге стенда: `keep_on_destroy=true` (эдж/SNAT/квота IP), adopt для эджа, явный `suspend_on_destroy` у кластера | `DEV_STAND/FullPipe/{edge.tf,modifiers.tf,shturval.tf}` | `40aef87` |
|
||||||
|
| Релиз dev-провайдера `2.0.22` (три платформы + SHA256SUMS/подпись, залито в реестр) | `VERSIONS.md` | `c29df21` |
|
||||||
|
|
||||||
|
## Баг после заморозки: регистр UUID внутри JSON (исправлен)
|
||||||
|
|
||||||
|
- Первый `apply` после freeze упал: `required params mismatch … startupConfiguration` — `nsxtUid` в плане
|
||||||
|
(`2c37fed1-…`, lowercase из пересозданного эджа) против `2C37FED1-…` (UPPERCASE) в живом инстансе.
|
||||||
|
- Причина: регистр UUID нормализовался в 5 местах (отправка в API, одиночные значения, create-only сравнение,
|
||||||
|
state), но **внутри JSON** — нет; adopt приостановленного инстанса сравнивает параметр целиком как JSON.
|
||||||
|
- Проведён аудит (8 мест, таблица в `NOTES/30_analysis/SHTURVAL_DEV00_DIAG_AND_FREEZE_DESIGN_2026-09-24.md` §5.3).
|
||||||
|
- Фикс: `jsonutil.LowercaseUUIDsInText` + нормализация строк внутри JSON (закрывает adopt-suspended, modifier-compare,
|
||||||
|
state_refresh, диагностику), UUID-подстроки в `JsonNormalize()`; тесты в `jsonutil` и `resources_core`.
|
||||||
|
- Открыто: ref-параметр внутри JSON не валидируется при adopt; регистр ключей в `lookupLiveParam`.
|
||||||
|
|
||||||
|
## Проверка цикла на живом стенде
|
||||||
|
|
||||||
|
- `terraform destroy` (провайдер `2.0.22`): `0 added, 0 changed, 5 destroyed`, ошибок нет.
|
||||||
|
Кластер и vDC ушли в `suspend`, эдж остался `running` с `ipSpaceName=internet-ipv4-v1`, квота IP — `count=3`,
|
||||||
|
state пуст. Предупреждения: «заморожен, а не удалён» ×2 (кластер, vDC), «оставлен как есть» (эдж),
|
||||||
|
«Аллокация IP не снималась» (квота), «SNAT не выключался».
|
||||||
|
- Обратный ход (`apply` → adopt + `resume`) — следующий шаг, запускает пользователь.
|
||||||
|
|
||||||
|
Бэкап перед правкой: `TMP/backup_2026-09-24/shturval.tf.before-adopt`.
|
||||||
|
|
||||||
|
## Итоги диагностики кластера `shturval-dev-00`
|
||||||
|
|
||||||
|
- Кластер здоров: 2 ноды Ready (k8s v1.35.1, платформа 2.14.0), `shturvalserviceconfigs` 41/41 `ready`,
|
||||||
|
`nodeconfigitems` 4/4, endpoints есть у всех 35 сервисов.
|
||||||
|
- Единственный «мусор» — 4 подвисших пода `kube-system/shturval-init-job` (3 Error + 1 Unknown) при
|
||||||
|
`Complete 1/1` у Job. Причина: webhook-и Штурвала недоступны, пока Cilium не поднял сеть
|
||||||
|
(`connect: operation not permitted`). Самоочистка по `ttlSecondsAfterFinished: 86400` (~25.09 14:31 UTC).
|
||||||
|
- Счётчики ЛК расшифрованы: `Pods` = готовые/всего (без Completed), «Системные сервисы» = число сервисов в режиме
|
||||||
|
`auto` (17/24 во время установки → 24/24), «Ingress» — домен-шаблон, «Конфигурация узлов» — NodeConfigItems.
|
||||||
|
|
||||||
|
## Итоги разбора destroy
|
||||||
|
|
||||||
|
- `nubes_vc_org_ip_allocation` при `keep_on_destroy = false` отправляет `count=0` и падает, если квота занята
|
||||||
|
(2 адреса держит кластер: `.146` API, `.148` ingress; `suspend` их не освобождает).
|
||||||
|
- Упавший destroy оставляет «рваное» состояние: SNAT снят, edge/vDC/квота — нет.
|
||||||
|
- `adopt_existing_on_create = true` решает восстановление: apply усыновил инстанс `94627ff4-…` и сам сделал
|
||||||
|
`resume`; SNAT восстановлен (`internet-ipv4-v1`). Проверено на живом стенде.
|
||||||
|
|
||||||
|
## Принятое направление (дизайн)
|
||||||
|
|
||||||
|
Три режима destroy в одной общей логике: `delete` (дефолт), `suspend` (где сервис умеет),
|
||||||
|
`keep` → `state_only` (эдж, SNAT, квота IP). Реализация — через генератор
|
||||||
|
(`TOOLS/resource-generator`), без ручных правок `resources_gen/`. Дефолты провайдера остаются разрушающими,
|
||||||
|
freeze включается явно в `.tf` стенда; в `Delete` обязательны предупреждения («заморожено», «оставлено как есть»).
|
||||||
|
Полный teardown — только явный opt-out и в порядке: кластер → `count=0` → SNAT → эдж → vDC.
|
||||||
@@ -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` |
|
| **PROD** | `nubes` | `1.*` | `TOOLS/config/prod` |
|
||||||
| **DEV** | `nubes-dev` | `3.*` | `TOOLS/config/dev` |
|
| **DEV** | `nubes-dev` | `2.*` | `TOOLS/config/dev` |
|
||||||
| **TEST** | `nubes-test` | `5.*` | `TOOLS/config/test` |
|
| **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...
|
│ NUBES_API_ENDPOINT = ...dev...
|
||||||
│ TOKEN_FILE = secrets/dev.token
|
│ TOKEN_FILE = secrets/dev.token
|
||||||
│ NAMESPACE = nubes-dev
|
│ NAMESPACE = nubes-dev
|
||||||
│ VERSION = 3.x.x
|
│ VERSION = 2.x.x ← актуальную брать из VERSIONS.md
|
||||||
│
|
│
|
||||||
├── test/profile.env
|
├── test/profile.env
|
||||||
└── prod/profile.env
|
└── prod/profile.env
|
||||||
@@ -65,7 +69,7 @@ cd ~/tf_provider
|
|||||||
### Шаг 3 — Собрать и залить в реестр
|
### Шаг 3 — Собрать и залить в реестр
|
||||||
|
|
||||||
```bash
|
```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.
|
Компилирует (linux/windows/darwin), подписывает GPG, заливает в S3.
|
||||||
@@ -75,7 +79,7 @@ cd ~/tf_provider
|
|||||||
```bash
|
```bash
|
||||||
./TOOLS/scripts/01_generate_yamls.sh --profile TOOLS/config/dev && \
|
./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/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)
|
## Быстрая заливка (без перегенерации YAML/Go)
|
||||||
@@ -83,14 +87,14 @@ cd ~/tf_provider
|
|||||||
Если YAML'ы и Go-код уже сгенерированы и не менялись — только шаг 3:
|
Если YAML'ы и Go-код уже сгенерированы и не менялись — только шаг 3:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# DEV
|
# DEV (2.*)
|
||||||
./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
|
||||||
|
|
||||||
# TEST
|
# TEST (3.*)
|
||||||
./TOOLS/scripts/03_build_and_upload_provider.sh --profile TOOLS/config/test 5.1.17
|
./TOOLS/scripts/03_build_and_upload_provider.sh --profile TOOLS/config/test 3.0.1
|
||||||
|
|
||||||
# PROD
|
# PROD (1.*)
|
||||||
./TOOLS/scripts/03_build_and_upload_provider.sh --profile TOOLS/config/prod 2.1.23
|
./TOOLS/scripts/03_build_and_upload_provider.sh --profile TOOLS/config/prod 1.0.1
|
||||||
```
|
```
|
||||||
|
|
||||||
Креды S3 подхватываются из `secrets/.s3cfg_registry`. Или через env:
|
Креды 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-конфиг пользователя
|
## Terraform-конфиг пользователя
|
||||||
|
|
||||||
@@ -138,7 +142,7 @@ terraform {
|
|||||||
required_providers {
|
required_providers {
|
||||||
nubes = {
|
nubes = {
|
||||||
source = "tf-registry.containerk8s.services.ngcloud.ru/nubes-dev/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 — Инструкции для агента
|
# План миграции в 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)
|
**Создан:** 2026-03-13 (Opus 4.6)
|
||||||
**Исполнитель:** Sonnet 4.6
|
**Исполнитель:** Sonnet 4.6
|
||||||
**Статус:** Ожидает исполнения
|
**Статус:** Ожидает исполнения
|
||||||
@@ -24,7 +31,7 @@ Nubes (nubes.ru) — российский cloud-провайдер, собств
|
|||||||
**Обязательно прочитать перед работой:**
|
**Обязательно прочитать перед работой:**
|
||||||
- `REPO_CONTENTS.md` — карта репозитория
|
- `REPO_CONTENTS.md` — карта репозитория
|
||||||
- `.github/copilot-instructions.md` — правила работы (IMMUTABILITY POLICY)
|
- `.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 -->
|
<!-- Актуальный API: https://lk-api-gateway.ngcloud.ru/api/v1/svc -->
|
||||||
# How It Was Done — Developer Guide (закрытая страница)
|
# 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
|
**Filename & Versioning:** howitwasdone.md / 2026‑02‑04 / Draft v1
|
||||||
|
|
||||||
Этот документ — единый технический мануал. Он доступен только по прямой ссылке и не включён в публичную навигацию.
|
Этот документ — единый технический мануал. Он доступен только по прямой ссылке и не включён в публичную навигацию.
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
# ПЛАН: живой прогон цепочки на DEV_STAND/FullPipe (2026-09-24)
|
||||||
|
|
||||||
|
> Стенд: dev, орга **`organ`** (`57eeacd1-dc7f-4a52-b903-7e5f7d3c1164`, realm `sandbox.nubes.ru`, тип `saas`,
|
||||||
|
> CD-имя `WZ01325-saas`). Провайдер `2.0.19` (`terraform init -upgrade` уже сделан, `validate` — Success).
|
||||||
|
> **`apply`/`destroy` запускает только пользователь.**
|
||||||
|
|
||||||
|
## 0. Что уже готово
|
||||||
|
|
||||||
|
- Ресурсы `nubes_vc_org_ip_allocation` (modify `vIPConfigure`) и `nubes_vc_nsxt_snat` (modify `ipSpaceName`) —
|
||||||
|
в провайдере, собраны в `2.0.19`, залиты в `nubes-dev`, есть unit-тесты канонизации.
|
||||||
|
- Конфиг стенда: `DEV_STAND/FullPipe/` — `vdc.tf`, `edge.tf`, `modifiers.tf` (аллокация после эджа, затем SNAT),
|
||||||
|
`organization = "organ"` + `org_uid`.
|
||||||
|
- Орга создана вручную (в tf её нет) — по решению пользователя.
|
||||||
|
|
||||||
|
## 1. Цель прогона
|
||||||
|
|
||||||
|
Проверить **одним `apply`**: `vdc → edge → IP на орге → SNAT`, затем чистый повторный `plan` и корректный
|
||||||
|
`destroy`. Это первый живой прогон обоих новых ресурсов: CRUD до сих пор не проверялся.
|
||||||
|
|
||||||
|
## 2. Перед прогоном (проверить значения)
|
||||||
|
|
||||||
|
1. `vdc_network_provider` (`snb1`), `vdc_provider_vdc` (`Intel Broadwell 2.4`), `vdc_storage_config` (`SATA`) —
|
||||||
|
убедиться в ЛК, что доступны для орги `organ` (значения брались из ЛК для прежней орги).
|
||||||
|
2. `ip_space_name` — сначала может быть недоступен: **список ipSpace в ЛК падает** (`Can't cast Complex Object
|
||||||
|
Type Struct to String`), пока нет vDC/эджа. Брать имя из прежних HAR: `internet-ipv4-v1`.
|
||||||
|
3. `ip_count` — `"3"` (строка).
|
||||||
|
|
||||||
|
## 3. Шаги прогона (пользователь)
|
||||||
|
|
||||||
|
| # | Команда | Ожидаемый результат |
|
||||||
|
|---|---|---|
|
||||||
|
| 1 | `terraform plan` | создание: `nubes_vc_vdc.vdc` → `nubes_vc_nsxt.edge` → `nubes_vc_org_ip_allocation.org_ip` → `nubes_vc_nsxt_snat.snat`; порядка не меньше |
|
||||||
|
| 2 | `terraform apply` | всё создаётся за один проход |
|
||||||
|
| 3 | `terraform plan` (повторно) | **пустой** — главный тест канонизации (иначе вечный diff) |
|
||||||
|
| 4 | проверить API (см. §4) | `vIPConfigure` и `ipSpaceName` в live-состоянии |
|
||||||
|
| 5 | изменить `ip_count` 3 → 2, `plan`+`apply` | меняется только аллокация, state сходится |
|
||||||
|
| 6 | `terraform destroy` | порядок `snat (no-needed)` → `org_ip (count=0)` → `edge` → `vdc`; орги не касается |
|
||||||
|
|
||||||
|
## 4. Что проверять и чем
|
||||||
|
|
||||||
|
```bash
|
||||||
|
TOK=$(tr -d '\n' < secrets/narodDEV.token) # токен орги organ
|
||||||
|
# состояние орги
|
||||||
|
curl -s -H "Authorization: Bearer $TOK" 'https://lk-api-gateway-dev.ngcloud.ru/api/v1/svc/instances/<org_uid>'
|
||||||
|
# состояние эджа
|
||||||
|
curl -s -H "Authorization: Bearer $TOK" 'https://lk-api-gateway-dev.ngcloud.ru/api/v1/svc/instances/<nsxt_uid>'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Гипотезы, которые прогон подтверждает/опровергает:**
|
||||||
|
|
||||||
|
1. **Имена live-ключей**: `state.params.vIPConfigure` (орга) и `state.params.ipSpaceName` (эдж) — взяты из HAR,
|
||||||
|
кодом не проверены. Если Read вернёт не то → увидим дрейф/пустое значение.
|
||||||
|
2. **Частичный payload не затирает остальное**: SNAT-модификация шлёт только `372`; `needEnableAVI`
|
||||||
|
и `virtualServicesCount` должны остаться прежними (`true` / `1`), т.к. досылаются из live
|
||||||
|
(`core/operation_run_bycode.go`). Проверить в состоянии эджа до/после.
|
||||||
|
3. **Один `apply`** проходит целиком без второго прогона (ради этого и делались ресурсы).
|
||||||
|
4. **Нет вечного diff** после apply (канонизация `vip_configure`).
|
||||||
|
5. **`Required` + пустое live** не даёт ошибок (лечение из ревью).
|
||||||
|
|
||||||
|
## 5. Точки отказа и что делать
|
||||||
|
|
||||||
|
| Симптом | Вероятная причина | Действие |
|
||||||
|
|---|---|---|
|
||||||
|
| аллокация падает `Can't cast ... Struct to String` | платформа ещё не видит `job.vcd.networkProvider`/`providerGateway` (эдж/VDC не в состоянии) | проверить порядок и фактическое состояние эджа; при необходимости — пауза/повторный `apply` |
|
||||||
|
| `Provider produced inconsistent result after apply` на `vdc`/`edge` | read-back перекрыл план (известный класс дефектов) | записать в NOTES, разбирать отдельно (это уже не про наши ресурсы) |
|
||||||
|
| повторный `plan` не пустой | порядок ключей/формат не сошлись | сверить, что вернул live, с `formatVipConfigure` |
|
||||||
|
| SNAT не включился | `372` не доехал / неверное имя ipSpace | проверить `state.params.ipSpaceName` эджа и лог операции |
|
||||||
|
| `destroy` падает | обратный modify на живой/мёртвый родитель | смотреть тексты диагностик ресурсов (мы развели: ошибка API ≠ «родителя нет») |
|
||||||
|
|
||||||
|
## 6. После прогона
|
||||||
|
|
||||||
|
1. Отчёт в `NOTES/30_analysis/` — что прошло, что упало, с HAR/логами.
|
||||||
|
2. Обновить память репозитория (подтверждённые факты вместо гипотез).
|
||||||
|
3. Если найдутся баги — отдельные коммиты + при необходимости новый релиз провайдера.
|
||||||
|
4. Публикация документации (`04_build_and_publish_docs.sh`) — отдельной командой.
|
||||||
|
|
||||||
|
**Не входит в этот прогон:** кластер Штурвал (`nubes_k8s_shturval_cluster`) — отдельным шагом, после того как
|
||||||
|
SNAT подтверждён.
|
||||||
@@ -0,0 +1,227 @@
|
|||||||
|
# ПЛАН: два ресурса-модификатора для цепочки Штурвала (2026-09-24)
|
||||||
|
|
||||||
|
> Статус: **план, не реализовано**. Отправляется на ревью Opus.
|
||||||
|
> Решения приняты пользователем: 2 ресурса сейчас, универсальность потом; орга — не наша (адресация по uid);
|
||||||
|
> «один ресурс = весь массив `vIPConfigure`»; тип атрибута — String+JSON; apply — только пользователь.
|
||||||
|
|
||||||
|
## 1. Цель
|
||||||
|
|
||||||
|
Дать клиенту возможность собрать цепочку **одним `apply`**:
|
||||||
|
|
||||||
|
```
|
||||||
|
nubes_vc_org (вне state, адресация по uid)
|
||||||
|
nubes_vc_vdc → nubes_vc_nsxt
|
||||||
|
nubes_vc_org_ip_allocation (modify 662, vIPConfigure) ← новый ресурс
|
||||||
|
nubes_vc_nsxt_snat (modify 372, ipSpaceName) ← новый ресурс
|
||||||
|
nubes_k8s_shturval_cluster
|
||||||
|
```
|
||||||
|
|
||||||
|
Сейчас это невозможно: `Create` не отправляет modify-only параметры, а `Update` — второй прогон.
|
||||||
|
|
||||||
|
## 2. Вне scope
|
||||||
|
|
||||||
|
- Универсальный механизм (реестр модификаторов, генераторные метки) — потом.
|
||||||
|
- `vcExternalIp` — не разбирали.
|
||||||
|
- Правка генератора по modify-only (см. §7) — отдельный этап, требует решения.
|
||||||
|
|
||||||
|
## 3. Ресурс 1 — `nubes_vc_org_ip_allocation`
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| Файл | `provider/internal/resources_core/org_ip_allocation_resource.go` (новый, hand-written) |
|
||||||
|
| Регистрация | `provider/internal/provider/provider.go`, `Resources()` (рядом с `NewServiceOperationResource`) |
|
||||||
|
| Атрибуты | `org_uid` — String, Required; `vip_configure` — String (JSON `[{"name":..,"count":..}]`), Required, нормализация JSON как в `resources_core/json_planmodifier.go`; `keep_on_destroy` — Bool, Optional, default `false` |
|
||||||
|
| ID | `org_uid` (один ресурс на оргу; массив целиком) |
|
||||||
|
| Create/Update | `modify` на инстансе орги: `vIPConfigure` = JSON-массив целиком (replace-семантика). Путь: `core.RunInstanceOperationUniversalByCode` (или обёртка `resources_core`), под `LockInstance(org_uid)` |
|
||||||
|
| Read | `core.GetInstanceStateParams(org_uid)` → ключ `vIPConfigure`; пустое/`[{}]`/`count=0` → нормализовать; родитель 404/deleted → `RemoveResource` (`resources_core.ShouldRemoveFromState`). **Нужен нормализующий planmodifier** (аналог JSON-модификатора), иначе вечный дрейф при плановом 3→0 (ревью Opus, п.3) |
|
||||||
|
| Delete | `keep_on_destroy=true` → no-op + Warning. Иначе: родитель жив → modify с `count="0"` по каждому элементу (**строкой**, как в HAR; форма проверена тестом 09-22) + Warning; родитель мёртв → no-op + Warning. Массив `[]` НЕ отправлять — не проверен (ревью Opus, п.2) |
|
||||||
|
| Import | passthrough по `org_uid` |
|
||||||
|
|
||||||
|
## 4. Ресурс 2 — `nubes_vc_nsxt_snat`
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| Файл | `provider/internal/resources_core/nsxt_snat_resource.go` (новый) |
|
||||||
|
| Атрибуты | `nsxt_uid` — String, Required; `ip_space_name` — String, Required (`no-needed` = SNAT выключен, канон из HAR); `keep_on_destroy` — Bool, Optional, default `false` |
|
||||||
|
| ID | `nsxt_uid` |
|
||||||
|
| Create/Update | `modify` 372 = `ip_space_name`. Отправляется **только** 372 (остальные досыпаются из live — проверить, см. §8 вопрос 1) |
|
||||||
|
| Read | live `ipSpaceName` из `state.params`; отсутствует или `no-needed` → null; родитель мёртв → `RemoveResource` |
|
||||||
|
| Delete | inverse: `modify` с `ipSpaceName = "no-needed"` (канон, подтверждён HAR) |
|
||||||
|
| Import | passthrough по `nsxt_uid` |
|
||||||
|
|
||||||
|
## 5. Зависимости и порядок
|
||||||
|
|
||||||
|
```
|
||||||
|
nubes_vc_nsxt → nubes_vc_org_ip_allocation → nubes_vc_nsxt_snat → k8s cluster
|
||||||
|
```
|
||||||
|
|
||||||
|
- SNAT обязан зависеть от org-IP: имя ipSpace берётся из аллокации (ребра графа TF не видит — связь по имени).
|
||||||
|
- Destroy пойдёт обратно: cluster → SNAT (`no-needed`) → org-IP (`count=0`) → nsxt → vdc.
|
||||||
|
- Инвариант: destroy модификаторов **не трогает** саму оргу.
|
||||||
|
|
||||||
|
## 6. Этапы работ (последовательность)
|
||||||
|
|
||||||
|
1. **Проверка по коду** (чтение): приоритет live→paramValue→default при дозаполнении параметров; `instance.go:478` (что именно эмитит Update).
|
||||||
|
2. `nubes_vc_org_ip_allocation` + регистрация + unit-тесты (нормализация JSON, чтение `[{}]`, Delete-ветки).
|
||||||
|
3. `nubes_vc_nsxt_snat` + регистрация + unit-тесты.
|
||||||
|
4. Общие хелперы в `resources_core` (если дублируются).
|
||||||
|
5. Живой прогон на dev (**apply — пользователь**): `FullPipe`, орга **saas** (`organization_type = "saas"`, иначе коллизия имени `WZ03709-iaas`).
|
||||||
|
6. Проверки после прогона: `plan` чистый (нет дрейфа), SNAT включён в одном apply, `destroy` не падает.
|
||||||
|
7. Документация: `HOW_TO/`/`docs/`, `VERSIONS.md`, коммиты по смыслу.
|
||||||
|
|
||||||
|
## 7. Отдельный этап (требует решения): генератор
|
||||||
|
|
||||||
|
Причина — инцидент: создание `nubes_vc_org` с `v_ip_configure` даёт `inconsistent result after apply`
|
||||||
|
(платформа после create отдаёт `vIPConfigure: [{}]`, read-back перекрывает план).
|
||||||
|
|
||||||
|
Минимальные правки генератора (по Opus):
|
||||||
|
- **а)** modify-only параметр → **Optional+Computed** + `Deprecated` + не отправлять в `Update` (переход без breaking; удаление атрибута — только в следующем major);
|
||||||
|
- **б)** исключить modify-only поля из **create-read-back** (`InputField`).
|
||||||
|
|
||||||
|
**Не реализуем в этом этапе** — ждём решения пользователя (правка генератора задевает все сервисы).
|
||||||
|
|
||||||
|
> ⚠️ По ревью Opus (2026-09-24) пункт **§7б — обязательное условие**, а не опциональное:
|
||||||
|
> без исключения modify-only из create-read-back при переходном варианте будет борьба за поле
|
||||||
|
> между instance-ресурсом и модификатором. Пункт остаётся обязательным follow-up.
|
||||||
|
>
|
||||||
|
> 📌 Раунд 3: §7б выделяется в **отдельный релиз A** (универсально, схема не меняется, non-breaking,
|
||||||
|
> полностью закрывает инцидент `inconsistent result` на create vc_org). Пункты §7а + §7в — **релиз B**
|
||||||
|
> вместе с новыми ресурсами.
|
||||||
|
|
||||||
|
## 8. Вопросы для ревью Opus
|
||||||
|
|
||||||
|
1. Верно ли, что `RunInstanceOperationUniversalByCode` дозаполняет незаданные параметры из **live `state.params`**
|
||||||
|
(а не из дефолтов формы)? Если да — SNAT-ресурс может шлать только 372. Если нет — нужен явный pre-read+merge.
|
||||||
|
2. Delete для «весь массив»: слать `[{name, count:"0"}]` (проверено тестом) или `[]` (не проверено)? Что безопаснее
|
||||||
|
и не оставит ли `[]` элемент в state платформы?
|
||||||
|
3. Read-нормализация: считать ли `count="0"` и `[{}]` одним состоянием «пусто»? Не даст ли это ложный дрейф
|
||||||
|
при плановом уменьшении 3 → 0?
|
||||||
|
4. Переходный вариант (Deprecated + Optional+Computed, instance больше не шлёт параметр): не появится ли дрейф,
|
||||||
|
когда значение выставил модификатор, а instance-ресурс его только читает?
|
||||||
|
5. Достаточно ли `depends_on` (SNAT → org-IP) для корректного destroy, если org-IP-модификатор должен
|
||||||
|
уничтожиться **до** эджа? Нужны ли дополнительные рёбра?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Ревью Opus (2026-09-24, отдельный чат)
|
||||||
|
|
||||||
|
**Вердикт фактуры:** оба документа (план и `HAR_FRESH_CREATE_2026-09-24.md`) проверены по коду — факты верны,
|
||||||
|
ссылки на пути точны.
|
||||||
|
|
||||||
|
**Ответы на вопросы §8:**
|
||||||
|
|
||||||
|
1. **Подтверждено кодом.** `operation_run_bycode.go:108-142` дозаполняет все незаданные параметры по приоритету
|
||||||
|
**live `state.params` → `paramValue` формы → `defaultValue`**; если ничего нет — параметр пропускается.
|
||||||
|
SNAT-ресурс может шлать только 372, pre-read+merge НЕ нужен.
|
||||||
|
2. Слать `[{name, count:"0"}]`. `[]` не проверен, риск пустого payload/reset.
|
||||||
|
3. `count="0"`, `[{}]`, пустой массив — одно состояние «пусто» при Read. Иначе `[{}]` после create даёт ложный
|
||||||
|
дрейф; и для случая 3→0 нужен нормализующий planmodifier.
|
||||||
|
4. **Дрейф возможен** в переходном варианте (борьба за поле с read-back instance-ресурса) → §7б обязателен.
|
||||||
|
5. `depends_on` достаточно: TF развернёт граф, SNAT уничтожится до org-IP. Доп. рёбер не нужно при условии,
|
||||||
|
что оба модификатора зависят от `nubes_vc_nsxt`, а кластер — от SNAT.
|
||||||
|
|
||||||
|
**Замечания кодеру:**
|
||||||
|
- Два новых ресурса **не закрывают** инцидент `inconsistent result` на `nubes_vc_org` (Required-поле остаётся):
|
||||||
|
§7 — обязательный follow-up, не «потом».
|
||||||
|
- `count` в payload — **строка** `"0"` (в HAR всегда строка); зафиксировать тип явно.
|
||||||
|
- Стенд: орга **`saas`**, иначе коллизия `WZ03709-iaas`.
|
||||||
|
|
||||||
|
**Фиксатор:** эпоха `kind: modifier` отменена — ветку не переиспользовать; новые ресурсы hand-written
|
||||||
|
в `resources_core`, без реестра модификаторов.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Раунд 3 — вопрос Опусу: «это не поломает ничего?» (составлен 2026-09-24)
|
||||||
|
|
||||||
|
**Контекст (факт).** Правка шаблона `templates/instance.go` действует на все ресурсы. Замер по
|
||||||
|
`generated/dev/resources_yaml/*.yaml`: modify-only параметры есть только у **5 сервисов** —
|
||||||
|
`19_vc_org` (`vIPConfigure`), `22_vc_nsxt` (`ipSpaceName`), `12_s3` (`maxBucketsPerUser`,
|
||||||
|
`maxObjectsPerBucket`, `maxSizeGbPerUser`), `90_postgres` (`refreshCert`), `109_zones_v2` (`records`).
|
||||||
|
Цель правки — только первые два; у остальных трёх это рабочие атрибуты `Update`.
|
||||||
|
|
||||||
|
**Вопросы:**
|
||||||
|
|
||||||
|
1. **Критерий отбора.** Предлагается признак в спеке (`owned_by_modifier: true`). Это доменная метка в
|
||||||
|
универсальном YAML, что противоречит прежнему канону «YAML без доменных меток». Какой критерий корректен
|
||||||
|
в вашей архитектуре: spec-флаг, «required только в modify» (тогда ловится `vIPConfigure`, но **не**
|
||||||
|
`ipSpaceName` — он `required: false`), или явный список в генераторе?
|
||||||
|
2. **Безопасность (б)** (исключить modify-only из create-read-back): безопасно ли это для всех 5 сервисов,
|
||||||
|
или у s3/postgres/zones read-back нужен (иначе drift/потеря значения в state)?
|
||||||
|
3. **Поведение для существующих конфигов.** У тех, кто уже пишет `v_ip_configure`/`ip_space_name` в `.tf`,
|
||||||
|
после (в) модификация молча перестанет отправляться. Правильно ли молчание, или нужно явное падение
|
||||||
|
(ошибка «параметр управляется ресурсом `…ip_allocation`») — и как это сделать, если схема общая?
|
||||||
|
4. **Снятие Required у 5 сервисов** — не ломает ли `UseStateForUnknown`/JSON-planmodifier и не порождает
|
||||||
|
ли drift у тех, у кого поле было обязательным и уже заполнено?
|
||||||
|
5. **Порядок релиза.** Правильно ли разводить: релиз A — только (б) (чинит create орги, ничего больше
|
||||||
|
не трогает), релиз B — (а)+(в) вместе с новыми ресурсами-модификаторами?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Ответы Opus (раунд 3)
|
||||||
|
|
||||||
|
1. **Критерий отбора.** Структурный признак «modify-only = есть в `modifyParams`, нет в `createParams`»
|
||||||
|
(симметрично `ComputeCreateOnly`) — факт спеки, но он ловит **все 5** сервисов и не отличает
|
||||||
|
«управляется модификатором» от «рабочий Update-атрибут». «Required только в modify» неполон
|
||||||
|
(пропускает `ipSpaceName`, `required:false`). **Автопризнака не существует — это доменное знание.**
|
||||||
|
`owned_by_modifier: true` в пер-сервисном YAML — отвергнуть (нарушает канон);
|
||||||
|
правильно — **явный список в конфиге генератора**.
|
||||||
|
2. **Безопасность (б): безопасно для всех 5.** Read-back в create кладёт в state пост-create дефолт
|
||||||
|
(`[{}]`), которого юзер не задавал — это и есть источник `inconsistent result`. Create их и так не шлёт.
|
||||||
|
**Steady-state Read и Update read-back их по-прежнему перечитывают**, поэтому дрейф не теряется;
|
||||||
|
(б) убирает только бессмысленную перезапись сразу после create. s3/postgres/zones не страдают.
|
||||||
|
3. **Существующие конфиги.** Жёстко падать нельзя (схема общая, «владелец» — доменное знание, сломает state).
|
||||||
|
Правильно — `Deprecated` с текстом «управляется ресурсом `…ip_allocation`» → warning на каждом plan.
|
||||||
|
Молчаливое прекращение отправки — плохой UX, не делать. Удаление атрибута — только в следующий major.
|
||||||
|
4. **Снятие Required.** Затрагивает только `vIPConfigure` (`ipSpaceName` уже Optional).
|
||||||
|
`Optional+Computed` — штатный безопасный переход; `UseStateForUnknown` гасит unknown и drift не создаёт;
|
||||||
|
у заполненных полей значение удержится через read-back. Борьба за поле снимается (б)+(в).
|
||||||
|
5. **Порядок релиза — подтверждён:**
|
||||||
|
- **A — только (б):** универсально, схема не меняется, non-breaking, **полностью закрывает** инцидент
|
||||||
|
`inconsistent result` на create `nubes_vc_org`; s3/postgres/zones не трогает.
|
||||||
|
- **B — (а)+(в) + новые ресурсы** (Deprecated на delegated-параметры, отцеп от read-back/send).
|
||||||
|
|
||||||
|
**Следствие для наших решений:** критерий «кто делегируется» задаётся явным списком в конфиге генератора;
|
||||||
|
работа разбивается на релиз A (маленький, безопасный) и релиз B (ресурсы + отцепка).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. ⚠️ Уточнение пользователя (2026-09-24): оргу делаем РУКАМИ в ЛК
|
||||||
|
|
||||||
|
**Факт:** орга создаётся вручную в ЛК и **в Terraform не заводится** — она одна на всё.
|
||||||
|
В tf она используется только как uid для модификаций.
|
||||||
|
|
||||||
|
**Что это меняет:**
|
||||||
|
|
||||||
|
1. `nubes_vc_org` в конфигурации **не используется** → дефект «`inconsistent result after apply` при create орги»
|
||||||
|
для этой задачи **не блокер** (остаётся латентным дефектом ресурса).
|
||||||
|
2. **Релиз A (правка create-read-back) становится необязательным** для цепочки Штурвала.
|
||||||
|
3. `nubes_vc_nsxt`: править генератор **тоже не нужно** — достаточно **не задавать** `ip_space_name` в `.tf`.
|
||||||
|
Атрибут Optional+Computed: SNAT выставит модификатор, read-back подхватит значение в state, дрейфа не будет.
|
||||||
|
4. Итог: для задачи нужны **только два новых ресурса** (`nubes_vc_org_ip_allocation`, `nubes_vc_nsxt_snat`),
|
||||||
|
оба адресуются по uid родителя. Правки генератора (§7, релизы A/B) — **отдельная тема**, не вход в эту работу.
|
||||||
|
|
||||||
|
**Открытый вопрос:** эдж (`nubes_vc_nsxt`) создаётся Terraform или тоже руками? На состав работ не влияет
|
||||||
|
(в обоих случаях нужны те же два ресурса), влияет только на пример конфигурации.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 13. Статус работ (обновлено 2026-09-24)
|
||||||
|
|
||||||
|
**Сделано:**
|
||||||
|
- ✅ Проверка по коду: `RunInstanceOperationUniversalByCode` дозаполняет незаданные параметры
|
||||||
|
(live → paramValue → default) — частичный payload безопасен.
|
||||||
|
- ✅ `nubes_vc_org_ip_allocation` — `provider/internal/resources_core/org_ip_allocation_resource.go`
|
||||||
|
(коммит `22cf259`) + unit-тесты нормализации (`[{}]` → «пусто»).
|
||||||
|
- ✅ `nubes_vc_nsxt_snat` — `provider/internal/resources_core/nsxt_snat_resource.go` (коммит `80d82a1`).
|
||||||
|
- ✅ Регистрация в `provider/internal/provider/provider.go` (коммит `73a7459`).
|
||||||
|
- ✅ Пример конфигурации: `tf_examples/modify_resources/` (README, `main.tf`, `terraform.tfvars.example`).
|
||||||
|
⚠️ Каталог `tf_examples/` в `.gitignore:16` — пример локальный, как и остальные примеры в этом каталоге.
|
||||||
|
- ✅ Публичная страница: `docs/curated/modifiers/org_ip_and_snat.md` + nav (коммит `62abcd6`).
|
||||||
|
- ✅ Ветка-снимок состояния: `save/state-before-modify-resources-2026-09-24`.
|
||||||
|
- ✅ `go build` / `go vet` / `go test ./...` — зелёные.
|
||||||
|
|
||||||
|
**Не сделано (ждёт команды пользователя):**
|
||||||
|
- ⏳ Живой прогон на dev (`FullPipe`, орга `saas`; `apply` — только пользователь).
|
||||||
|
- ⏳ Бамп версии провайдера, сборка и заливка (`TOOLS/scripts/03_build_and_upload_provider.sh`).
|
||||||
|
- ⏳ Публикация документации (`04_build_and_publish_docs.sh`).
|
||||||
|
- ⏳ Решение по правке генератора (релизы A/B, §7) — отдельная тема.
|
||||||
@@ -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)
|
# План: перегенерация провайдеров всех стендов (версия 0.0.1)
|
||||||
|
|
||||||
> Для Flash. Генерацию выполняет Flash по этому плану. Документацию НЕ трогать.
|
> Для Flash. Генерацию выполняет Flash по этому плану. Документацию НЕ трогать.
|
||||||
@@ -53,16 +57,16 @@ cd /home/naeel/TF/tf_provider
|
|||||||
|
|
||||||
| Стенд | Namespace | Бинарники в S3 |
|
| Стенд | Namespace | Бинарники в S3 |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| dev | `nubes-dev` | `nubes-terraform-registry/tf-registry.containerk8s.services.ngcloud.ru/nubes-dev/nubes/0.0.1/` |
|
| dev | `nubes-dev` | `terraform-registry/tf-registry.containerk8s.services.ngcloud.ru/nubes-dev/nubes/0.0.1/` |
|
||||||
| test | `nubes-test` | `.../nubes-test/nubes/0.0.1/` |
|
| test | `nubes-test` | `terraform-registry/tf-registry.containerk8s.services.ngcloud.ru/nubes-test/nubes/0.0.1/` |
|
||||||
| prod | `nubes` | `.../nubes/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`).
|
- Go 1.23.1, docker 29.1.3, `mc`, GPG (`secrets/private_key.asc`, `public_key.asc`).
|
||||||
- Токены API: `secrets/{dev,test,prod}.token` на месте.
|
- Токены API: `secrets/{dev,test,prod}.token` на месте.
|
||||||
- API-эндпоинты доступны (HTTP 403 без токена — ожидаемо, токен передаёт 01).
|
- API-эндпоинты доступны (HTTP 403 без токена — ожидаемо, токен передаёт 01).
|
||||||
- `TOOLS/config/<стенд>/operation_timeouts.json` на месте.
|
- `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` (документация не нужна сейчас).
|
- НЕ запускать `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) Файлы для изучения (указать полные пути)
|
## 2) Файлы для изучения (указать полные пути)
|
||||||
- docs/ARCHITECTURE_NEW.md — архитектурный обзор
|
- NOTES/30_analysis/ARCHITECTURE_NEW.md — архитектурный обзор
|
||||||
- docs/README.md, docs/index.md — документация / навигация
|
- docs/README.md, docs/index.md — документация / навигация
|
||||||
- docs/howitwasdone.md — история решений
|
- NOTES/50_process/howitwasdone.md — история решений
|
||||||
- docs/ai_universal_provider_gen.md — процесс генерации провайдера (YAML → Go)
|
- docs/ai_universal_provider_gen.md — процесс генерации провайдера (YAML → Go)
|
||||||
- universal_rebuild/tools/gen/main.go — генератор (основная логика)
|
- universal_rebuild/tools/gen/main.go — генератор (основная логика)
|
||||||
- universal_rebuild/internal/resources_gen/* — примеры сгенерированных ресурсов
|
- 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 конкретных рекомендаций по приоритету — что добавить/изменить в генераторе или ресурсах.
|
||||||
|
```
|
||||||
@@ -0,0 +1,831 @@
|
|||||||
|
# Ревью Opus: два новых ресурса-модификатора (2026-09-24)
|
||||||
|
|
||||||
|
> Что приложено: полный код двух новых ресурсов, тестов, фрагмент регистрации, известные проблемы и вопросы.
|
||||||
|
> Репо: `tf_provider`, коммиты `22cf259`, `80d82a1`, `73a7459`. Провайдер DEV `2.0.18` собран и залит.
|
||||||
|
> **Просьба: ревью полное, включая то, что я не вижу. Код не писался под ревью — можно предлагать переписать.**
|
||||||
|
|
||||||
|
## 1. Контекст
|
||||||
|
|
||||||
|
- Организация Cloud Director (сервис 19) и сетевой шлюз периметра (сервис 22) создаются **вручную в ЛК**.
|
||||||
|
В Terraform их нет — адресуются по `uid`.
|
||||||
|
- В схемах `nubes_vc_org` / `nubes_vc_nsxt` modify-параметры **есть** (генератор мержит create+modify),
|
||||||
|
но `Create` их не отправляет → в одном `apply` цепочку не собрать. Поэтому сделаны два отдельных ресурса,
|
||||||
|
которые делают только `modify`.
|
||||||
|
- Орга и эдж — единственные ресурсы своего типа (одна орга на realm, один эдж на vDC).
|
||||||
|
|
||||||
|
## 2. Известный баг (найден после заливки, ещё НЕ исправлен)
|
||||||
|
|
||||||
|
`formatVipConfigure` (файл 1, строка 348) собирает `{"name":…,"count":…}`.
|
||||||
|
Terraform `jsonencode` сортирует ключи по алфавиту:
|
||||||
|
```
|
||||||
|
$ terraform console
|
||||||
|
> jsonencode([{name="internet-ipv4-v1", count="3"}])
|
||||||
|
"[{\"count\":\"3\",\"name\":\"internet-ipv4-v1\"}]"
|
||||||
|
```
|
||||||
|
`JsonNormalize` (приложен ниже) только компактит JSON, порядок ключей не меняет.
|
||||||
|
→ план (`count,name`) ≠ state после Read (`name,count`) → **вечный diff**.
|
||||||
|
|
||||||
|
## 3. Риски, которые я не могу проверить без живой платформы
|
||||||
|
|
||||||
|
1. `vip_configure` и `ip_space_name` — **Required**, а `Read` может вернуть `null` («аллокации нет»).
|
||||||
|
Корректно ли это для Required-атрибута (не будет ли ошибки/вечного diff)?
|
||||||
|
2. `Update` **не делает read-back** после modify — не приведёт ли это к inconsistent result / дрейфу.
|
||||||
|
3. Имена live-ключей (`vIPConfigure`, `ipSpaceName`) взяты из HAR ЛК, не сверены с кодом.
|
||||||
|
4. `setSnat`: пустая строка молча заменяется на `no-needed` (скрытое поведение).
|
||||||
|
5. CRUD живым прогоном **не проверялся вообще** — только `go build`/`vet`/юнит-тесты парсинга.
|
||||||
|
|
||||||
|
## 4. Приложенный код
|
||||||
|
|
||||||
|
### 4.1. `provider/internal/resources_core/org_ip_allocation_resource.go`
|
||||||
|
|
||||||
|
```go
|
||||||
|
package resources_core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"terraform-provider-nubes/internal/core"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/path"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ resource.Resource = &OrgIpAllocationResource{}
|
||||||
|
var _ resource.ResourceWithConfigure = &OrgIpAllocationResource{}
|
||||||
|
var _ resource.ResourceWithImportState = &OrgIpAllocationResource{}
|
||||||
|
|
||||||
|
// OrgIpAllocationResource управляет аллокацией внешних IP на СУЩЕСТВУЮЩЕЙ организации
|
||||||
|
// (сервис 19, vc_org) через операцию modify с параметром vIPConfigure (id 662).
|
||||||
|
//
|
||||||
|
// Организация НЕ управляется Terraform: она создаётся один раз вручную в ЛК
|
||||||
|
// и адресуется здесь по uid.
|
||||||
|
//
|
||||||
|
// Семантика операции — replace всего массива: переданное значение полностью заменяет
|
||||||
|
// текущую аллокацию (проверено тестом NOTES/30_analysis/ORG_IP_MODIFIER_TEST_2026-09-22.md).
|
||||||
|
// Поэтому ресурс владеет массивом ЦЕЛИКОМ, а не отдельным элементом.
|
||||||
|
type OrgIpAllocationResource struct {
|
||||||
|
client *core.UniversalClient
|
||||||
|
}
|
||||||
|
|
||||||
|
type OrgIpAllocationModel struct {
|
||||||
|
ID types.String `tfsdk:"id"`
|
||||||
|
OrgUID types.String `tfsdk:"org_uid"`
|
||||||
|
VIPConfigure types.String `tfsdk:"vip_configure"`
|
||||||
|
KeepOnDestroy types.Bool `tfsdk:"keep_on_destroy"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// vipAllocation — элемент массива vIPConfigure. count ВСЕГДА строка:
|
||||||
|
// ЛК присылает его строкой (HAR/globak.har), API принимает строкой.
|
||||||
|
type vipAllocation struct {
|
||||||
|
Name string
|
||||||
|
Count string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewOrgIpAllocationResource() resource.Resource {
|
||||||
|
return &OrgIpAllocationResource{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *OrgIpAllocationResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
|
||||||
|
resp.TypeName = req.ProviderTypeName + "_vc_org_ip_allocation"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *OrgIpAllocationResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
|
||||||
|
resp.Schema = schema.Schema{
|
||||||
|
MarkdownDescription: "Аллокация внешних IP (vIPConfigure) на существующей организации Cloud Director. " +
|
||||||
|
"Организация создаётся вручную в ЛК, ресурс адресует её по `org_uid`. " +
|
||||||
|
"Операция имеет replace-семантику: массив перезаписывается целиком.",
|
||||||
|
Attributes: map[string]schema.Attribute{
|
||||||
|
"id": schema.StringAttribute{
|
||||||
|
Computed: true,
|
||||||
|
PlanModifiers: []planmodifier.String{
|
||||||
|
stringplanmodifier.UseStateForUnknown(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"org_uid": schema.StringAttribute{
|
||||||
|
Required: true,
|
||||||
|
MarkdownDescription: "UUID существующей услуги «Организация в Cloud Director».",
|
||||||
|
PlanModifiers: []planmodifier.String{
|
||||||
|
stringplanmodifier.RequiresReplace(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"vip_configure": schema.StringAttribute{
|
||||||
|
Required: true,
|
||||||
|
MarkdownDescription: "JSON-массив аллокаций: `[{\"name\":\"internet-ipv4-v1\",\"count\":\"3\"}]`. " +
|
||||||
|
"Значение перезаписывает текущую аллокацию целиком. `count` — строка.",
|
||||||
|
PlanModifiers: []planmodifier.String{
|
||||||
|
JsonNormalize(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"keep_on_destroy": schema.BoolAttribute{
|
||||||
|
Optional: true,
|
||||||
|
Computed: true,
|
||||||
|
Default: booldefault.StaticBool(false),
|
||||||
|
MarkdownDescription: "Не снимать аллокацию IP при `destroy` (по умолчанию `false` — квота обнуляется, " +
|
||||||
|
"`count=0` по каждому элементу).",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *OrgIpAllocationResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
|
||||||
|
var plan OrgIpAllocationModel
|
||||||
|
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.applyAllocation(ctx, plan.OrgUID, plan.VIPConfigure); err != nil {
|
||||||
|
resp.Diagnostics.AddError("Ошибка клиента", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
plan.ID = types.StringValue(strings.TrimSpace(plan.OrgUID.ValueString()))
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *OrgIpAllocationResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
|
||||||
|
var plan OrgIpAllocationModel
|
||||||
|
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.applyAllocation(ctx, plan.OrgUID, plan.VIPConfigure); err != nil {
|
||||||
|
resp.Diagnostics.AddError("Ошибка клиента", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
plan.ID = types.StringValue(strings.TrimSpace(plan.OrgUID.ValueString()))
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *OrgIpAllocationResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
|
||||||
|
var state OrgIpAllocationModel
|
||||||
|
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
orgUID := strings.TrimSpace(state.OrgUID.ValueString())
|
||||||
|
if orgUID == "" || r.client == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
remove, err := ShouldRemoveFromState(ctx, r.client, orgUID)
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError("Ошибка клиента", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if remove {
|
||||||
|
// Организации больше нет — ресурс тоже не нужен.
|
||||||
|
resp.State.RemoveResource(ctx)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
live, err := r.client.GetInstanceStateParams(ctx, orgUID)
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError("Ошибка клиента", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
raw, ok := live["vIPConfigure"]
|
||||||
|
if !ok {
|
||||||
|
// Платформа не вернула параметр — считаем, что аллокации нет
|
||||||
|
// (у свежей орги ключ присутствует со значением `[{}]`, что тоже «пусто»).
|
||||||
|
state.VIPConfigure = types.StringNull()
|
||||||
|
} else {
|
||||||
|
items, parseErr := parseVipConfigure(raw)
|
||||||
|
if parseErr != nil {
|
||||||
|
resp.Diagnostics.AddError("Ошибка чтения состояния", parseErr.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(items) == 0 {
|
||||||
|
state.VIPConfigure = types.StringNull()
|
||||||
|
} else {
|
||||||
|
state.VIPConfigure = types.StringValue(formatVipConfigure(items))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state.ID = types.StringValue(orgUID)
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *OrgIpAllocationResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
|
||||||
|
var state OrgIpAllocationModel
|
||||||
|
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
orgUID := strings.TrimSpace(state.OrgUID.ValueString())
|
||||||
|
if orgUID == "" || r.client == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !state.KeepOnDestroy.IsNull() && !state.KeepOnDestroy.IsUnknown() && state.KeepOnDestroy.ValueBool() {
|
||||||
|
resp.Diagnostics.AddWarning(
|
||||||
|
"Аллокация IP не снималась",
|
||||||
|
fmt.Sprintf("keep_on_destroy = true: квота внешних IP организации %s оставлена без изменений.", orgUID),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
remove, err := ShouldRemoveFromState(ctx, r.client, orgUID)
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddWarning(
|
||||||
|
"Аллокация IP не снималась",
|
||||||
|
fmt.Sprintf("не удалось проверить существование организации %s: %s", orgUID, err),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if remove {
|
||||||
|
resp.Diagnostics.AddWarning(
|
||||||
|
"Аллокация IP не снималась",
|
||||||
|
fmt.Sprintf("организация %s не найдена — обратный modify пропущен.", orgUID),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
unlock := r.client.LockInstance(orgUID)
|
||||||
|
defer unlock()
|
||||||
|
|
||||||
|
// Имена берём из LIVE-состояния (что реально выделено), при неудаче — из конфигурации.
|
||||||
|
items := []vipAllocation{}
|
||||||
|
if live, liveErr := r.client.GetInstanceStateParams(ctx, orgUID); liveErr == nil {
|
||||||
|
if parsed, parseErr := parseVipConfigure(live["vIPConfigure"]); parseErr == nil {
|
||||||
|
items = parsed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(items) == 0 {
|
||||||
|
if parsed, parseErr := parseVipConfigure(state.VIPConfigure.ValueString()); parseErr == nil {
|
||||||
|
items = parsed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(items) == 0 {
|
||||||
|
resp.Diagnostics.AddWarning(
|
||||||
|
"Аллокация IP не снималась",
|
||||||
|
"не удалось определить выделенные ipSpace — обратный modify пропущен.",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обратный modify: тот же массив, но count=0 (форма проверена тестом 09-22).
|
||||||
|
// Пустой массив `[]` НЕ отправляем — его семантика на платформе не проверена.
|
||||||
|
zero := make([]vipAllocation, 0, len(items))
|
||||||
|
for _, item := range items {
|
||||||
|
zero = append(zero, vipAllocation{Name: item.Name, Count: "0"})
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.client.RunInstanceOperationUniversalByCode(ctx, orgUID, "modify", map[string]string{
|
||||||
|
"vIPConfigure": formatVipConfigure(zero),
|
||||||
|
}); err != nil {
|
||||||
|
resp.Diagnostics.AddError("Ошибка клиента", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.Diagnostics.AddWarning(
|
||||||
|
"Квота IP обнулена",
|
||||||
|
fmt.Sprintf("по организации %s отправлен modify с count=0: %s", orgUID, formatVipConfigure(zero)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *OrgIpAllocationResource) 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("Ошибка", "Неверный тип клиента, ожидается *core.UniversalClient")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r.client = client
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *OrgIpAllocationResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
|
||||||
|
uid := strings.TrimSpace(req.ID)
|
||||||
|
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), uid)...)
|
||||||
|
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("org_uid"), uid)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// applyAllocation отправляет modify с массивом vIPConfigure целиком.
|
||||||
|
func (r *OrgIpAllocationResource) applyAllocation(ctx context.Context, orgUID types.String, vipConfigure types.String) error {
|
||||||
|
uid := strings.TrimSpace(orgUID.ValueString())
|
||||||
|
if uid == "" {
|
||||||
|
return fmt.Errorf("org_uid обязателен")
|
||||||
|
}
|
||||||
|
if r.client == nil {
|
||||||
|
return fmt.Errorf("клиент не инициализирован")
|
||||||
|
}
|
||||||
|
|
||||||
|
items, err := parseVipConfigure(vipConfigure.ValueString())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(items) == 0 {
|
||||||
|
return fmt.Errorf("vip_configure не содержит ни одной аллокации (name+count)")
|
||||||
|
}
|
||||||
|
|
||||||
|
unlock := r.client.LockInstance(uid)
|
||||||
|
defer unlock()
|
||||||
|
|
||||||
|
// Именно ByCode (без idempotency-pre-check): pre-check сравнивает с paramValue ФОРМЫ
|
||||||
|
// операции, а это не live-состояние инстанса (см. core/modifier_compare.go и
|
||||||
|
// комментарий в core/operation_cfs.go) — можно было бы ложно пропустить modify.
|
||||||
|
return r.client.RunInstanceOperationUniversalByCode(ctx, uid, "modify", map[string]string{
|
||||||
|
"vIPConfigure": formatVipConfigure(items),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseVipConfigure разбирает значение параметра vIPConfigure.
|
||||||
|
// Пустые элементы (`{}`) — легальное состояние «не выделено» у свежей орги
|
||||||
|
// (NOTES/30_analysis/HAR_FRESH_CREATE_2026-09-24.md) и отбрасываются.
|
||||||
|
func parseVipConfigure(raw string) ([]vipAllocation, error) {
|
||||||
|
trimmed := strings.TrimSpace(raw)
|
||||||
|
if trimmed == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var items []map[string]interface{}
|
||||||
|
if err := json.Unmarshal([]byte(trimmed), &items); err != nil {
|
||||||
|
return nil, fmt.Errorf("не удалось разобрать vIPConfigure %q: %w", trimmed, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
out := make([]vipAllocation, 0, len(items))
|
||||||
|
for _, item := range items {
|
||||||
|
name := ""
|
||||||
|
if v, ok := item["name"]; ok && v != nil {
|
||||||
|
name = strings.TrimSpace(fmt.Sprint(v))
|
||||||
|
}
|
||||||
|
if name == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
count := "0"
|
||||||
|
if v, ok := item["count"]; ok && v != nil {
|
||||||
|
if parsed := strings.TrimSpace(fmt.Sprint(v)); parsed != "" {
|
||||||
|
count = parsed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out = append(out, vipAllocation{Name: name, Count: count})
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// formatVipConfigure собирает канонический payload: [{"name":"…","count":"…"}]
|
||||||
|
// (порядок ключей как в HAR; count — строка).
|
||||||
|
func formatVipConfigure(items []vipAllocation) string {
|
||||||
|
if len(items) == 0 {
|
||||||
|
return "[]"
|
||||||
|
}
|
||||||
|
parts := make([]string, 0, len(items))
|
||||||
|
for _, item := range items {
|
||||||
|
parts = append(parts, fmt.Sprintf(`{"name":%q,"count":%q}`, item.Name, item.Count)) // ← строка 348, ИСТОЧНИК БАГА
|
||||||
|
}
|
||||||
|
return "[" + strings.Join(parts, ",") + "]"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2. `provider/internal/resources_core/nsxt_snat_resource.go`
|
||||||
|
|
||||||
|
```go
|
||||||
|
package resources_core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"terraform-provider-nubes/internal/core"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/path"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ resource.Resource = &NsxtSnatResource{}
|
||||||
|
var _ resource.ResourceWithConfigure = &NsxtSnatResource{}
|
||||||
|
var _ resource.ResourceWithImportState = &NsxtSnatResource{}
|
||||||
|
|
||||||
|
// NsxtSnatResource включает/выключает SNAT у СУЩЕСТВУЮЩЕГО сетевого шлюза периметра
|
||||||
|
// (сервис 22, vc_nsxt) через операцию modify с параметром ipSpaceName (id 372).
|
||||||
|
//
|
||||||
|
// Зачем отдельный ресурс: ipSpaceName есть ТОЛЬКО в операции modify (в create его нет),
|
||||||
|
// поэтому одним ресурсом «create + modify» в одном apply не сделать.
|
||||||
|
//
|
||||||
|
// Канонические значения (HAR/edge_.har, NOTES/30_analysis/HAR_SNAT_MODIFY_FINDINGS.md):
|
||||||
|
// - включить SNAT: ip_space_name = "<имя ipSpace из аллокации организации>";
|
||||||
|
// - выключить SNAT: ip_space_name = "no-needed" (легальное значение платформы).
|
||||||
|
type NsxtSnatResource struct {
|
||||||
|
client *core.UniversalClient
|
||||||
|
}
|
||||||
|
|
||||||
|
type NsxtSnatModel struct {
|
||||||
|
ID types.String `tfsdk:"id"`
|
||||||
|
NsxtUID types.String `tfsdk:"nsxt_uid"`
|
||||||
|
IpSpaceName types.String `tfsdk:"ip_space_name"`
|
||||||
|
KeepOnDestroy types.Bool `tfsdk:"keep_on_destroy"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// noNeededIpSpace — каноническое значение «SNAT не нужен».
|
||||||
|
const noNeededIpSpace = "no-needed"
|
||||||
|
|
||||||
|
func NewNsxtSnatResource() resource.Resource {
|
||||||
|
return &NsxtSnatResource{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *NsxtSnatResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
|
||||||
|
resp.TypeName = req.ProviderTypeName + "_vc_nsxt_snat"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *NsxtSnatResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
|
||||||
|
resp.Schema = schema.Schema{
|
||||||
|
MarkdownDescription: "SNAT (ipSpaceName) на существующем сетевом шлюзе периметра. " +
|
||||||
|
"Шлюз создаётся отдельным ресурсом `nubes_vc_nsxt`, здесь задаётся только SNAT. " +
|
||||||
|
"Значение `no-needed` выключает SNAT.",
|
||||||
|
Attributes: map[string]schema.Attribute{
|
||||||
|
"id": schema.StringAttribute{
|
||||||
|
Computed: true,
|
||||||
|
PlanModifiers: []planmodifier.String{
|
||||||
|
stringplanmodifier.UseStateForUnknown(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"nsxt_uid": schema.StringAttribute{
|
||||||
|
Required: true,
|
||||||
|
MarkdownDescription: "UUID существующей услуги «Сетевой шлюз периметра (Edge)».",
|
||||||
|
PlanModifiers: []planmodifier.String{
|
||||||
|
stringplanmodifier.RequiresReplace(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"ip_space_name": schema.StringAttribute{
|
||||||
|
Required: true,
|
||||||
|
MarkdownDescription: "Имя ipSpace для внешнего IP (SNAT). Значение `no-needed` выключает SNAT. " +
|
||||||
|
"Имя должно быть выделено на организации (см. `nubes_vc_org_ip_allocation`).",
|
||||||
|
},
|
||||||
|
"keep_on_destroy": schema.BoolAttribute{
|
||||||
|
Optional: true,
|
||||||
|
Computed: true,
|
||||||
|
Default: booldefault.StaticBool(false),
|
||||||
|
MarkdownDescription: "Не выключать SNAT при `destroy` (по умолчанию `false` — отправляется " +
|
||||||
|
"`ipSpaceName = \"no-needed\"`).",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *NsxtSnatResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
|
||||||
|
var plan NsxtSnatModel
|
||||||
|
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.setSnat(ctx, plan.NsxtUID, plan.IpSpaceName); err != nil {
|
||||||
|
resp.Diagnostics.AddError("Ошибка клиента", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
plan.ID = types.StringValue(strings.TrimSpace(plan.NsxtUID.ValueString()))
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *NsxtSnatResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
|
||||||
|
var plan NsxtSnatModel
|
||||||
|
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.setSnat(ctx, plan.NsxtUID, plan.IpSpaceName); err != nil {
|
||||||
|
resp.Diagnostics.AddError("Ошибка клиента", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
plan.ID = types.StringValue(strings.TrimSpace(plan.NsxtUID.ValueString()))
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *NsxtSnatResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
|
||||||
|
var state NsxtSnatModel
|
||||||
|
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
nsxtUID := strings.TrimSpace(state.NsxtUID.ValueString())
|
||||||
|
if nsxtUID == "" || r.client == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
remove, err := ShouldRemoveFromState(ctx, r.client, nsxtUID)
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError("Ошибка клиента", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if remove {
|
||||||
|
resp.State.RemoveResource(ctx)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
live, err := r.client.GetInstanceStateParams(ctx, nsxtUID)
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError("Ошибка клиента", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ключа ipSpaceName нет, пока SNAT ни разу не включали (HAR fresh-create),
|
||||||
|
// поэтому отсутствие ключа = null. Значение "no-needed" (SNAT выключен) — реальное.
|
||||||
|
if raw, ok := live["ipSpaceName"]; !ok || strings.TrimSpace(raw) == "" {
|
||||||
|
state.IpSpaceName = types.StringNull()
|
||||||
|
} else {
|
||||||
|
state.IpSpaceName = types.StringValue(strings.TrimSpace(raw))
|
||||||
|
}
|
||||||
|
|
||||||
|
state.ID = types.StringValue(nsxtUID)
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *NsxtSnatResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
|
||||||
|
var state NsxtSnatModel
|
||||||
|
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
nsxtUID := strings.TrimSpace(state.NsxtUID.ValueString())
|
||||||
|
if nsxtUID == "" || r.client == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !state.KeepOnDestroy.IsNull() && !state.KeepOnDestroy.IsUnknown() && state.KeepOnDestroy.ValueBool() {
|
||||||
|
resp.Diagnostics.AddWarning(
|
||||||
|
"SNAT не выключался",
|
||||||
|
fmt.Sprintf("keep_on_destroy = true: ipSpaceName шлюза %s оставлен без изменений.", nsxtUID),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
remove, err := ShouldRemoveFromState(ctx, r.client, nsxtUID)
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddWarning(
|
||||||
|
"SNAT не выключался",
|
||||||
|
fmt.Sprintf("не удалось проверить существование шлюза %s: %s", nsxtUID, err),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if remove {
|
||||||
|
resp.Diagnostics.AddWarning(
|
||||||
|
"SNAT не выключался",
|
||||||
|
fmt.Sprintf("шлюз %s не найден — обратный modify пропущен.", nsxtUID),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
unlock := r.client.LockInstance(nsxtUID)
|
||||||
|
defer unlock()
|
||||||
|
|
||||||
|
// Обратный modify: каноническое «SNAT выключен» = no-needed (подтверждено HAR).
|
||||||
|
if err := r.client.RunInstanceOperationUniversalByCode(ctx, nsxtUID, "modify", map[string]string{
|
||||||
|
"ipSpaceName": noNeededIpSpace,
|
||||||
|
}); err != nil {
|
||||||
|
resp.Diagnostics.AddError("Ошибка клиента", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.Diagnostics.AddWarning(
|
||||||
|
"SNAT выключен",
|
||||||
|
fmt.Sprintf("по шлюзу %s отправлен modify с ipSpaceName = %q.", nsxtUID, noNeededIpSpace),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *NsxtSnatResource) 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("Ошибка", "Неверный тип клиента, ожидается *core.UniversalClient")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r.client = client
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *NsxtSnatResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
|
||||||
|
uid := strings.TrimSpace(req.ID)
|
||||||
|
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), uid)...)
|
||||||
|
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("nsxt_uid"), uid)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// setSnat отправляет modify только с ipSpaceName. Остальные параметры операции
|
||||||
|
// (needEnableAVI, virtualServicesCount, qosProfile, routedNetConfiguration) досылаются
|
||||||
|
// клиентом из LIVE-состояния инстанса — приоритет live → paramValue формы → default
|
||||||
|
// (core/operation_run_bycode.go), поэтому частичный payload ничего не затирает.
|
||||||
|
func (r *NsxtSnatResource) setSnat(ctx context.Context, nsxtUID types.String, ipSpaceName types.String) error {
|
||||||
|
uid := strings.TrimSpace(nsxtUID.ValueString())
|
||||||
|
if uid == "" {
|
||||||
|
return fmt.Errorf("nsxt_uid обязателен")
|
||||||
|
}
|
||||||
|
if r.client == nil {
|
||||||
|
return fmt.Errorf("клиент не инициализирован")
|
||||||
|
}
|
||||||
|
|
||||||
|
value := strings.TrimSpace(ipSpaceName.ValueString())
|
||||||
|
if value == "" {
|
||||||
|
value = noNeededIpSpace
|
||||||
|
}
|
||||||
|
|
||||||
|
unlock := r.client.LockInstance(uid)
|
||||||
|
defer unlock()
|
||||||
|
|
||||||
|
// ByCode, а не ByIdempotent: idempotency-сравнение идёт с paramValue ФОРМЫ операции,
|
||||||
|
// а не с live-состоянием инстанса — можно ложно пропустить modify.
|
||||||
|
return r.client.RunInstanceOperationUniversalByCode(ctx, uid, "modify", map[string]string{
|
||||||
|
"ipSpaceName": value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3. `provider/internal/resources_core/org_ip_allocation_test.go`
|
||||||
|
|
||||||
|
```go
|
||||||
|
package resources_core
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestParseVipConfigure_EmptyAndBroken(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
raw string
|
||||||
|
want int
|
||||||
|
}{
|
||||||
|
{"пустая строка", "", 0},
|
||||||
|
{"пустой массив", "[]", 0},
|
||||||
|
{"пустой элемент (свежая орга)", "[{}]", 0},
|
||||||
|
{"только name без count", `[{"name":"internet-ipv4-v1"}]`, 1},
|
||||||
|
{"элемент без name", `[{"count":"3"}]`, 0},
|
||||||
|
}
|
||||||
|
for _, tc := range cases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
got, err := parseVipConfigure(tc.raw)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("неожиданная ошибка: %v", err)
|
||||||
|
}
|
||||||
|
if len(got) != tc.want {
|
||||||
|
t.Fatalf("получено %d элементов, ожидалось %d (%+v)", len(got), tc.want, got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseVipConfigure_CountAsString(t *testing.T) {
|
||||||
|
got, err := parseVipConfigure(`[{"name":"internet-ipv4-v1","count":4}]`)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("неожиданная ошибка: %v", err)
|
||||||
|
}
|
||||||
|
if len(got) != 1 || got[0].Count != "4" {
|
||||||
|
t.Fatalf("ожидался count=\"4\", получено %+v", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFormatVipConfigure_Canonical(t *testing.T) {
|
||||||
|
got := formatVipConfigure([]vipAllocation{{Name: "internet-ipv4-v1", Count: "3"}})
|
||||||
|
want := `[{"name":"internet-ipv4-v1","count":"3"}]` // ← ожидание неверное: Terraform даёт count,name
|
||||||
|
if got != want {
|
||||||
|
t.Fatalf("получено %q, ожидалось %q", got, want)
|
||||||
|
}
|
||||||
|
if empty := formatVipConfigure(nil); empty != "[]" {
|
||||||
|
t.Fatalf("для пустого списка ожидалось \"[]\", получено %q", empty)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseVipConfigure_RoundTripIsStable(t *testing.T) {
|
||||||
|
raw := `[{"name":"internet-ipv4-v1","count":"4"}]`
|
||||||
|
items, err := parseVipConfigure(raw)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("неожиданная ошибка: %v", err)
|
||||||
|
}
|
||||||
|
if again := formatVipConfigure(items); again != raw {
|
||||||
|
t.Fatalf("round-trip не стабилен: %q → %q", raw, again)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseVipConfigure_InvalidJSON(t *testing.T) {
|
||||||
|
if _, err := parseVipConfigure(`{"name":"x"}`); err == nil {
|
||||||
|
t.Fatal("ожидалась ошибка на объект вместо массива")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.4. Регистрация — `provider/internal/provider/provider.go`
|
||||||
|
|
||||||
|
```go
|
||||||
|
func (p *NubesProvider) Resources(ctx context.Context) []func() resource.Resource {
|
||||||
|
resources := resources_gen.AllResources()
|
||||||
|
resources = append(resources, resources_core.NewServiceOperationResource)
|
||||||
|
// Ресурсы-модификаторы для операций, которых нет в create-схеме ресурсов-инстансов.
|
||||||
|
// Организация и шлюз создаются вручную в ЛК, поэтому адресуются по uid, а не ссылкой на ресурс.
|
||||||
|
resources = append(resources, resources_core.NewOrgIpAllocationResource)
|
||||||
|
resources = append(resources, resources_core.NewNsxtSnatResource)
|
||||||
|
return resources
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.5. Существующий plan-modifier `JsonNormalize` (`resources_core/json_planmodifier.go`)
|
||||||
|
|
||||||
|
```go
|
||||||
|
// PlanModifyString сворачивает JSON до компактного вида.
|
||||||
|
// Если значение не является корректным JSON — оставляет как есть, не добавляет ошибку.
|
||||||
|
func (m jsonNormalizePlanModifier) PlanModifyString(_ context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) {
|
||||||
|
if req.PlanValue.IsUnknown() || req.PlanValue.IsNull() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
raw := req.PlanValue.ValueString()
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if err := json.Compact(&buf, []byte(raw)); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp.PlanValue = types.StringValue(buf.String())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. Вопросы на ревью
|
||||||
|
|
||||||
|
1. **Как правильно закрыть баг порядка ключей** — (а) сортировать ключи в обоих местах (`count`,`name`);
|
||||||
|
(б) свой plan-modifier, канонизирующий ввод через parse→canonical, чтобы любой порядок от юзера сходился;
|
||||||
|
(в) отказаться от JSON-строки и сделать nested-атрибут (тогда `jsonencode` у юзера не нужен)?
|
||||||
|
Что правильно и что меньше ломает?
|
||||||
|
2. **`Required` vs `Optional+Computed`** для `vip_configure` / `ip_space_name`: `Read` может вернуть «пусто».
|
||||||
|
Корректно ли писать `null` в state для Required-атрибута, или это неверно и надо другой тип?
|
||||||
|
3. Нужен ли **read-back после Create/Update** (сейчас его нет)? Не приведёт ли отсутствие read-back
|
||||||
|
к inconsistent result или наоборот — к тому, что мы храним в state не то, что на платформе?
|
||||||
|
4. **Delete**: последовательность «`ShouldRemoveFromState` → `LockInstance` → `ByCode`» корректна?
|
||||||
|
Ошибки API при destroy — warning (как сейчас) или error?
|
||||||
|
5. **Идемпотентность**: сознательно не используем `ByIdempotent`, потому что его сравнение идёт с `paramValue`
|
||||||
|
формы, а не с live. Согласен, или есть другой способ не гонять лишний modify?
|
||||||
|
6. **Имена live-ключей** (`vIPConfigure`, `ipSpaceName`): где проверить, чтобы не полагаться на HAR?
|
||||||
|
7. **Что ещё в этом коде сломается**, чего я не вижу? Особенно: имена/семантика диагностик,
|
||||||
|
поведение `void`-возвратов, `RemoveResource` vs `RemoveResource`-в-Delete, импорт.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 6. Ответ Opus на ревью (2026-09-24)
|
||||||
|
|
||||||
|
**Вердикт:** главный блокер — **баг порядка ключей + `Required` с `null`**. Оба чинятся
|
||||||
|
канонизирующим plan-modifier'ом. Всё остальное (Configure/Import/Lock/diagnostics) — корректно.
|
||||||
|
|
||||||
|
**По вопросам:**
|
||||||
|
|
||||||
|
1. **Баг порядка ключей → вариант (б):** plan-modifier, прогоняющий значение через
|
||||||
|
`parseVipConfigure → formatVipConfigure`. Сортировка ключей (а) не спасает: `jsonencode` юзера даст
|
||||||
|
`count,name`, а `formatVipConfigure` — `name,count`; минус только nested (в). Чинить и тест
|
||||||
|
`TestFormatVipConfigure_Canonical` (ожидание в нём неверное).
|
||||||
|
2. **`Required` + `null` в `Read` = источник `Provider produced inconsistent result`.** После apply
|
||||||
|
state обязан совпасть с планом. Правильно: **не писать `null`**, хранить конфиг-значение; либо делать
|
||||||
|
атрибут `Optional`, а не `Required`.
|
||||||
|
3. **Read-back не обязателен**, но **канонизация ввода обязательна** — иначе inconsistent-result при первом
|
||||||
|
`refresh` (там и всплывёт баг п.1).
|
||||||
|
4. **Delete:** последовательность `ShouldRemoveFromState → Lock → ByCode` корректна. Но ошибки API при destroy
|
||||||
|
должны быть **error, а не warning**: иначе реальный сбой обнуления квоты замалчивается, ресурс уходит из
|
||||||
|
state, квота висит. Warning — только для «родителя уже нет».
|
||||||
|
5. **Идемпотентность:** `ByCode` выбран правильно (`ByIdempotent` сравнивает с `paramValue` формы, ложно
|
||||||
|
пропустит modify).
|
||||||
|
6. **Имена live-ключей:** в коде провайдера их нет — только HAR; сверить можно исключительно живым
|
||||||
|
`GetInstanceStateParams` (прогон). Пока это риск, а не факт.
|
||||||
|
7. **Дополнительно:**
|
||||||
|
- `setSnat`: тихая подмена `""` → `no-needed` — заменить на валидацию (ошибку).
|
||||||
|
- `nsxt_snat.Read`: `no-needed` пишется в state как реальное значение — согласовать с решением п.2.
|
||||||
|
- `applyAllocation` при пустом массиве → error, значит «снять всё» через `vip_configure` нельзя
|
||||||
|
(только destroy) — **задокументировать** в описании атрибута.
|
||||||
|
- Раздел 3 (риски живой платформы) без прогона не закрывается — остаётся открытым.
|
||||||
|
|
||||||
|
## Итог по ревью: что сделано и где ревью ошиблось
|
||||||
|
|
||||||
|
**⚠️ Совет Opus (вариант «б», канонизация в plan-modifier) — НЕВЕРЕН.** Plan-modifier не имеет права
|
||||||
|
менять значение пользовательского атрибута: Terraform отвечает
|
||||||
|
`Provider produced invalid plan: planned value does not match config value`.
|
||||||
|
Это правило описано в нашем же сгенерированном коде (`22_vc_nsxt_resource.go`, комментарий в `ModifyPlan`).
|
||||||
|
Проверено живым `terraform plan` 2026-09-24 (ошибка воспроизведена).
|
||||||
|
|
||||||
|
Правильное решение (коммит `807dfde`):
|
||||||
|
- plan-modifier удалён полностью (`JsonNormalize` тоже снят — он компактит, то есть тоже менял бы значение);
|
||||||
|
- в `Read` — смысловое сравнение `vipAllocationsEqual`: если смысл совпал (порядок ключей/формат не важны),
|
||||||
|
значение пользователя НЕ переписывается; пишется только реальный дрейф.
|
||||||
|
|
||||||
|
**Выполнено корректно:**
|
||||||
|
1. ✅ Убран plan-modifier, менявший пользовательское значение; сравнение — смысловое (коммит `807dfde`).
|
||||||
|
2. ✅ `null` в `Required`-атрибуты не пишется — при пустом live сохраняется текущее значение state.
|
||||||
|
3. ✅ `Delete`: ошибки API → `AddError`; warning только для отсутствующего родителя.
|
||||||
|
4. ✅ `setSnat`: валидация пустой строки вместо тихой подмены на `no-needed`.
|
||||||
|
5. ✅ Задокументировано: «снять всё» через `vip_configure` нельзя, только `destroy`.
|
||||||
|
6. ✅ Тесты: смысловое сравнение (порядок ключей, разный count/имя, пустая аллокация).
|
||||||
|
7. ⚠️ Релиз `2.0.19` залит, но **содержит сломанный plan-modifier** — для работы из реестра нужен `2.0.20`.
|
||||||
@@ -280,7 +280,7 @@ CSS скрывает обе боковые панели mkdocs:
|
|||||||
2. **TOOLS/docs-generator/internal/writers/writers.go** — ВСЯ генерация .md (~1000 строк, ключевой файл)
|
2. **TOOLS/docs-generator/internal/writers/writers.go** — ВСЯ генерация .md (~1000 строк, ключевой файл)
|
||||||
3. **TOOLS/docs-generator/main.go** — CLI, флаги, оркестрация
|
3. **TOOLS/docs-generator/main.go** — CLI, флаги, оркестрация
|
||||||
4. **TOOLS/scripts/05_generate_docs_llm.py** — LLM-обогащение, SYSTEM_PROMPT
|
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-стили
|
6. **docs/30_registry/assets/extra.css** — CSS-стили
|
||||||
7. **docs/30_registry/javascripts/fix-slash.js** — JS (trailing slash fix)
|
7. **docs/30_registry/javascripts/fix-slash.js** — JS (trailing slash fix)
|
||||||
|
|
||||||
@@ -43,6 +43,25 @@
|
|||||||
- `universal_rebuild/tools/gen/main.go`
|
- `universal_rebuild/tools/gen/main.go`
|
||||||
- Читает YAML и генерирует ресурсы + `registry.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)
|
## 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,78 @@
|
|||||||
|
# HAR fresh-create: что происходит при создании орги/эджа (dev, 2026-09-24)
|
||||||
|
|
||||||
|
> Источники: `HAR/globak.har` (ЛК: создание орги + vDC + эджа, затем два modify),
|
||||||
|
> `HAR/org_already exists.har` (отказ создания орги из-за коллизии имени).
|
||||||
|
> Стенд: `lk-api-gateway-dev.ngcloud.ru`, realm `sandbox.nubes.ru`.
|
||||||
|
> Цель разбора: понять, что реально приходит в `state.params` после `create`
|
||||||
|
> (влияет на read-back в сгенерированных ресурсах).
|
||||||
|
|
||||||
|
## 1. Поток создания в ЛК
|
||||||
|
|
||||||
|
Инстанс создаётся **в два шага**, не одним запросом:
|
||||||
|
|
||||||
|
1. `POST /instances` — тело **только** `{"serviceId":N,"displayName":"…","descr":""}`. Никаких параметров.
|
||||||
|
2. `POST /instanceOperations` — `{"instanceUid":"…","operation":"create"}` → возвращает `instanceOperationUid`.
|
||||||
|
3. `POST /instanceOperationCfsParams` — по одному запросу на параметр: `{"paramValue":"…","instanceOperationUid":"…","svcOperationCfsParamId":NNN}`.
|
||||||
|
4. `GET /instanceOperations/{opUid}/validate-cfs`.
|
||||||
|
5. `POST /instanceOperations/{opUid}/run`.
|
||||||
|
6. Поллинг `GET /instanceOperations/{opUid}` до `dtFinish`.
|
||||||
|
|
||||||
|
Это в точности тот же набор эндпоинтов, что использует наш провайдер (`core/operation_run.go`, `operation_cfs.go`).
|
||||||
|
|
||||||
|
## 2. Параметры операций (из HAR)
|
||||||
|
|
||||||
|
| Сервис | Операция | Параметры |
|
||||||
|
|---|---|---|
|
||||||
|
| Орга (19) | create | `418 resourceRealm=sandbox.nubes.ru`, `556 organizationType`, `1125 orgSuffix` |
|
||||||
|
| vDC (21) | create | `30`, `746`, `335`, `397`, `557`, `558`, `361` (+ `8` = uid орги) |
|
||||||
|
| Эдж / vc_nsxt (22) | create | `621 vdcType=vdc`, `8 vdcUid`, `622`, `340 needEnableAVI`, `341 virtualServicesCount`, `825 qosProfile`, `1110 routedNetConfiguration` |
|
||||||
|
| Орга (19) | modify (207) | `662 vIPConfigure = [{"name":"internet-ipv4-v1","count":"3"}]` |
|
||||||
|
| Эдж (22) | modify (111) | `368 needEnableAVI`, `369 virtualServicesCount=4`, `856 qosProfile`, **`372 ipSpaceName=internet-ipv4-v1`**, `1112 routedNetConfiguration` |
|
||||||
|
|
||||||
|
`372 ipSpaceName` **не участвует в create** — только в modify. Ровно как в нашем `Update`
|
||||||
|
(`22_vc_nsxt_resource.go`), который шлёт 368/369/372/856/1112.
|
||||||
|
|
||||||
|
## 3. `state.params` до и после modify
|
||||||
|
|
||||||
|
Ответ `GET /instances/{uid}`: параметры лежат в **`instance.state.params`**
|
||||||
|
(`instance.params` = `null`). Наш `GetInstanceStateParams` (`core/instance_params.go:35-45`)
|
||||||
|
читает именно этот путь — то есть read-back их видит.
|
||||||
|
|
||||||
|
| Инстанс | Сразу после create | После modify |
|
||||||
|
|---|---|---|
|
||||||
|
| Орга `df5ec5f2…` («kontra») | `{"admins":[], "vIPConfigure":[{}], "resourceRealm":"sandbox.nubes.ru", "organizationType":"saas"}` | `vIPConfigure=[{"name":"internet-ipv4-v1","count":"3"}]`, state version 3 → 4 |
|
||||||
|
| Эдж `ad0ab577…` («tedj») | `vdcUid`, `vdcType`, `qosProfile="QoS-100Mbit"`, `vdcGroupUid=""`, `needEnableAVI=true`, `virtualServicesCount="1"`, `routedNetConfiguration` — **ключа `ipSpaceName` НЕТ** | `ipSpaceName="internet-ipv4-v1"`, `virtualServicesCount="4"`, version 1 → 2 |
|
||||||
|
|
||||||
|
Ключевое: у орги `vIPConfigure` **присутствует и равен `[{}]`** (пустой элемент);
|
||||||
|
у эджа `ipSpaceName` **отсутствует** до первого modify.
|
||||||
|
|
||||||
|
## 4. Провал операции приходит внутри тела, а не HTTP-кодом
|
||||||
|
|
||||||
|
`HAR/org_already exists.har`: создание орги с `organizationType=iaas` и `orgSuffix=suff`:
|
||||||
|
|
||||||
|
- `POST /instances` → 201, `POST /instanceOperations` → 201, `validate-cfs` → 204, `run` → 201;
|
||||||
|
- финальный `GET /instanceOperations/{opUid}`: `submitResult="201"`, `isSuccessful=false`,
|
||||||
|
`errorLog="Организация с именем 'WZ03709-iaas' уже существует в рамках ресурсной платформы sandbox.nubes.ru"`.
|
||||||
|
|
||||||
|
Вывод: **ошибку операции нужно читать из `errorLog`/`isSuccessful`** поллинга; HTTP-код ничего не скажет.
|
||||||
|
|
||||||
|
Дополнительно: имя орги формируется как `<suffix>-<тип>` (`WZ03709-iaas` / `WZ03709-saas`),
|
||||||
|
то есть в одном realm — по одной орге каждого типа; повтор даёт ту же ошибку.
|
||||||
|
|
||||||
|
## 5. Выводы для нашего провайдера
|
||||||
|
|
||||||
|
1. `nubes_vc_org.v_ip_configure` — **Required** в схеме (generator мержит create+modify, `loader.go:96`),
|
||||||
|
но при `Create` не отправляется, а read-back после create вернёт `[{}]` вместо планового значения
|
||||||
|
→ риск `Provider produced inconsistent result after apply` на создании орги. **Прогоном не проверено.**
|
||||||
|
2. `nubes_vc_nsxt.ip_space_name` — Optional+Computed: при create ключа в state нет, значение сохраняется
|
||||||
|
в state, но **SNAT не включается**; включается только следующим `apply` (Update → 372). **Прогоном не проверено.**
|
||||||
|
3. `RefreshResourceState` (`resources_core/state_refresh.go`) перезаписывает поля из `state.params`;
|
||||||
|
для modify-only параметров это поведение опасное — в create его включать не следует (универсальная правка генератора).
|
||||||
|
4. Из п.1–2 следует, что одной правкой «добавить два ресурса-модификатора» инцидент может не закрыться:
|
||||||
|
схема `nubes_vc_org` останется с Required-полем.
|
||||||
|
|
||||||
|
## 6. Ограничения разбора
|
||||||
|
|
||||||
|
- `apply`/`plan` не запускались: пункты 1–2 — вывод из кода + HAR, не подтверждены живым прогоном.
|
||||||
|
- Проверено на одном стенде (dev), одной орге (`NarodOrg` — во втором HAR имя `WZ03709-iaas` уже занято).
|
||||||
|
- `qosProfile` в create ЛК отправляет пустым, после modify в state = `QoS-100Mbit`.
|
||||||
@@ -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,128 @@
|
|||||||
|
# Q&A с Opus: дизайн ресурсов-модификаторов (2026-09-24)
|
||||||
|
|
||||||
|
> Кто: вопросы составлены нами (Copilot), ответы — Opus (внешний агент, по разрешению пользователя).
|
||||||
|
> Контекст: решено делать два ресурса-модификатора (`nubes_vc_org_ip_allocation`, `nubes_vc_nsxt_snat`).
|
||||||
|
> Статус: ответы приняты к сведению, **код не писался**, часть утверждений Opus мною не проверена (пометки ниже).
|
||||||
|
|
||||||
|
## Вопросы и ответы
|
||||||
|
|
||||||
|
### 1. Инварианты Read/Delete ресурса-модификатора
|
||||||
|
**Ответ Opus:**
|
||||||
|
- Read: `RemoveResource` только если родитель исчез (404/deleted) — у нас есть `ShouldRemoveFromState`
|
||||||
|
(Opus ссылается на `modifier.go`). Расхождение значения параметра ≠ повод удалять ресурс: это дрейф,
|
||||||
|
обновить поле в state.
|
||||||
|
- Delete = inverse modify (`count=0` / `needEnableAVI=false` / `ipSpaceName="no-needed"`) — «шаблон
|
||||||
|
`DeleteStrategy=inverse` + `override` уже реализован».
|
||||||
|
- Если родитель уже удалён: inverse пропустить, ресурс убрать из state (no-op + Warning), не падать на ошибке API.
|
||||||
|
|
||||||
|
### 2. Reset-to-default в `*WithDefaults`
|
||||||
|
**Ответ Opus:** защита «уже встроена»: и `RunInstanceOperationUniversalWithDefaults` (`operation_run.go:138`),
|
||||||
|
и by-code путь (`operation_run_bycode.go:108`) досылают незаданные параметры с приоритетом
|
||||||
|
**live `state.params` → `paramValue` формы → `defaultValue`**. Достаточно шлать только `ipSpaceName`.
|
||||||
|
Отдельный pre-read live + merge делать не нужно; `ByCode`/`ByIdempotent` — не нужны.
|
||||||
|
Дополнительно `RunOperationByCodeIdempotent` (`check_before_run`) сверяет desired == current и пропускает лишний run.
|
||||||
|
|
||||||
|
### 3. Генератор: modify-only параметр с `required: true`
|
||||||
|
**Ответ Opus (минимальный набор):**
|
||||||
|
- **(а)** modify-only → всегда Optional (снять Required в схеме). Обязательно.
|
||||||
|
- **(б)** исключить modify-only из create-read-back (не добавлять его `InputField` в Create/Read). Обязательно.
|
||||||
|
- **(в)** «после create догонять modify» — **не нужно**: это ответственность отдельного modifier-ресурса.
|
||||||
|
- Breaking: снятие Required — не breaking (Optional шире). Breaking — если **удалить** атрибут из схемы
|
||||||
|
instance у тех, кто его уже прописал в `.tf`. Формулировка Opus: «modify-only параметров в схеме instance
|
||||||
|
быть не должно вовсе — их место в modifier-ресурсе».
|
||||||
|
|
||||||
|
### 4. Диагноз «inconsistent result after apply» на создании орги
|
||||||
|
**Ответ Opus: подтверждает.** `state_refresh.go`, цикл `inputs`: берёт `paramsMap["vIPConfigure"]` из
|
||||||
|
`state.params` (платформа отдаёт `[{}]`), через `setFieldValue`/`ParseString` перекрывает план; для
|
||||||
|
Required-атрибута TF требует final == config → ошибка. Корректно: не читать modify-only обратно в Create
|
||||||
|
(п.3б) и вернуть запланированное значение, либо Optional+Computed со схлопыванием `[{}]`→null.
|
||||||
|
|
||||||
|
### 5. Порядок destroy
|
||||||
|
**Ответ Opus:** явный `depends_on` нужен — связь между org-IP и SNAT идёт по **имени** ipSpace, ребра графа
|
||||||
|
TF не видит. Цепочка: `vdc → org → org-IP → edge → SNAT → кластер`; при корректных `depends_on` destroy
|
||||||
|
пойдёт в обратном порядке. Обязательные рёбра: SNAT → org-IP, modifier → родитель. Достаточно при условии,
|
||||||
|
что inverse-Delete терпит уже удалённого родителя (п.1).
|
||||||
|
|
||||||
|
### 6. Трактовка `[{}]` в Read
|
||||||
|
**Ответ Opus:** `[{}]` = «не выделено», нормализовать в null/пусто. `count=0` и `[{}]` — одно состояние
|
||||||
|
«пусто», иначе ложный дрейф на каждом plan.
|
||||||
|
|
||||||
|
## Мои замечания к ответам (не проверено кодом, требует внимания)
|
||||||
|
|
||||||
|
1. **Opus опирается на machinery отменённого захода.** Он говорит про `modifier.go`, `DeleteStrategy=inverse`,
|
||||||
|
`override`, «уже реализовано». Это шаблон генератора из эпохи `kind: modifier`, которую мы **сознательно
|
||||||
|
отменили** (см. баннер LEGACY в `NOTES/20_prompts/**`, `docs/60_strategy/modifier_resources_ideology_and_specification.md`).
|
||||||
|
Ответы про «уже встроено» нельзя принимать как готовое решение — это код отменённой ветки.
|
||||||
|
2. **Противоречие внутри п.3:** сначала «modify-only → всегда Optional (оставить в схеме instance)»,
|
||||||
|
потом «modify-only в схеме instance быть не должно вовсе». Это разные изменения: Optional+Computed vs удаление.
|
||||||
|
Нужно выбрать одно, иначе получим двух владельцев одного параметра (instance-ресурс и модификатор).
|
||||||
|
3. **Два владельца параметра.** Если `ip_space_name` остаётся в `nubes_vc_nsxt` **и** появляется
|
||||||
|
`nubes_vc_nsxt_snat`, Terraform не увидит конфликт: оба будут шлать 372. Значит, из `Update`
|
||||||
|
сгенерированного `nubes_vc_nsxt` параметр надо убирать — иначе fight/drift. В ответах Opus этого нет.
|
||||||
|
4. **П.2 не проверял сам.** Утверждение «приоритет live → paramValue → defaultValue уже встроен» противоречит
|
||||||
|
комментарию в `19_vc_org_resource.go` про reset-баг (`state_params["needEnableAVI"]="false"`, когда на
|
||||||
|
платформе `true`). Нужна проверка `operation_run.go:138` и `operation_run_bycode.go:108` по коду.
|
||||||
|
5. **Политика destroy для не-нашей орги.** Орга не в state (адресация по uid). При `destroy` конфигурации
|
||||||
|
родитель не удаляется — но org-IP-модификатор по §1 выполнит inverse (`count=0`). Нужно решение:
|
||||||
|
снимать квоту или оставлять (`keep_on_destroy`)— у Opus этого нет.
|
||||||
|
6. **Один элемент vs весь массив.** `vIPConfigure` — массив. Если ресурс управляет одним элементом
|
||||||
|
(по `ip_space_name`), то два ресурса на разные ipSpace возможны; если всем массивом — нет. `count=0`
|
||||||
|
как inverse предполагает поэлементную модель, но в ответах это не зафиксировано.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Раунд 2 (те же сутки): ответы Opus на 5 уточняющих вопросов
|
||||||
|
|
||||||
|
### Про противоречие в п.3 (раунд 1)
|
||||||
|
Opus признал: это были две несовместимые опции.
|
||||||
|
- **Канон (цель):** modify-only параметра в схеме instance быть не должно — владелец отдельный modifier-ресурс.
|
||||||
|
- **«Всегда Optional»** — только переходный вариант, если параметр временно оставлен в instance.
|
||||||
|
- Одновременно оба тезиса не действуют.
|
||||||
|
|
||||||
|
### 1. Два владельца 372/662
|
||||||
|
Один владелец. Instance **перестаёт слать** 372/662: убрать из `ModifyParams` генератора
|
||||||
|
(не эмитить в `params` map в `instance.go:478`, Update). Незаданные параметры при этом не сбросятся —
|
||||||
|
досылаются из live `state.params` (см. п.2 раунда 1). Поле в instance остаётся максимум как read-back
|
||||||
|
(Computed) либо убирается вовсе.
|
||||||
|
|
||||||
|
### 2. Массив vs элемент
|
||||||
|
`vIPConfigure` — `array-map-fixed` с **replace-семантикой всего массива**: отправка `[{name,count}]`
|
||||||
|
перезаписывает массив целиком.
|
||||||
|
- **Один ресурс = весь массив** — просто и безопасно.
|
||||||
|
- Два ресурса на разные ipSpace — только с read→merge→send-full-array; без merge last-write-wins.
|
||||||
|
- **Рекомендация MVP Opus: один ресурс = весь массив.** Мультиресурс по имени — отдельная фича.
|
||||||
|
|
||||||
|
### 3. Destroy, когда родитель не наш
|
||||||
|
Флаг `keep_on_destroy` (bool, Optional):
|
||||||
|
- родитель жив и `keep_on_destroy=false` (дефолт) → inverse (`count=0`);
|
||||||
|
- родитель 404 / вне нашего контроля → пропустить + Warning (не падать);
|
||||||
|
- `keep_on_destroy=true` → всегда no-op + Warning.
|
||||||
|
|
||||||
|
### 4. Вывод атрибута из instance-схемы (не breaking)
|
||||||
|
Два шага:
|
||||||
|
- **сейчас**: `Deprecated: "..."` + **Optional+Computed** + прекратить отправку в modify (read-back остаётся);
|
||||||
|
- **следующий major**: удалить атрибут.
|
||||||
|
|
||||||
|
### 5. Тип атрибута в новом ресурсе
|
||||||
|
**String + JSON + `JsonNormalize()`** (как сейчас `v_ip_configure`), потому что:
|
||||||
|
- wire-формат `array-map-fixed` — JSON-строка;
|
||||||
|
- `json_planmodifier.go` — `planmodifier.String` (на list/nested не встанет);
|
||||||
|
- `RefreshResourceState` читает input-поля только как scalar string/bool/int.
|
||||||
|
Nested list даёт лучший UX, но требует нового кода в `state_refresh.go`. Для MVP — String+JsonNormalize.
|
||||||
|
|
||||||
|
## Мои замечания к раунду 2
|
||||||
|
|
||||||
|
1. **П.2 меняет интерфейс заявленного ресурса.** Мы планировали `nubes_vc_org_ip_allocation`
|
||||||
|
с `ip_space_name` + `count` (по элементу). Opus рекомендует «один ресурс = весь массив»
|
||||||
|
(list `{name,count}`). Это разные ресурсы по UX и по семантике Delete — требует решения пользователя.
|
||||||
|
2. **Проверяемость.** Утверждение про `instance.go:478` и про приоритет live при дозаполнении я не
|
||||||
|
проверял по коду — числовой якорь может быть неточным (в прошлом ответе он ссылался на
|
||||||
|
`modifier.go` отменённой ветки).
|
||||||
|
3. **`Deprecated` + `Optional+Computed`** — единственный вариант, который проходит без breaking, согласен;
|
||||||
|
но это правка сгенерированной схемы → правится в генераторе, не в `resources_gen/*.go`.
|
||||||
|
|
||||||
|
## Что дальше
|
||||||
|
|
||||||
|
- Решение пользователя по п.2 замечаний (элемент vs весь массив).
|
||||||
|
- Проверка по коду приоритета live-дозаполнения и строки `instance.go:478` (чтение, без правок).
|
||||||
|
- После решения — план реализации 2 ресурсов.
|
||||||
@@ -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,307 @@
|
|||||||
|
# Штурвал dev-00: диагностика, destroy-засада с квотой IP и дизайн «freeze on destroy» (2026-09-24)
|
||||||
|
|
||||||
|
> Источники: `kubectl` из локали (контекст `tazet@narod.ru@shturval-dev-00`), API ЛК dev
|
||||||
|
> (`https://lk-api-gateway-dev.ngcloud.ru/api/v1/svc`, токен `secrets/narodDEV.token`), код провайдера
|
||||||
|
> (`provider/`), генератор (`TOOLS/resource-generator/`), конфиг стенда `DEV_STAND/FullPipe/`.
|
||||||
|
> Все выводы — только из этих источников; где не проверено, отмечено «не проверено».
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Стенд и кластер
|
||||||
|
|
||||||
|
- Услуга **150 «Kubernetes кластер Штурвал»**, инстанс `shturval-dev`, uid `94627ff4-33a5-48f2-aca1-695741e0b6a2`.
|
||||||
|
Создан 24.09.2026 17:18:01, операция `create` завершена 17:36:58 (`isSuccessful=true`, `errorLog=null`).
|
||||||
|
- `state.out`: `webUrl=https://k8s.ngcloud.ru/clusters/shturval-dev-00/dashboard`,
|
||||||
|
`kubernetesApiAddress=185.247.187.146`, `ingressAddress=185.247.187.148`.
|
||||||
|
- Параметры: `clusterName=shturval-dev-00`, `vdcUid=d0937335-…` (`fullpipe-vdc`),
|
||||||
|
`nsxtUid=2C37FED1-E8F8-4A84-8434-7851C7C8B5D6` (эдж `fullpipe-edge`), `appVersion=2.14.0`,
|
||||||
|
`exLogging/exMonitoring/exLocalCsi/exVip/exUpdate/exIngress/exNamedCsi = true`, CP 1× `TKG 4CPU 8RAM` / 50 ГБ,
|
||||||
|
workers 1× `TKG 4CPU 8RAM` / 50 ГБ (`workers-shturval-dev`, labelDeck=true).
|
||||||
|
- kubeconfig: сервер `https://185.247.187.146:6443`; client v1.34.1, server v1.35.1; узлы 2× Ready
|
||||||
|
(control-plane + worker), Ubuntu 24.04.5, containerd 2.2.1.
|
||||||
|
- Организация `organ` (uid `57eeacd1-dc7f-4a52-b903-7e5f7d3c1164`, CD-имя `WZ01325-saas`, realm `sandbox.nubes.ru`).
|
||||||
|
|
||||||
|
### Состояние кластера (снимок 17:52 MSK)
|
||||||
|
|
||||||
|
- Подов 49 (готовых 44). Не-Running остались только подвисшие поды установщика:
|
||||||
|
`shturval-init-job-489mk`, `-98n4k`, `-vkwk7` (Error), `-l8457` (Unknown); рядом `-9587k` (Completed).
|
||||||
|
- Job `kube-system/shturval-init-job`: label `shturval.tech/init`, **без ownerReferences**,
|
||||||
|
`backoffLimit: 10`, `ttlSecondsAfterFinished: 86400`, nodeSelector `control-plane`,
|
||||||
|
образ `r.shturval.tech/shturval-install:2.14.0`, `/scripts/run.sh`. Итог: `failed: 4`, `succeeded: 1`,
|
||||||
|
завершён 14:31:46 UTC (17:31 MSK) → поды удалятся сами ~25.09 14:31 UTC.
|
||||||
|
- Причина падений (лог пода): `UPGRADE FAILED: failed to create resource: conversion webhook for
|
||||||
|
ops.shturval.tech/v1beta1, Kind=ShturvalRepoConfig failed: Post
|
||||||
|
"https://shturval-services-webhook-service.shturval-services-system.svc:443/convert?timeout=30s":
|
||||||
|
dial tcp 10.97.147.91:443: connect: operation not permitted`. То же в логе оператора:
|
||||||
|
`failed calling webhook "vshturvalupdate.kb.io" … connect: operation not permitted` — до готовности Cilium
|
||||||
|
webhook-и недоступны. Следующая попытка прошла (релиз `shturval-services.v3`), всё поднялось.
|
||||||
|
- Остальное зелёное: `shturvalserviceconfigs` 41/41 `ready=true` (24 в режиме `auto`, 17 в `absent`),
|
||||||
|
`nodeconfigitems` 4/4 `ready=true`, `nodeconfigs` 2/2, у всех 35 сервисов есть endpoints,
|
||||||
|
IngressClass `nginx` 1, ingress-controller 1/1.
|
||||||
|
|
||||||
|
### UI-счётчики (расшифровка)
|
||||||
|
|
||||||
|
| Колонка | Что это на самом деле |
|
||||||
|
|---|---|
|
||||||
|
| `Pods 44/48` | готовые/всего поды (48 = все поды минус `Completed`) |
|
||||||
|
| «Системные сервисы» | число `ShturvalServiceConfig` в режиме `auto`: 17/24 во время установки → 24/24 |
|
||||||
|
| «Конфигурация узлов» 4/4 | `nodeconfigitems.node.shturval.tech` `ready=true` |
|
||||||
|
| «Ingress» | домен `*.shturval-dev-00.ip-185-247-187-148.shturval.link`, **не** счётчик |
|
||||||
|
| ⚠️ на «Pods» | ровно 4 подвисших пода `shturval-init-job` |
|
||||||
|
|
||||||
|
Статус «Работает с ошибками» в первом снимке (31/53 подов) был снят во время установки; после догрузки — «Работает».
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Что DevOps может сделать с мусором init-job
|
||||||
|
|
||||||
|
Ответ на вопрос «что выставить в настройках деплоя Штурвала»:
|
||||||
|
|
||||||
|
- В услуге 150 и в конфиге стенда таких ручек **нет** (есть только `vdc_uid`/`nsxt_uid`, `cluster_name`,
|
||||||
|
галочки `ex_*`, sizing/count, внешние адреса). `backoffLimit` и `ttlSecondsAfterFinished` зашиты
|
||||||
|
в манифест установщика платформы.
|
||||||
|
- Поэтому вариантов два: подождать самоочистку по `ttlSecondsAfterFinished: 86400`, либо тикет в команду
|
||||||
|
Штурвала: уменьшить TTL и/или не запускать установку компонентов до готовности Cilium
|
||||||
|
(иначе снова `operation not permitted` на webhook-ах).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Destroy стенда и засада с квотой IP
|
||||||
|
|
||||||
|
Порядок destroy: `nubes_vc_nsxt_snat.snat` → `nubes_vc_org_ip_allocation.org_ip` → `nubes_vc_nsxt.edge` → `nubes_vc_vdc.vdc`.
|
||||||
|
|
||||||
|
- `snat` удалился успешно (2m27s), отправив modify `ipSpaceName = "no-needed"` (warning «SNAT выключен»).
|
||||||
|
- `org_ip_allocation` упал:
|
||||||
|
`Error: Ошибка клиента — операция D9FB606D-C86E-4D30-A58D-44282C4508AE завершилась с ошибкой:
|
||||||
|
Кол-во зантяых Ip в тенанте 'WZ01325-saas': 2. Невозможно выставить параметр count ниже этого параметра`.
|
||||||
|
- Причина в коде: `Delete` аллокации при `keep_on_destroy = false` отправляет обратный modify
|
||||||
|
`vIPConfigure = [{"name":"internet-ipv4-v1","count":"0"}]`
|
||||||
|
(`provider/internal/resources_core/org_ip_allocation_resource.go:258-274`); при `keep_on_destroy = true`
|
||||||
|
ничего не отправляется (строки 212-215). В стенде сейчас `keep_on_destroy = false`
|
||||||
|
(`DEV_STAND/FullPipe/modifiers.tf:39` для квоты, `:29` для SNAT).
|
||||||
|
- Кто держит 2 адреса: инстанс кластера Штурвала — `.146` (API) и `.148` (ingress). `suspend` адреса
|
||||||
|
**не** освобождает (suspend выполнен 17:59:59 MSK успешно, `isDeleted=false`, `uptime=0`, адреса в `state.out` остались).
|
||||||
|
- Последствие упавшего destroy: прерван, до edge/vDC дело не дошло; в state остались `vdc`, `edge`,
|
||||||
|
`org_ip_allocation`, а `snat` уже удалён — «рваное» состояние.
|
||||||
|
|
||||||
|
### Метаданные платформы (проверено через API ЛК)
|
||||||
|
|
||||||
|
- Обязательные заголовки: `Authorization: Bearer <secrets/narodDEV.token>`,
|
||||||
|
браузерный `User-Agent`, `Referer: https://deck-dev.ngcloud.ru/` — без них DDoS-Guard отдаёт `403 Forbidden`.
|
||||||
|
- Эндпоинты: `GET /instances?page=1&size=200`, `GET /instances/{uid}`; параметры — в
|
||||||
|
`instance.state.params` (верхнеуровневый `instance.params = null`), статус — `explainedStatus`.
|
||||||
|
- `availableOperations`: кластер — `delete, modify, suspend, resume, reconcile, create_user, delete_user`;
|
||||||
|
vDC — `delete, modify, suspend, resume, reconcile`; **эдж — `delete, modify, reconcile` (suspend отсутствует)**.
|
||||||
|
- `dependencies`/`dependentInstances`: у кластера и эджа пусто; у `fullpipe-vdc` в зависимых три инстанса
|
||||||
|
`fullpipe-edge` (`b289beb8…`, `86a01033…`, `2c37fed1…`) — рабочий только `2c37fed1…`, два других сироты
|
||||||
|
от прошлых прогонов. Кластера в зависимых нет → платформа не блокирует удаление эджа/квоты при живом кластере.
|
||||||
|
- vDC (услуга 21) по инструкции удаляется только через 14 дней после `suspend`; при живых Edge/vApp/VM/кластере
|
||||||
|
удаление — через поддержку.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. `adopt_existing_on_create` — как усыновление реально работает
|
||||||
|
|
||||||
|
- Кластера не было в state, а ресурс есть в `shturval.tf` → план показывал `will be created`. Это **не**
|
||||||
|
доказательство отсутствия adopt: проверка/adopt выполняются в `Create` на apply
|
||||||
|
(`provider/internal/resources_gen/150_k8s_sthutrval_cluster_resource.go:211`).
|
||||||
|
- Дефолт adopt — `false` (там же, строка 136). При существующем инстансе:
|
||||||
|
`running` либо `suspended` без adopt → hard error «РЕСУРС С ТАКИМ ИМЕНЕМ УЖЕ СУЩЕСТВУЕТ (…RUNNING/SUSPEND)»
|
||||||
|
(`provider/internal/resources_core/resource_diagnostics_required.go:241-259`). Дубль при этом не создаётся.
|
||||||
|
- Исправление: добавлен `adopt_existing_on_create = true` в `DEV_STAND/FullPipe/shturval.tf`
|
||||||
|
(коммит `57abb7b`; бэкап `TMP/backup_2026-09-24/shturval.tf.before-adopt`).
|
||||||
|
- Результат apply (проверено): state получил `id=94627ff4-…`; провайдер сам выполнил `resume`
|
||||||
|
(18:26:30, success) — инстанс `running`, `isSuspended=false`; кластер жив (2 ноды Ready);
|
||||||
|
`nubes_vc_nsxt_snat.snat` в state (`internet-ipv4-v1`), live эдж `ipSpaceName=internet-ipv4-v1`
|
||||||
|
(modify 18:23:12, success). План после apply: действий по ресурсам нет, только дрейф state
|
||||||
|
(`edge.state_params.ipSpaceName`: `no-needed` → `internet-ipv4-v1`) и `Changes to Outputs`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Дизайн «freeze on destroy» (решение)
|
||||||
|
|
||||||
|
**Требование заказчика:** пользователь стенда не должен ничего делать руками и не должен звать DevOps.
|
||||||
|
`destroy` не удаляет, а «замораживает»: кластер → `suspend`, vDC → `suspend`, эдж → оставить как есть,
|
||||||
|
SNAT → не выключать, квота IP → не трогать. Следующий `apply` возвращает всё в работу.
|
||||||
|
|
||||||
|
**Что уже есть в ядре:**
|
||||||
|
|
||||||
|
- `resources_core/crud.go:64-96` — `DeleteResourceWithTimeout(..., destroyBehavior, ...)`, режимы
|
||||||
|
`state_only`/`detach` (ничего не делаем, ресурс забывается) и `suspend` (шлём операцию `suspend`).
|
||||||
|
- `crud.go:162-214` — adopt на create: для `StateSuspended` при `resumeIfExists` шлёт `resume` и ждёт готовности.
|
||||||
|
- `nubes_k8s_sthutrval_cluster` и `nubes_vc_vdc` — `suspend_on_destroy` (default `true`) + `adopt_existing_on_create`.
|
||||||
|
- `nubes_vc_nsxt_snat` и `nubes_vc_org_ip_allocation` — `keep_on_destroy` (в стенде `false`).
|
||||||
|
- `nubes_vc_nsxt` (эдж) — только `adopt_existing_on_create`; в `provider/resources_yaml/22_vc_nsxt.yaml`
|
||||||
|
нет операции `suspend` → генератор ставит `deleteMode := "delete"`
|
||||||
|
(`TOOLS/resource-generator/internal/templates/instance.go:546-552`), т.е. эдж удаляется по-настоящему.
|
||||||
|
|
||||||
|
**Решение:** три режима в одной общей логике — `delete` (дефолт), `suspend` (где сервис умеет),
|
||||||
|
`keep` → `state_only` (эдж, SNAT, IP-квота). Дефолты провайдера остаются разрушающими; freeze включается
|
||||||
|
явно в `.tf` стенда. Обязательны предупреждения в выводе destroy («заморожено (suspend)», «оставлен как есть:
|
||||||
|
эдж», «квота IP не изменена») — иначе freeze выглядит как успешное удаление.
|
||||||
|
|
||||||
|
**Реализация — только через генератор (ручные правки `resources_gen/` затрёт регенерация):**
|
||||||
|
|
||||||
|
1. `TOOLS/resource-generator/internal/types/types.go` — в `LifecycleSpec` добавить
|
||||||
|
`KeepOnDestroyDefault *bool \`yaml:"keep_on_destroy_default"\``, в `GenResource` — `KeepOnDestroy bool`.
|
||||||
|
2. `TOOLS/resource-generator/internal/loader/loader.go` — читать новый ключ (дефолт `false`),
|
||||||
|
как сейчас читается `suspend_on_destroy_default` (строки ~114-118).
|
||||||
|
3. `TOOLS/resource-generator/internal/templates/instance.go` — эмитить атрибут `keep_on_destroy`
|
||||||
|
(Optional+Computed, дефолт из YAML) в schema и модель; в `Delete` собирать режим:
|
||||||
|
`suspend` → `state_only` (keep) → `delete`.
|
||||||
|
4. `provider/resources_yaml/22_vc_nsxt.yaml` — `keep_on_destroy_default: false`.
|
||||||
|
5. Регенерация + проверка воспроизводимости (`TOOLS/scripts/10_yaml_stability_run.sh`) → сборка/релиз.
|
||||||
|
|
||||||
|
`resources_core`-ресурсы (SNAT, квота IP) менять не нужно — флаг там уже есть.
|
||||||
|
|
||||||
|
**Конфиг стенда для freeze:**
|
||||||
|
|
||||||
|
| Файл / ресурс | Сейчас | Надо |
|
||||||
|
|---|---|---|
|
||||||
|
| `modifiers.tf` → `nubes_vc_org_ip_allocation.org_ip` | `keep_on_destroy = false` (:39) | `true` |
|
||||||
|
| `modifiers.tf` → `nubes_vc_nsxt_snat.snat` | `keep_on_destroy = false` (:29) | `true` |
|
||||||
|
| `edge.tf` → `nubes_vc_nsxt.edge` | атрибутов нет | `keep_on_destroy = true` + `adopt_existing_on_create = true` |
|
||||||
|
| `shturval.tf` → `nubes_k8s_sthutrval_cluster.shturval` | `adopt=true`, `suspend_on_destroy` дефолт | `adopt=true` (есть) + `suspend_on_destroy = true` явно |
|
||||||
|
| `vdc.tf` → `nubes_vc_vdc.vdc` | `suspend_on_destroy = true`, `adopt = true` (:17,19) | без изменений |
|
||||||
|
|
||||||
|
**Что будет при destroy в режиме freeze:** из state ресурсы уйдут, но в облаке ничего не изменится —
|
||||||
|
SNAT останется включённым (Delete при `keep=true` печатает «SNAT не выключался», `nsxt_snat_resource.go:185-190`),
|
||||||
|
квота IP — `count=3`, эдж — running, vDC и кластер — suspended. При следующем `apply` ресурсы создадутся заново
|
||||||
|
и усыновят живые объекты (`suspend` → `resume`, `running` → просто UID), SNAT/квота отправят те же значения → no-op.
|
||||||
|
|
||||||
|
**Полный teardown** — только явный opt-out (`keep_on_destroy=false` / `suspend_on_destroy=false`) и в порядке:
|
||||||
|
кластер → `count=0` → SNAT → эдж → vDC. Иначе `count` ниже занятых не опустить, а удаление эджа оставит кластер
|
||||||
|
без внешнего API/ingress.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5.1. Реализовано (вечер 24.09)
|
||||||
|
|
||||||
|
**Генератор (коммит `22c6c83`):**
|
||||||
|
|
||||||
|
- `TOOLS/resource-generator/internal/types/types.go` — в `ServiceSpec.Lifecycle` добавлен `KeepOnDestroyDefault *bool`
|
||||||
|
(`yaml:"keep_on_destroy_default"`), в `GenResource` — `KeepOnDestroy bool`.
|
||||||
|
- `TOOLS/resource-generator/internal/loader/loader.go` — читает ключ из YAML (дефолт `false`) и передаёт в генератор.
|
||||||
|
- `TOOLS/resource-generator/internal/templates/instance.go` — атрибут `keep_on_destroy` (Optional+Computed, дефолт из YAML)
|
||||||
|
во всех instance-ресурсах; в `Delete` режим выбирается так: `keep_on_destroy` → `state_only` (приоритет),
|
||||||
|
иначе `suspend_on_destroy` (где сервис умеет) → `suspend`, иначе `delete`; после успешного удаления печатаются
|
||||||
|
предупреждения «Ресурс заморожен, а не удалён» / «Ресурс оставлен как есть, а не удалён».
|
||||||
|
- Флаг получили **все 40 instance-ресурсов** (проверено: `grep -l keep_on_destroy generated/dev/go/*_resource.go`).
|
||||||
|
Subresource-ресурсы (пользователи/БД/бэкапы) — без него: другой шаблон, у них нет своего suspend.
|
||||||
|
|
||||||
|
**YAML-спеки не правим:** `01_generate_yamls.sh` перезаписывает `generated/<stand>/resources_yaml/*.yaml` из API,
|
||||||
|
поэтому ручной ключ там не живёт. Ключ `keep_on_destroy_default` поддержан, но не используется:
|
||||||
|
дефолт `false` берётся из нулевого значения Go.
|
||||||
|
|
||||||
|
**Проверка:**
|
||||||
|
|
||||||
|
- `02_generate_resources_and_docs_v2.sh --profile TOOLS/config/dev` → `dev-materialize.sh dev` →
|
||||||
|
`go build ./...` в `provider/` — OK, `go test ./...` — OK.
|
||||||
|
- Локальная проверка конфига без релиза: собран свой бинарь в `TMP/devbin/`, `dev_overrides` —
|
||||||
|
`TMP/terraformrc.dev`; `TF_CLI_CONFIG_FILE=TMP/terraformrc.dev terraform validate` — Success,
|
||||||
|
`terraform plan` — `0 to add, 5 to change, 0 to destroy`, у ресурсов меняется только новый
|
||||||
|
атрибут (`keep_on_destroy = false -> true` у квоты, `+ keep_on_destroy = false` у vDC/кластера/эджа/SNAT) плюс
|
||||||
|
пересчёт outputs.
|
||||||
|
|
||||||
|
**Конфиг стенда (коммит `40aef87`):** `modifiers.tf` — `keep_on_destroy = true` у квоты IP (`:31`) и SNAT (`:42`);
|
||||||
|
`edge.tf` — `keep_on_destroy = true` (`:23`) + `adopt_existing_on_create = true` (`:27`);
|
||||||
|
`shturval.tf` — явные `adopt_existing_on_create = true` (`:113`) и `suspend_on_destroy = true` (`:117`);
|
||||||
|
у vDC в `vdc.tf:17,19` оба флага уже были.
|
||||||
|
|
||||||
|
**Релиз выполнен:** `03_build_and_upload_provider.sh --profile TOOLS/config/dev 2.0.22` → три платформы (linux/darwin/windows amd64) + `SHA256SUMS`/`.sig` залиты, версия видна в реестре (проверено `GET /v1/providers/nubes-dev/nubes/versions` → `2.0.22`); `VERSIONS.md` обновлён (коммит `c29df21`).
|
||||||
|
|
||||||
|
**Что осталось:** проверить цикл на живом стенде: `destroy` = заморозка (кластер/vDC → suspend, эдж/SNAT/квота IP → state_only с предупреждениями) и `apply` = разморозка (adopt + resume). `apply`/`destroy` запускает только пользователь.
|
||||||
|
|
||||||
|
**Состояние на 19:4x:** пин в `DEV_STAND/FullPipe/versions.tf` поднят до `2.0.22`, `terraform plan` → «No changes» (дрейф по `edge.state_params.ipSpaceName` ушёл после apply SNAT). Флаги в state: кластер — `adopt=true`, `suspend_on_destroy=true`, `keep=false`; vDC — то же; эдж — `keep=true`, `adopt=true`; SNAT — `keep=true`; квота IP — `keep=true`. Кластер жив: 2 ноды Ready, подов 49 (готовых 44 — те же 4 мусорных пода init-job).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5.2. Проверено вживую: `destroy` = заморозка (24.09, вечер)
|
||||||
|
|
||||||
|
`terraform destroy` на `DEV_STAND/FullPipe` (провайдер `2.0.22`) — «Apply complete! Resources: 0 added, 0 changed, 5 destroyed», ошибок нет. Предупреждения вывода:
|
||||||
|
|
||||||
|
- `SNAT не выключался` — `keep_on_destroy = true`: `ipSpaceName` шлюза оставлен без изменений (NSXT-логика, `nsxt_snat_resource.go`);
|
||||||
|
- `Ресурс оставлен как есть, а не удалён` — эдж (`vc_nsxt`, service_id=22) не менялся в облаке;
|
||||||
|
- `Аллокация IP не снималась` — квота внешних IP организации оставлена без изменений;
|
||||||
|
- `Ресурс заморожен, а не удалён` (×2) — vDC (`vc_vdc`, 21) и кластер (`k8s_sthutrval_cluster`, 150) переведены в `suspend`.
|
||||||
|
|
||||||
|
Состояние после destroy (проверено kubectl + API ЛК):
|
||||||
|
|
||||||
|
| Объект | Статус |
|
||||||
|
|---|---|
|
||||||
|
| `terraform state list` | пусто (все 5 ресурсов убраны из стейта) |
|
||||||
|
| Кластер `94627ff4…` | `suspended`, `isSuspended=true`, не удалён |
|
||||||
|
| vDC `d0937335…` | `suspended`, `isSuspended=true`, не удалён |
|
||||||
|
| Эдж `2c37fed1…` | `running`, `ipSpaceName=internet-ipv4-v1` (SNAT включён) |
|
||||||
|
| Орга `57eeacd1…` | `running`, `vIPConfigure=[{name:internet-ipv4-v1,count:3}]` (квота не тронута) |
|
||||||
|
| Кластерный API `.146:6443` | TCP принимается эджем, но k8s не отвечает (`connection reset by peer`) — ВМ кластера спят |
|
||||||
|
| Ingress `.148:443` | открыт (эдж/AVI живут) |
|
||||||
|
|
||||||
|
Осталось проверить обратный ход: `terraform apply` должен усыновить те же инстансы (`adopt_existing_on_create=true`)
|
||||||
|
и разморозить их (`resume`) — запускает пользователь.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5.3. Баг: регистр UUID внутри JSON (первый `apply` после заморозки)
|
||||||
|
|
||||||
|
**Симптом.** `apply` после destroy (провайдер `2.0.22`) упал:
|
||||||
|
`Error: Ошибка клиента … required params mismatch for resource_name shturval-dev: startupConfiguration
|
||||||
|
(plan={…"nsxtUid":"2c37fed1-…"}, actual={…"nsxtUid":"2C37FED1-…"})`.
|
||||||
|
Эдж после пересоздания вернул UUID в lowercase, а в живом инстансе кластера тот же UUID лежит в UPPERCASE.
|
||||||
|
|
||||||
|
**Почему вылезло именно сейчас.** Регистр ранее учли в пяти местах — `core/refsvc.go:20` (lowercase при отправке),
|
||||||
|
`core/refsvc_resolve.go:28-29`, `resources_core/params_compare.go` (`normalizeCompareValue` — одиночные значения),
|
||||||
|
шаблон `instance.go:204` (`strings.EqualFold` для create-only), плюс восстановление регистра в state.
|
||||||
|
Ни одно из них не смотрит **внутрь JSON**, а adopt **приостановленного** инстанса сравнивает параметр целиком как JSON
|
||||||
|
(`RequiredParamsMismatch` → `paramsEquivalent` → `JSONStringsEquivalent` → `normalizeJSONScalarsToStrings`,
|
||||||
|
где было `case string: return val`). У Штурвала ref-параметры упакованы в JSON (`startupConfiguration`),
|
||||||
|
а путь adopt-suspended задействован впервые.
|
||||||
|
|
||||||
|
**Аудит: где ещё может вылезти.**
|
||||||
|
|
||||||
|
| # | Место | Что ломает |
|
||||||
|
|---|---|---|
|
||||||
|
| 1 | `resources_core/required_params_compare.go:94` | adopt suspended — hard error (сегодняшний кейс) |
|
||||||
|
| 2 | `core/modifier_compare.go:47,53` | ложное «не равно» → лишний `modify` при каждом apply (сейчас спит: у `org_ip_allocation` UUID внутри `vip_configure` нет) |
|
||||||
|
| 3 | `resources_core/state_refresh.go:150` | сохранение планового JSON при эквивалентности → в стейт уедет регистр API |
|
||||||
|
| 4 | `resources_core/resource_diagnostics_required.go:104` | та же `RequiredParamsMismatch` в create-диагностике |
|
||||||
|
| 5 | `resources_core/params_compare.go` (`ParamsMatchForResume`) | одиночный UUID ок, JSON — та же дыра (в сгенерированном коде не вызывается) |
|
||||||
|
| 6 | `resources_core/json_planmodifier.go` (`JsonNormalize`) | только `json.Compact` → для user-facing JSON-атрибутов с UUID риск вечного diff |
|
||||||
|
| 7 | `resources_core/ref_validation.go` (`ValidateRefParamsOnAdopt`) | ref-параметр, зашитый внутрь JSON, не проверяется вовсе → чужой инстанс не отловится (открыто) |
|
||||||
|
| 8 | `core/operation_run.go:151`, `operation_run_bycode.go:125` (`lookupLiveParam`) | подстановка live-значений по ключам; при другом регистре ключа молча не сработает (надо проверить, открыто) |
|
||||||
|
|
||||||
|
**Фикс (коммит — см. ниже).**
|
||||||
|
|
||||||
|
- `internal/core/jsonutil/jsonutil.go`: добавлен `LowercaseUUIDsInText` (regex по UUID-подстроке) и строковые значения
|
||||||
|
внутри JSON теперь нормализуются (`normalizeJSONScalarsToStrings`, `case string`) — закрывает пункты 1–5 сразу.
|
||||||
|
- `internal/resources_core/json_planmodifier.go`: `JsonNormalize()` после `json.Compact` приводит UUID-подстроки
|
||||||
|
к lowercase (типы и порядок ключей НЕ меняются) — закрывает пункт 6.
|
||||||
|
- Тесты: `internal/core/jsonutil/jsonutil_test.go` (UUID внутри вложенного JSON, регистр, разные UUID, числа/bool,
|
||||||
|
текст без UUID), `internal/resources_core/params_compare_test.go` (`paramsEquivalent` на реальном `startupConfiguration`).
|
||||||
|
|
||||||
|
**Открыто (7–8):** валидация ref-параметров внутри JSON и регистр ключей в `lookupLiveParam` — отдельная задача
|
||||||
|
(требует решения, что делать при mismatch, и живой проверки).
|
||||||
|
|
||||||
|
**Релиз:** `2.0.23` собран и залит в dev-реестр (`03_build_and_upload_provider.sh`), версия видна в реестре;
|
||||||
|
`VERSIONS.md` обновлён. После него нужно повторить `apply` на стенде (усыновление + `resume`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Мои ошибки в этой сессии (обязательно к фиксации)
|
||||||
|
|
||||||
|
1. Сказал, что apply «либо даст ошибку, либо создаст дубль кластера» — **неверно**: будет hard error
|
||||||
|
«РЕСУРС С ТАКИМ ИМЕНЕМ УЖЕ СУЩЕСТВУЕТ», дубль не создаётся (проверено в коде).
|
||||||
|
2. Интерпретировал `will be created` в плане как доказательство отсутствия adopt — adopt работает в `Create`, не в plan.
|
||||||
|
3. Перепутал колонки UI: «17/24» — это «Системные сервисы», а «Ingress» — домен-шаблон, а не счётчик.
|
||||||
|
4. Предлагал ручные обходы (`terraform state rm`, `removed`-блок, `-target`) там, где требуется автоматический
|
||||||
|
freeze флагами — пользователь это отклонил.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Открытые вопросы / тикет в платформу
|
||||||
|
|
||||||
|
1. Job установщика: Failed-поды живут сутки (`ttlSecondsAfterFinished: 86400`), `backoffLimit: 10`,
|
||||||
|
очистки нет; установка компонентов идёт до готовности Cilium → EPERM на webhook-ах.
|
||||||
|
2. Эдж: в `availableOperations` нет `suspend` → «заморозить» его платформенно невозможно, только «не трогать».
|
||||||
|
3. Квота IP: `count` нельзя опустить ниже занятых, штатного API «занято N» нет — только текст ошибки.
|
||||||
|
4. vDC: полное удаление только через 14 дней после `suspend`; при живых сущностях — через поддержку.
|
||||||
@@ -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,45 @@
|
|||||||
|
# CHAT RESUME — Штурвал dev-00: диагностика + дизайн «freeze on destroy» (2026-09-24)
|
||||||
|
|
||||||
|
> Полная версия с источниками: `NOTES/30_analysis/SHTURVAL_DEV00_DIAG_AND_FREEZE_DESIGN_2026-09-24.md`
|
||||||
|
|
||||||
|
## Что сделано
|
||||||
|
|
||||||
|
1. **Проверка кластера из локали** (контекст `tazet@narod.ru@shturval-dev-00`, API `185.247.187.146:6443`):
|
||||||
|
всё зелёное — 2 ноды Ready, `shturvalserviceconfigs` 41/41 `ready`, `nodeconfigitems` 4/4,
|
||||||
|
у всех 35 сервисов есть endpoints. Остался только «мусор»: 4 подвисших пода `kube-system/shturval-init-job`
|
||||||
|
(3 Error + 1 Unknown) — Job уже `Complete 1/1`, поды уйдут сами по `ttlSecondsAfterFinished: 86400`
|
||||||
|
(~25.09 14:31 UTC). Причина падений — webhook-вызовы до готовности Cilium: `connect: operation not permitted`.
|
||||||
|
2. **Расшифрованы счётчики ЛК**: `Pods 44/48` = готовые/всего (48 = поды без Completed); «Системные сервисы»
|
||||||
|
= число сервисов в режиме `auto` (17/24 → 24/24 после установки); «Ingress» = домен, не счётчик;
|
||||||
|
⚠️ на «Pods» = те 4 подвисших пода.
|
||||||
|
3. **Разобрана ошибка destroy**: `nubes_vc_org_ip_allocation` шлёт `count=0`, платформа не даёт опустить
|
||||||
|
`count` ниже занятых (2 адреса держит кластер: `.146` API и `.148` ingress; `suspend` адреса не освобождает).
|
||||||
|
Destroy прервался на аллокации, SNAT успел сняться → «рваное» состояние.
|
||||||
|
4. **Adopt починен**: добавлен `adopt_existing_on_create = true` в `DEV_STAND/FullPipe/shturval.tf`
|
||||||
|
(коммит `57abb7b`). Apply усыновил существующий инстанс `94627ff4-…` и сам сделал `resume` (18:26:30) —
|
||||||
|
кластер снова running, SNAT восстановлен (`internet-ipv4-v1`, modify 18:23:12).
|
||||||
|
5. **Решение по дизайну** (Опус + наше): три режима destroy в одной логике — `delete` (дефолт),
|
||||||
|
`suspend` (где сервис умеет), `keep` → `state_only` (эдж, SNAT, квота IP). Реализация — **через генератор**
|
||||||
|
(`TOOLS/resource-generator`: types/loader/templates + `keep_on_destroy_default` в YAML), не ручными правками
|
||||||
|
`resources_gen/`. Дефолты провайдера остаются разрушающими; freeze включается явно в `.tf` стенда.
|
||||||
|
|
||||||
|
## Что осталось сделать (по команде)
|
||||||
|
|
||||||
|
1. Правка генератора: `keep_on_destroy` для инстанс-ресурсов (эдж в первую очередь) + предупреждения в `Delete`.
|
||||||
|
2. `DEV_STAND/FullPipe`: `keep_on_destroy = true` в `modifiers.tf` (:29 snat, :39 квота) и в `edge.tf`
|
||||||
|
(+ `adopt_existing_on_create = true`), кластеру — явный `suspend_on_destroy = true`.
|
||||||
|
3. Регенерация + проверка воспроизводимости `10_yaml_stability_run.sh`, сборка/релиз провайдера.
|
||||||
|
4. Тикет в платформу: TTL/очистка Failed-подов установщика, отсутствие `suspend` у эджа, `count` ниже занятых.
|
||||||
|
|
||||||
|
## Полезное для воспроизведения
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# состояние кластера
|
||||||
|
kubectl get nodes; kubectl get pods -A | grep -v -E "Running|Completed"
|
||||||
|
kubectl -n kube-system get job shturval-init-job -o json | jq '.spec.backoffLimit,.spec.ttlSecondsAfterFinished,.status'
|
||||||
|
# API ЛК dev (нужны User-Agent и Referer, иначе 403)
|
||||||
|
TOK=$(tr -d '\n' < secrets/narodDEV.token)
|
||||||
|
curl -s -H "Authorization: Bearer $TOK" -H "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36" \
|
||||||
|
-H "Referer: https://deck-dev.ngcloud.ru/" \
|
||||||
|
'https://lk-api-gateway-dev.ngcloud.ru/api/v1/svc/instances/94627ff4-33a5-48f2-aca1-695741e0b6a2'
|
||||||
|
```
|
||||||
@@ -0,0 +1,224 @@
|
|||||||
|
# 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. ⛔ **ИСПРАВЛЕНО 2026-09-24. Прежняя формулировка «схема строится ТОЛЬКО из `create`» — НЕВЕРНА.**
|
||||||
|
Генератор **мержит** create+modify: `TOOLS/resource-generator/internal/loader/loader.go:96` →
|
||||||
|
`schemaParams := params.Merge(createParams, modifyParams)`; коммит `261809b` (2026-09-22)
|
||||||
|
«is_modifiable=true → параметр НЕ create-only».
|
||||||
|
Следствие: modify-параметры **уже в схемах** и применяются в `Update` —
|
||||||
|
`nubes_vc_org.v_ip_configure` (шлёт `662`), `nubes_vc_nsxt.ip_space_name` (шлёт `372`).
|
||||||
|
Разбор и live-факты: `NOTES/30_analysis/HAR_FRESH_CREATE_2026-09-24.md`.
|
||||||
|
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.
|
||||||
|
4. **Ложный «факт» §3.1 («схема только из `create`»).** Проверено в коде 2026-09-24: генератор мержит
|
||||||
|
create+modify (`loader.go:96`), поэтому `v_ip_configure` и `ip_space_name` **уже есть** в схемах
|
||||||
|
`nubes_vc_org` / `nubes_vc_nsxt` и работают через `Update`. Вывод «прописать поле в .tf → падает на plan»
|
||||||
|
относится максимум к провайдеру, собранному до коммита `261809b` (2026-09-22). Детали —
|
||||||
|
`NOTES/30_analysis/HAR_FRESH_CREATE_2026-09-24.md`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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/30_analysis/HAR_FRESH_CREATE_2026-09-24.md` — разбор create орги/эджа + свежий `state.params` (`vIPConfigure: [{}]`, отсутствие `ipSpaceName`)
|
||||||
|
- `NOTES/30_analysis/HAR_SNAT_MODIFY_FINDINGS.md` — правки/ограничения (часть опровергнута тестом; раньше в карте отсутствовал)
|
||||||
|
- `HAR/globak.har`, `HAR/org_already exists.har` — записи ЛК от 2026-09-24
|
||||||
|
- `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
|
| **Инструкции: сборка, заливка, добавление сервиса** | **[`HOW_TO/`](HOW_TO/README.md)** ← начинать отсюда |
|
||||||
S3 bucket, VM upload and mirror steps, stand-specific URLs, and the legacy
|
| Рабочие материалы: планы, промпты, анализы, выжимки чатов | [`NOTES/`](NOTES/README.md) |
|
||||||
script that must not be used.
|
| Пользовательская документация (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
|
```bash
|
||||||
export TOKEN_FILE=/home/naeel/terra/08-33-41.token
|
cd /home/naeel/TF/tf_provider
|
||||||
./01_generate_yamls.sh
|
|
||||||
|
# 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:
|
Ключевое: **схема tf-ресурса строится из операции `create`** в YAML, а ID операций/параметров
|
||||||
- Go files in `universal_rebuild/internal/resources_gen`
|
сохраняются из API. Нюансы и известные ограничения — в `NOTES/30_analysis/` и `NOTES/README.md`.
|
||||||
- Docs in `docs/30_registry/resources`
|
|
||||||
|
|
||||||
## Step 3: Build and upload provider
|
---
|
||||||
|
|
||||||
Script: `03_build_and_upload_provider.sh`
|
## Стенды и реестр
|
||||||
|
|
||||||
Uses `registry-server-build/build-provider.sh` and signs with:
|
| Стенд | Namespace | Диапазон версий | Профиль |
|
||||||
- `secrets/private_key.asc` (ignored by git)
|
|---|---|---|---|
|
||||||
|
| PROD | `nubes` | `1.*` | `TOOLS/config/prod` |
|
||||||
|
| DEV | `nubes-dev` | `2.*` | `TOOLS/config/dev` |
|
||||||
|
| TEST | `nubes-test` | `3.*` | `TOOLS/config/test` |
|
||||||
|
|
||||||
Example:
|
- Реестр: `tf-registry.containerk8s.services.ngcloud.ru`; бакет бинарников `nubes-terraform-registry`.
|
||||||
```bash
|
- Общий конфиг реестра: `TOOLS/config/registry.env`; стенд-специфика: `TOOLS/config/<стенд>/profile.env`
|
||||||
export S3_ENDPOINT=https://s3.msk-1.ngcloud.ru
|
(`NUBES_API_ENDPOINT`, `TOKEN_FILE`, `NAMESPACE`, `VERSION`).
|
||||||
export S3_ACCESS_KEY=...
|
- Список сервисов для генерации: `TOOLS/config/services_list.txt`.
|
||||||
export S3_SECRET_KEY=...
|
|
||||||
./03_build_and_upload_provider.sh 2.0.2
|
|
||||||
```
|
|
||||||
|
|
||||||
## Step 4: Build and publish docs
|
---
|
||||||
|
|
||||||
Script: `04_build_and_publish_docs.sh`
|
## ⛔ Чего не делать
|
||||||
|
|
||||||
Example:
|
- **Не использовать** легаси-схемы версий (`prod=2.*`, `dev=3.*`, `test=5.*`, `0.0.1`).
|
||||||
```bash
|
- **Не использовать** закрытые API и хосты: `index.cfm`, `registry.kube5s.ru`, `deck-api.ngcloud.ru`.
|
||||||
export S3_ENDPOINT=https://s3.msk-1.ngcloud.ru
|
- **Не вызывать** старые бинарники из `TOOLS/*/bin/` — скрипты пересобирают генераторы сами.
|
||||||
export S3_ACCESS_KEY=...
|
- **Не перегенерировать GPG-ключ** подписи — сломается `terraform init` у пользователей.
|
||||||
export S3_SECRET_KEY=...
|
- **Не путать бакеты:** бинарники `nubes-terraform-registry`, документация `terraform-registry`.
|
||||||
./04_build_and_publish_docs.sh 2.0.2
|
- **Не опираться** на файлы с баннером ⛔ в `NOTES/` и `HISTORY/` — это отменённые («ложные») пути.
|
||||||
```
|
|
||||||
|
|
||||||
## 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
|
|
||||||
|
|||||||
@@ -0,0 +1,200 @@
|
|||||||
|
name: vc_nsxt
|
||||||
|
service_id: 22
|
||||||
|
service_display_name: Сетевой шлюз периметра (Edge)
|
||||||
|
service_short_name: vc_nsxt
|
||||||
|
service_man: '# Инструкция по развертыванию Сетевой шлюз периметра (Edge) через платформу<br/><br/>## 1. Общая информация<br/><br/>Сервис **Сетевой шлюз периметра (Edge)** предназначен для создания периметрового сетевого шлюза в рамках одного виртуального датацентра (vDC) или группы виртуальных датацентров (groupvDC). <br/>При создании автоматически разворачивается routed-сеть с адресным пространством `10.10.102.0/24`. <br/>Сервис обеспечивает сетевую изоляцию, маршрутизацию, а также может включать функциональность балансировщика нагрузки AVI (ALB) для последующей интеграции, включая поддержку кластеров Штурвал.<br/><br/>### Доступные операции<br/><br/>**create** — Создание нового Edge с привязкой к vDC или groupvDC и развёртыванием routed-сети. <br/>**delete** — Удаление ранее созданного Edge. Недоступно при наличии зависимых услуг (vApp, VM, Кластер Штурвал). <br/>**modify** — Изменение параметров и сетевых настроек существующего Edge.<br/><br/>---<br/><br/>## 2. Параметры развертывания<br/><br/>Ниже приведены параметры операции **create**.<br/><br/>### Тип родительской услуги<br/>Определяет контекст размещения Edge. <br/>Допустимые значения: `vdc`, `groupvdc`. <br/>Использование:<br/>- При выборе `vdc` обязателен параметр **UUID VDC**.<br/>- При выборе `groupvdc` обязателен параметр **UUID Группы VDC**.<br/><br/>### UUID VDC<br/>Идентификатор виртуального датацентра. <br/>Необходимо предварительно создать vDC через услугу «Виртуальный датацентр (vDC)». <br/>Указывается только при выборе родительского типа `vdc`.<br/><br/>### UUID Группы VDC<br/>Идентификатор группы виртуальных датацентров. <br/>Создаётся через услугу «Группа виртуальных датацентров (groupvDC)». <br/>Используется при выборе родительского типа `groupvdc`.<br/><br/>### Включить ALB<br/>Флаг активации AVI Load Balancer в Cloud Director. <br/>Пример значения: `true`/`false`. <br/>Нужен для развёртывания кластера Штурвал и возможности создания Virtual Services.<br/><br/>### Наименование segroup<br/>Имя сервиса групп (segroup) на уровне ресурсной платформы. <br/>Обязательно при включённом ALB. <br/>Определяет группу, в которой будут выделяться пулы AVI для Virtual Services.<br/><br/>### virtualServicesCount<br/>Количество резервируемых виртуальных сервисов (Virtual Services) на AVI. <br/>Типичное значение: 1–10 в зависимости от нагрузки.<br/><br/>---<br/><br/>## 3. Рекомендованные характеристики<br/><br/>### Тестовое окружение (Test)<br/><br/>- Тип родительской услуги: `vdc` <br/>- virtualServicesCount: 1–2 <br/>- ALB: выключен по умолчанию (включать только при необходимости тестирования Штурвала) <br/>- segroup: задаётся только при включённом ALB <br/>- VDC гарантии CPU/RAM: минимальные (поскольку изменить их после создания нельзя)<br/><br/>### Промышленное окружение (Production)<br/><br/>- Тип родительской услуги: `groupvdc` при работе с распределёнными нагрузками или `vdc` для локализованных проектов <br/>- virtualServicesCount: 3–10, исходя из предполагаемого количества публикаций <br/>- ALB: включён, если требуется высокая доступность сервисов или используется кластер Штурвал <br/>- segroup: рекомендуется использовать выделенную группу под проект <br/>- VDC гарантии CPU/RAM: повышенные, с учётом того, что изменить их после создания невозможно без пересоздания vDC<br/><br/>---<br/><br/>## 4. Выходные параметры<br/><br/>### Имя Edge<br/>Уникальное название созданного сетевого шлюза, используется для ссылок и дальнейших операций.<br/><br/>### Имя routed-сети<br/>Автоматически созданная routed-сеть с подсетью `10.10.102.0/24`, используется для подключения ресурсов.<br/><br/>### Параметры Edge<br/>Набор параметров, указанных пользователем при создании: тип услуги, ALB, segroup, virtualServicesCount и др. <br/>Используются в операциях modify и для анализа состояния сервиса.<br/><br/>---<br/><br/>## 5. Дополнительная информация<br/><br/>- В рамках сервиса **один раз при создании задаётся гарантированная доля CPU/RAM vDC**. Изменить её невозможно — требуется пересоздание vDC. <br/>- Удаление Edge недоступно при наличии зависимых ресурсов (vApp, VM, кластер Штурвал). <br/>- При использовании ALB необходимо корректно указывать segroup, чтобы обеспечить корректное выделение пулов AVI. <br/>- При выборе режима `groupvdc` следует учитывать распределение нагрузки между несколькими vDC. <br/>- SNAT для всех ресурсов внутри Edge можно активировать через операцию modify (параметры выделения VIP и ipSpace).<br/><br/>'
|
||||||
|
lifecycle:
|
||||||
|
suspend_on_destroy_default: false
|
||||||
|
adopt_existing_on_create_default: false
|
||||||
|
outputs:
|
||||||
|
params:
|
||||||
|
- code: state_params
|
||||||
|
type: map
|
||||||
|
- code: state_out
|
||||||
|
type: map
|
||||||
|
- code: state_params_flat
|
||||||
|
type: map
|
||||||
|
- code: state_out_flat
|
||||||
|
type: map
|
||||||
|
- code: vault_secrets
|
||||||
|
type: map
|
||||||
|
sensitive: true
|
||||||
|
- code: vault_url
|
||||||
|
type: string
|
||||||
|
- code: vault_user_path
|
||||||
|
type: string
|
||||||
|
- code: vault_fields
|
||||||
|
type: list
|
||||||
|
operations:
|
||||||
|
- name: create
|
||||||
|
id: 10
|
||||||
|
kind: instance
|
||||||
|
action: create
|
||||||
|
params:
|
||||||
|
- id: 8
|
||||||
|
code: vdcUid
|
||||||
|
data_type: string
|
||||||
|
required: false
|
||||||
|
ref_svc_id: 21
|
||||||
|
descr: UUID Услуги `Виртуальный датацентр (vDC)`
|
||||||
|
man: 'Необходимо, если выбран тип подключаемой VDC: `vdc`'
|
||||||
|
sort: 10
|
||||||
|
- id: 340
|
||||||
|
code: needEnableAVI
|
||||||
|
data_type: boolean
|
||||||
|
required: true
|
||||||
|
default: "false"
|
||||||
|
value_list:
|
||||||
|
- "false"
|
||||||
|
- "true"
|
||||||
|
descr: Включение Load Balancer
|
||||||
|
man: Параметры `Наименование segroup`, `Кол-во VS на AVI` необходимо также указать
|
||||||
|
sort: 30
|
||||||
|
is_modifiable: true
|
||||||
|
- id: 341
|
||||||
|
code: virtualServicesCount
|
||||||
|
data_type: integer > 0
|
||||||
|
required: false
|
||||||
|
default: "1"
|
||||||
|
maxvalue: 4
|
||||||
|
minvalue: 1
|
||||||
|
descr: Кол-во виртуальных сервисов, которые **резервируются** на AVI
|
||||||
|
man: Во избежании коллапса пока выделяется до 4<br/>Необходимо указывать, если включён параметр `Включить ALB`
|
||||||
|
sort: 50
|
||||||
|
is_modifiable: true
|
||||||
|
- id: 621
|
||||||
|
code: vdcType
|
||||||
|
data_type: string
|
||||||
|
required: true
|
||||||
|
default: vdc
|
||||||
|
value_list:
|
||||||
|
- vdc
|
||||||
|
- vdcGroup
|
||||||
|
descr: Тип родительской услуги
|
||||||
|
man: При выборе vdc обеспечивает сетевую доступность только в рамках этого vdc<br/>При выборе groupvdc обеспечивает сетевую доступность между всеми vdc, которые включены в groupvdc
|
||||||
|
sort: 0
|
||||||
|
- id: 622
|
||||||
|
code: vdcGroupUid
|
||||||
|
data_type: string
|
||||||
|
required: false
|
||||||
|
ref_svc_id: 29
|
||||||
|
descr: UUID Услуги `Группа виртуальных датацентров (groupvDC)`
|
||||||
|
man: 'Необходимо, если выбран тип подключаемой VDC: `groupvdc`'
|
||||||
|
sort: 20
|
||||||
|
- id: 825
|
||||||
|
code: qosProfile
|
||||||
|
required: false
|
||||||
|
descr: Параметр пока не работает<br/>Должен возвращать поле из ресурсной платформы типа vc из .vcd.hardware.gatewayQoSProfiles<br/><br/>Как по идее должен работать.<br/>Нужно заполнить или CFS vdcUid, или vdcGroupUid<br/>Идеально конечно проверять `if (params.vdcType == "vdc" && params.vdcUid != "")` или `if (params.vdcType == "vdcGroup" && params.vdcGroupUid != "")`<br/><br/>Далее надо пойти по стейту vdc -> org -> resPlatform, взять gatewayQoSProfiles<br/>Или пойти по стейту vdcgroup -> vdc -> org -> resPlatform, взять gatewayQoSProfiles<br/><br/>Регулярку могу написать (Виталя)
|
||||||
|
man: if (vdcUid != '") {<br/> наборфункций1<br/>} <br/><br/>elif (vdcGroupUid != "") {<br/> наборфункций2<br/>}<br/><br/>else {<br/> return "Необходимо выбрать vdc или vdcgroup"<br/>}
|
||||||
|
sort: 60
|
||||||
|
depends_on: vdcUid,vdcGroupUid
|
||||||
|
is_modifiable: true
|
||||||
|
- id: 1110
|
||||||
|
code: routedNetConfiguration
|
||||||
|
data_type: map-fixed
|
||||||
|
required: true
|
||||||
|
sort: 70
|
||||||
|
is_modifiable: true
|
||||||
|
sub_params:
|
||||||
|
- id: 649
|
||||||
|
code: ipAddrPool
|
||||||
|
data_type: string
|
||||||
|
required: true
|
||||||
|
default: 10.10.102.0/24
|
||||||
|
regex: ^((25[0-4]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?|0)\.){3}0/24$
|
||||||
|
man: '**Параметр не изменяемый**<br/>Адрессный пул, которая будет присвоена routed-сети. Описывается как (10.10.10.0/24). Маска 24 обязательна. (.1) - шлюз. (.2-.254) - Под адресацию для ВМ'
|
||||||
|
is_modifiable: false
|
||||||
|
- id: 650
|
||||||
|
code: mainDns
|
||||||
|
data_type: string
|
||||||
|
required: true
|
||||||
|
default: 81.22.46.22
|
||||||
|
regex: ^((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?|0)\.){3}(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?|0)$
|
||||||
|
man: Подставляется автоматически в resolv.conf при создании виртуальных машин
|
||||||
|
is_modifiable: false
|
||||||
|
- id: 651
|
||||||
|
code: secondDns
|
||||||
|
data_type: string
|
||||||
|
required: true
|
||||||
|
default: 185.247.187.77
|
||||||
|
regex: ^((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?|0)\.){3}(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?|0)$
|
||||||
|
man: Подставляется автоматически в resolv.conf при создании виртуальных машин
|
||||||
|
is_modifiable: false
|
||||||
|
- name: delete
|
||||||
|
id: 25
|
||||||
|
kind: instance
|
||||||
|
action: delete
|
||||||
|
man: Невозможно удалить если routed-сеть в Edge связана с:<br/>- vApp<br/>- VM<br/>- Kubernetes-кластер Штурвал<br/><br/>Запустить операцию возможно, но будет ошибка
|
||||||
|
params: []
|
||||||
|
- name: modify
|
||||||
|
id: 111
|
||||||
|
kind: instance
|
||||||
|
action: modify
|
||||||
|
man: Для создания SNAT Правила убедитесь, что в организации есть свободные IP<br/>Редактировать кол-во свободных IP можно в услуге `Организация в Cloud Director` -> `modify`
|
||||||
|
params:
|
||||||
|
- id: 368
|
||||||
|
code: needEnableAVI
|
||||||
|
data_type: boolean
|
||||||
|
required: false
|
||||||
|
value_list:
|
||||||
|
- "false"
|
||||||
|
- "true"
|
||||||
|
descr: Включение Load Balancer
|
||||||
|
man: Параметры `Наименование segroup`, `Кол-во VS на AVI` необходимо также указать
|
||||||
|
sort: 10
|
||||||
|
- id: 369
|
||||||
|
code: virtualServicesCount
|
||||||
|
data_type: integer > 0
|
||||||
|
required: false
|
||||||
|
maxvalue: 4
|
||||||
|
minvalue: 1
|
||||||
|
descr: Кол-во виртуальных сервисов, которые выделяются на AVI
|
||||||
|
man: Во избежании коллапса пока выделяется до 4<br/>Необходимо указывать, если включён параметр `Включить ALB`
|
||||||
|
sort: 30
|
||||||
|
- id: 372
|
||||||
|
code: ipSpaceName
|
||||||
|
data_type: string
|
||||||
|
required: false
|
||||||
|
descr: Имя ip Space для внешнего IP
|
||||||
|
man: Необходимо указывать, если включён параметр `Выделить VIP для SNAT`
|
||||||
|
sort: 50
|
||||||
|
- id: 856
|
||||||
|
code: qosProfile
|
||||||
|
data_type: string
|
||||||
|
required: false
|
||||||
|
sort: 40
|
||||||
|
- id: 1112
|
||||||
|
code: routedNetConfiguration
|
||||||
|
data_type: map-fixed
|
||||||
|
required: true
|
||||||
|
sort: 70
|
||||||
|
sub_params:
|
||||||
|
- id: 652
|
||||||
|
code: ipAddrPool
|
||||||
|
data_type: string
|
||||||
|
required: true
|
||||||
|
default: ""
|
||||||
|
regex: ^((25[0-4]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?|0)\.){3}0/24$
|
||||||
|
man: '**Параметр не изменяемый**<br/>Адрессный пул, которая будет присвоена routed-сети. Описывается как (10.10.10.0/24). Маска 24 обязательна. (.1) - шлюз. (.2-.254) - Под адресацию для ВМ'
|
||||||
|
is_modifiable: false
|
||||||
|
- id: 653
|
||||||
|
code: mainDns
|
||||||
|
data_type: string
|
||||||
|
required: true
|
||||||
|
default: ""
|
||||||
|
regex: ^((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?|0)\.){3}(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?|0)$
|
||||||
|
man: Подставляется автоматически в resolv.conf при создании виртуальных машин
|
||||||
|
is_modifiable: false
|
||||||
|
- id: 654
|
||||||
|
code: secondDns
|
||||||
|
data_type: string
|
||||||
|
required: true
|
||||||
|
default: ""
|
||||||
|
regex: ^((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?|0)\.){3}(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?|0)$
|
||||||
|
man: Подставляется автоматически в resolv.conf при создании виртуальных машин
|
||||||
|
is_modifiable: false
|
||||||
|
- name: reconcile
|
||||||
|
id: 252
|
||||||
|
kind: action
|
||||||
|
action: reconcile
|
||||||
|
params: []
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
resource "nubes_vc_nsxt" "edge" {
|
||||||
|
resource_name = var.nsxt_resource_name
|
||||||
|
|
||||||
|
# Тип родительской услуги: "vdc" (нужен vdc_uid) или "vdcGroup" (нужен vdc_group_uid)
|
||||||
|
vdc_type = var.nsxt_vdc_type
|
||||||
|
|
||||||
|
# refSvc-поле: принимает UUID или имя. Здесь берём UID созданного VDC,
|
||||||
|
# чтобы Edge гарантированно создавался после vDC.
|
||||||
|
vdc_uid = nubes_vc_vdc.vdc.id
|
||||||
|
|
||||||
|
need_enable_avi = var.nsxt_need_enable_avi
|
||||||
|
virtual_services_count = var.nsxt_virtual_services_count
|
||||||
|
|
||||||
|
# routed-сеть, которую разворачивает Edge (SingleNestedAttribute -> объект)
|
||||||
|
routed_net_configuration = {
|
||||||
|
ip_addr_pool = var.nsxt_ip_addr_pool
|
||||||
|
main_dns = var.nsxt_main_dns
|
||||||
|
second_dns = var.nsxt_second_dns
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,583 @@
|
|||||||
|
package templates
|
||||||
|
|
||||||
|
const Instance = `package resources_gen
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
{{- if .NeedsStringsImport }}
|
||||||
|
"strings"
|
||||||
|
{{- end }}
|
||||||
|
{{- if .NeedsFmtImport }}
|
||||||
|
"fmt"
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
"terraform-provider-nubes/internal/core"
|
||||||
|
"terraform-provider-nubes/internal/resources_core"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/path"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
|
||||||
|
{{- if .NeedsInt64Default }}
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
|
||||||
|
{{- end }}
|
||||||
|
{{- if .NeedsStringDefault }}
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
|
||||||
|
{{- end }}
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
|
||||||
|
{{- if .NeedsBoolUseStateForUnknown }}
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier"
|
||||||
|
{{- end }}
|
||||||
|
{{- if .NeedsInt64UseStateForUnknown }}
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier"
|
||||||
|
{{- end }}
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Code generated by TOOLS/resource-generator. DO NOT EDIT.
|
||||||
|
// Service: {{.Name}}
|
||||||
|
// Service ID: {{.ServiceID}}
|
||||||
|
|
||||||
|
var _ resource.Resource = &{{ToCamel .Name}}Resource{}
|
||||||
|
var _ resource.ResourceWithModifyPlan = &{{ToCamel .Name}}Resource{}
|
||||||
|
var _ resource.ResourceWithImportState = &{{ToCamel .Name}}Resource{}
|
||||||
|
|
||||||
|
type {{ToCamel .Name}}Resource struct {
|
||||||
|
client *core.UniversalClient
|
||||||
|
}
|
||||||
|
|
||||||
|
{{- range .SchemaParams }}
|
||||||
|
{{- if (IsNested .) }}
|
||||||
|
// {{NestedModelName $.Name .Code}} — вложенная модель для map-fixed параметра {{.Code}}.
|
||||||
|
type {{NestedModelName $.Name .Code}} struct {
|
||||||
|
{{- range .SubParams }}
|
||||||
|
{{ToCamel .Code}} {{ParamType .}} ` + "`" + `tfsdk:"{{ToSnake .Code}}" json:"{{.Code}}"` + "`" + `
|
||||||
|
{{- end }}
|
||||||
|
}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
type {{ToCamel .Name}}Model struct {
|
||||||
|
ID types.String ` + "`" + `tfsdk:"id"` + "`" + `
|
||||||
|
ResourceName types.String ` + "`" + `tfsdk:"resource_name"` + "`" + `
|
||||||
|
OperationTimeout types.String ` + "`" + `tfsdk:"operation_timeout"` + "`" + `
|
||||||
|
LogLevel types.String ` + "`" + `tfsdk:"log_level"` + "`" + `
|
||||||
|
{{- if .HasRedeploy }}
|
||||||
|
// --- Redeploy support (ARCHITECTURE.md) ---
|
||||||
|
// git_revision: при изменении вызывает redeploy вместо modify.
|
||||||
|
// Опциональное поле — если не задано, modify работает как обычно.
|
||||||
|
GitRevision types.String ` + "`" + `tfsdk:"git_revision"` + "`" + `
|
||||||
|
{{- end }}
|
||||||
|
{{- range .SchemaParams }}
|
||||||
|
{{- if (IsNested .) }}
|
||||||
|
{{ToCamel .Code}} {{NestedTfType . $.Name}} ` + "`" + `tfsdk:"{{ToSnake .Code}}"` + "`" + `
|
||||||
|
{{- else }}
|
||||||
|
{{ToCamel .Code}} {{ParamType .}} ` + "`" + `tfsdk:"{{ToSnake .Code}}"` + "`" + `
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .SupportsSuspendDestroy }}
|
||||||
|
SuspendOnDestroy types.Bool ` + "`" + `tfsdk:"suspend_on_destroy"` + "`" + `
|
||||||
|
{{- end }}
|
||||||
|
AdoptExistingOnCreate types.Bool ` + "`" + `tfsdk:"adopt_existing_on_create"` + "`" + `
|
||||||
|
{{- range .OutputParams }}
|
||||||
|
{{ToCamel .Code}} {{OutputType .}} ` + "`" + `tfsdk:"{{ToSnake .Code}}"` + "`" + `
|
||||||
|
{{- end }}
|
||||||
|
}
|
||||||
|
|
||||||
|
func New{{ToCamel .Name}}Resource() resource.Resource {
|
||||||
|
return &{{ToCamel .Name}}Resource{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *{{ToCamel .Name}}Resource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
|
||||||
|
resp.TypeName = req.ProviderTypeName + "_{{.Name}}"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *{{ToCamel .Name}}Resource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
|
||||||
|
attrs := map[string]schema.Attribute{
|
||||||
|
"id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}},
|
||||||
|
"resource_name": schema.StringAttribute{Required: true},
|
||||||
|
"operation_timeout": schema.StringAttribute{Optional: true},
|
||||||
|
"log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."},
|
||||||
|
{{- range .SchemaParams }}
|
||||||
|
{{- if (IsNested .) }}
|
||||||
|
"{{ToSnake .Code}}": {{NestedSchemaBlock .}}
|
||||||
|
{{- range .SubParams }}
|
||||||
|
"{{ToSnake .Code}}": schema.{{SubSchemaType .}}Attribute{
|
||||||
|
{{- if .Default }}Optional: true, Computed: true, Default: {{SubDefaultExpr .}},{{else if .Required}}Required: true,{{else}}Optional: true,{{end}}
|
||||||
|
{{- if ne (ParamDescription .) "" }}MarkdownDescription: {{ParamDescription .}},{{end}}
|
||||||
|
},
|
||||||
|
{{- end }}
|
||||||
|
{{NestedSchemaEnd .}}
|
||||||
|
{{- else }}
|
||||||
|
"{{ToSnake .Code}}": schema.{{if eq (ParamType .) "types.Bool"}}Bool{{else if eq (ParamType .) "types.Int64"}}Int64{{else}}String{{end}}Attribute{
|
||||||
|
{{- if and .Required (eq (ParamDefaultExpr .) "") (eq .RefSvcId 0) }}Required: true,{{else}}Optional: true,{{end}}
|
||||||
|
{{- if ne (ParamDefaultExpr .) "" }}Computed: true, Default: {{ParamDefaultExpr .}},{{- else if or .IsJson (ShouldBeOptionalComputed .) }}Computed: true,{{- end }}
|
||||||
|
{{- if ne (ParamDescription .) "" }}MarkdownDescription: {{ParamDescription .}},{{end}}
|
||||||
|
{{- if .Sensitive }}Sensitive: true,{{end}}
|
||||||
|
{{- if .IsJson }}PlanModifiers: []planmodifier.String{resources_core.JsonNormalize()},{{- else if ShouldUseStateForUnknown . }}PlanModifiers: []planmodifier.{{if eq (ParamType .) "types.Bool"}}Bool{boolplanmodifier.UseStateForUnknown()}{{else if eq (ParamType .) "types.Int64"}}Int64{int64planmodifier.UseStateForUnknown()}{{else}}String{stringplanmodifier.UseStateForUnknown()}{{end}},{{- end }}
|
||||||
|
},
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .HasRedeploy }}
|
||||||
|
// --- Redeploy support (ARCHITECTURE.md) ---
|
||||||
|
// При изменении git_revision вызывается redeploy вместо modify.
|
||||||
|
// Если не задано — modify работает без изменений.
|
||||||
|
"git_revision": schema.StringAttribute{Optional: true, MarkdownDescription: "Git revision (commit hash/tag). Changing this triggers redeploy instead of modify."},
|
||||||
|
{{- end }}
|
||||||
|
{{- if .SupportsSuspendDestroy }}
|
||||||
|
"suspend_on_destroy": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool({{.SuspendOnDestroy}})},
|
||||||
|
{{- end }}
|
||||||
|
"adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool({{.AdoptExistingOnCreate}})},
|
||||||
|
{{- range .OutputParams }}
|
||||||
|
{{- if or (OutputIsMap .) (OutputIsList .) }}
|
||||||
|
"{{ToSnake .Code}}": schema.{{if OutputIsMap .}}Map{{else}}List{{end}}Attribute{Computed: true, ElementType: types.StringType{{if OutputSensitive .}}, Sensitive: true{{end}}},
|
||||||
|
{{- else }}
|
||||||
|
"{{ToSnake .Code}}": schema.StringAttribute{Computed: true{{if OutputSensitive .}}, Sensitive: true{{end}}},
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.Schema = schema.Schema{Attributes: attrs}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *{{ToCamel .Name}}Resource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
|
||||||
|
if r.client == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var config *{{ToCamel .Name}}Model
|
||||||
|
resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if config == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var state *{{ToCamel .Name}}Model
|
||||||
|
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Destroy-план (plan == null): create-time проверку «уже существует / adopt»
|
||||||
|
// запускать нельзя — удаление не валидируется через существование инстанса.
|
||||||
|
if req.Plan.Raw.IsNull() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if state != nil && !state.ID.IsNull() && !state.ID.IsUnknown() {
|
||||||
|
var plan {{ToCamel .Name}}Model
|
||||||
|
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
{{- if .HasRefSvcParams }}
|
||||||
|
// FIX(uuid-case): resolve ref_svc params НЕ делаем в ModifyPlan.
|
||||||
|
// Terraform правило: plan ОБЯЗАН равняться config для user-provided атрибутов.
|
||||||
|
// Resolve (displayName → UUID или uppercase → lowercase) нужен только для
|
||||||
|
// API-вызова в Create/Update. Делать его здесь = менять plan = ошибка
|
||||||
|
// "Provider produced invalid plan: planned value does not match config value".
|
||||||
|
{{- end }}
|
||||||
|
if !plan.ResourceName.IsNull() && !plan.ResourceName.IsUnknown() && !state.ResourceName.IsNull() && !state.ResourceName.IsUnknown() {
|
||||||
|
if plan.ResourceName.ValueString() != state.ResourceName.ValueString() {
|
||||||
|
resp.Diagnostics.AddError("Нельзя изменить resource_name", "Параметр resource_name задается при создании и не может быть изменен. Создайте новый ресурс с другим именем.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{{- range .CreateOnlyParams }}
|
||||||
|
{{- if not (IsNested .) }}
|
||||||
|
if !plan.{{ToCamel .Code}}.IsNull() && !plan.{{ToCamel .Code}}.IsUnknown() && !state.{{ToCamel .Code}}.IsNull() && !state.{{ToCamel .Code}}.IsUnknown() {
|
||||||
|
{{- if eq (ParamType .) "types.Bool" }}
|
||||||
|
if plan.{{ToCamel .Code}}.ValueBool() != state.{{ToCamel .Code}}.ValueBool() {
|
||||||
|
resp.Diagnostics.AddError("Нельзя изменить {{ToSnake .Code}}", "Параметр задается при создании и не может быть изменен.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
{{- else if eq (ParamType .) "types.Int64" }}
|
||||||
|
if plan.{{ToCamel .Code}}.ValueInt64() != state.{{ToCamel .Code}}.ValueInt64() {
|
||||||
|
resp.Diagnostics.AddError("Нельзя изменить {{ToSnake .Code}}", "Параметр задается при создании и не может быть изменен.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
{{- else }}
|
||||||
|
// FIX(uuid-case): сравниваем без учёта регистра — API возвращает UUID
|
||||||
|
// в lowercase, пользователь мог написать upper/mixed. Это одно и то же
|
||||||
|
// значение, менять его нельзя только если оно реально другое.
|
||||||
|
if !strings.EqualFold(plan.{{ToCamel .Code}}.ValueString(), state.{{ToCamel .Code}}.ValueString()) {
|
||||||
|
resp.Diagnostics.AddError("Нельзя изменить {{ToSnake .Code}}", "Параметр задается при создании и не может быть изменен.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
{{- end }}
|
||||||
|
}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
{{- range .SchemaParams }}
|
||||||
|
{{- if and .Required (eq (ParamDefaultExpr .) "") (not (IsNested .)) }}
|
||||||
|
if config.{{ToCamel .Code}}.IsNull() {
|
||||||
|
resp.Diagnostics.AddError("Missing required attribute", "{{ToSnake .Code}} is required.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if config.{{ToCamel .Code}}.IsUnknown() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
// ⛔ Create-time проверка существования/усыновления здесь СОЗНАТЕЛЬНО НЕ вызывается.
|
||||||
|
//
|
||||||
|
// Причина: при tainted-ресурсе Terraform планирует ЗАМЕНУ (destroy+create), и
|
||||||
|
// create-узел замены приходит в ModifyPlan с prior state = null — ровно как у
|
||||||
|
// нового ресурса. Отличить «замену» от «создания» на этом уровне невозможно,
|
||||||
|
// поэтому проверка «РЕСУРС С ТАКИМ ИМЕНЕМ УЖЕ СУЩЕСТВУЕТ» ложно срабатывала на
|
||||||
|
// ещё не удалённый инстанс и блокировала plan/destroy.
|
||||||
|
//
|
||||||
|
// Проверка осталась в Create (CreateExistingResourceDiagnosticsWithDomainAndServices):
|
||||||
|
// на apply она выполняется ПОСЛЕ удаления старого инстанса, поэтому конфликта уже нет.
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *{{ToCamel .Name}}Resource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
|
||||||
|
var data {{ToCamel .Name}}Model
|
||||||
|
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
{{- range .SchemaParams }}
|
||||||
|
{{- if and .Required (eq (ParamDefaultExpr .) "") (not (IsNested .)) }}
|
||||||
|
if data.{{ToCamel .Code}}.IsNull() || data.{{ToCamel .Code}}.IsUnknown() {
|
||||||
|
resp.Diagnostics.AddError("Missing required attribute", "{{ToSnake .Code}} is required.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .HasRefSvcParams }}
|
||||||
|
{{- range .CreateParams }}
|
||||||
|
{{- if and (gt .RefSvcId 0) (eq (ParamType .) "types.String") }}
|
||||||
|
// refSvc-поле резолвим только для API-запроса.
|
||||||
|
// config не перезаписываем: пользовательский display name или UUID должен
|
||||||
|
// пройти в state ровно в том виде, в котором его передал Terraform.
|
||||||
|
resolved{{ToCamel .Code}} := data.{{ToCamel .Code}}.ValueString()
|
||||||
|
if !data.{{ToCamel .Code}}.IsNull() && !data.{{ToCamel .Code}}.IsUnknown() {
|
||||||
|
var err error
|
||||||
|
resolved{{ToCamel .Code}}, err = r.client.ResolveRefSvcParamValue(ctx, {{.RefSvcId}}, data.{{ToCamel .Code}}.ValueString())
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError("Ошибка клиента", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
resourceName := data.ResourceName.ValueString()
|
||||||
|
desiredDomain := ""
|
||||||
|
{{- if .HasDomainParam }}
|
||||||
|
if !data.Domain.IsNull() && !data.Domain.IsUnknown() {
|
||||||
|
desiredDomain = data.Domain.ValueString()
|
||||||
|
}
|
||||||
|
{{- end }}
|
||||||
|
domainServiceIDs := []int{ {{- range .DomainServiceIDs }}{{.}}, {{- end }} }
|
||||||
|
resp.Diagnostics.Append(resources_core.CreateExistingResourceDiagnosticsWithDomainAndServices(ctx, r.client, {{.ServiceID}}, resourceName, data.AdoptExistingOnCreate.ValueBool(), desiredDomain, domainServiceIDs, {{.SupportsSuspendDestroy}})...)
|
||||||
|
// ⛔ Проверяем HasError ДО create — при hard-error (running без adopt, suspend без adopt,
|
||||||
|
// not created, конфликт) сайд-эффект create не должен выполняться.
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
params := map[int]string{
|
||||||
|
{{- range .CreateParams }}
|
||||||
|
{{- if not (IsNested .) }}
|
||||||
|
{{- if and (gt .RefSvcId 0) (eq (ParamType .) "types.String") }}
|
||||||
|
{{.ID}}: resolved{{ToCamel .Code}},
|
||||||
|
{{- else }}
|
||||||
|
{{.ID}}: {{ParamFormat . (printf "data.%s" (ToCamel .Code))}},
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
}
|
||||||
|
{{- range .CreateParams }}
|
||||||
|
{{- if (IsNested .) }}
|
||||||
|
if data.{{ToCamel .Code}} != nil {
|
||||||
|
params[{{.ID}}] = {{NestedJSONExpr . "data"}}
|
||||||
|
}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
operationTimeout := ""
|
||||||
|
if !data.OperationTimeout.IsNull() && !data.OperationTimeout.IsUnknown() {
|
||||||
|
operationTimeout = data.OperationTimeout.ValueString()
|
||||||
|
}
|
||||||
|
if !data.LogLevel.IsNull() && !data.LogLevel.IsUnknown() {
|
||||||
|
ctx = core.CtxWithLogLevel(ctx, data.LogLevel.ValueString())
|
||||||
|
}
|
||||||
|
id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, {{.ServiceID}}, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError("Ошибка клиента", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data.ID = types.StringValue(id)
|
||||||
|
|
||||||
|
state, diags := resources_core.RefreshResourceState(ctx, r.client, id, {{.ServiceID}}, data, []resources_core.StateField{
|
||||||
|
{{- range .OutputParams }}
|
||||||
|
{Code: "{{.Code}}"},
|
||||||
|
{{- end }}
|
||||||
|
}, []resources_core.InputField{
|
||||||
|
{{- range .SchemaParams }}
|
||||||
|
{{- if eq .RefSvcId 0 }}
|
||||||
|
{Code: "{{.Code}}", Field: "{{ToCamel .Code}}", Type: "{{.Type}}"},
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
})
|
||||||
|
resp.Diagnostics.Append(diags...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *{{ToCamel .Name}}Resource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
|
||||||
|
var state {{ToCamel .Name}}Model
|
||||||
|
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if state.ID.IsNull() || state.ID.IsUnknown() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if r.client != nil {
|
||||||
|
remove, err := resources_core.ShouldRemoveFromState(ctx, r.client, state.ID.ValueString())
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError("Ошибка клиента", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if remove {
|
||||||
|
resp.State.RemoveResource(ctx)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
newState, diags := resources_core.RefreshResourceState(ctx, r.client, state.ID.ValueString(), {{.ServiceID}}, state, []resources_core.StateField{
|
||||||
|
{{- range .OutputParams }}
|
||||||
|
{Code: "{{.Code}}"},
|
||||||
|
{{- end }}
|
||||||
|
}, []resources_core.InputField{
|
||||||
|
{{- range .SchemaParams }}
|
||||||
|
{{- if eq .RefSvcId 0 }}
|
||||||
|
{Code: "{{.Code}}", Field: "{{ToCamel .Code}}", Type: "{{.Type}}"},
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
})
|
||||||
|
resp.Diagnostics.Append(diags...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *{{ToCamel .Name}}Resource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
|
||||||
|
var plan {{ToCamel .Name}}Model
|
||||||
|
var state {{ToCamel .Name}}Model
|
||||||
|
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
|
||||||
|
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
instanceID := state.ID
|
||||||
|
if instanceID.IsNull() || instanceID.IsUnknown() {
|
||||||
|
instanceID = plan.ID
|
||||||
|
}
|
||||||
|
if instanceID.IsNull() || instanceID.IsUnknown() {
|
||||||
|
resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра для modify")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
hasServiceParamChanges := false
|
||||||
|
{{- range .ModifyParams }}
|
||||||
|
{{- if not (IsNested .) }}
|
||||||
|
if !hasServiceParamChanges {
|
||||||
|
if plan.{{ToCamel .Code}}.IsNull() != state.{{ToCamel .Code}}.IsNull() || plan.{{ToCamel .Code}}.IsUnknown() != state.{{ToCamel .Code}}.IsUnknown() {
|
||||||
|
hasServiceParamChanges = true
|
||||||
|
} else if !plan.{{ToCamel .Code}}.IsNull() && !plan.{{ToCamel .Code}}.IsUnknown() && !state.{{ToCamel .Code}}.IsNull() && !state.{{ToCamel .Code}}.IsUnknown() {
|
||||||
|
{{- if eq (ParamType .) "types.Bool" }}
|
||||||
|
if plan.{{ToCamel .Code}}.ValueBool() != state.{{ToCamel .Code}}.ValueBool() {
|
||||||
|
hasServiceParamChanges = true
|
||||||
|
}
|
||||||
|
{{- else if eq (ParamType .) "types.Int64" }}
|
||||||
|
if plan.{{ToCamel .Code}}.ValueInt64() != state.{{ToCamel .Code}}.ValueInt64() {
|
||||||
|
hasServiceParamChanges = true
|
||||||
|
}
|
||||||
|
{{- else }}
|
||||||
|
if !strings.EqualFold(plan.{{ToCamel .Code}}.ValueString(), state.{{ToCamel .Code}}.ValueString()) {
|
||||||
|
hasServiceParamChanges = true
|
||||||
|
}
|
||||||
|
{{- end }}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{- if .HasRedeploy }}
|
||||||
|
// --- Redeploy support (ARCHITECTURE.md) ---
|
||||||
|
// Проверяем изменился ли git_revision.
|
||||||
|
// Если да — вызываем redeploy вместо modify.
|
||||||
|
// git_revision = "" (не задано) → обычный modify.
|
||||||
|
redeployRequested := false
|
||||||
|
if !plan.GitRevision.IsNull() && !plan.GitRevision.IsUnknown() {
|
||||||
|
if state.GitRevision.IsNull() || state.GitRevision.IsUnknown() || plan.GitRevision.ValueString() != state.GitRevision.ValueString() {
|
||||||
|
redeployRequested = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
if !hasServiceParamChanges{{if .HasRedeploy}} && !redeployRequested{{end}} {
|
||||||
|
// Сервисные параметры не изменились → modify НЕ вызываем.
|
||||||
|
//
|
||||||
|
// НО read-back поля (state_params*, state_out*, vault_*) обязаны быть
|
||||||
|
// перечитаны из инстанса. Раньше здесь слепо копировались значения из
|
||||||
|
// старого state, из-за чего tfstate хранил устаревшее значение:
|
||||||
|
// пример — state_params["needEnableAVI"]="false", когда на платформе уже
|
||||||
|
// true (модификатор включил ALB). Это давало ложный дрейф
|
||||||
|
// «Objects have changed outside of Terraform» на каждом plan.
|
||||||
|
plan.ID = instanceID
|
||||||
|
refreshed, refreshDiags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), {{.ServiceID}}, plan, []resources_core.StateField{
|
||||||
|
{{- range .OutputParams }}
|
||||||
|
{Code: "{{.Code}}"},
|
||||||
|
{{- end }}
|
||||||
|
}, []resources_core.InputField{
|
||||||
|
{{- range .SchemaParams }}
|
||||||
|
{{- if eq .RefSvcId 0 }}
|
||||||
|
{Code: "{{.Code}}", Field: "{{ToCamel .Code}}", Type: "{{.Type}}"},
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
})
|
||||||
|
resp.Diagnostics.Append(refreshDiags...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &refreshed)...)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
operationTimeout := ""
|
||||||
|
if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() {
|
||||||
|
operationTimeout = plan.OperationTimeout.ValueString()
|
||||||
|
}
|
||||||
|
if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() {
|
||||||
|
ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString())
|
||||||
|
}
|
||||||
|
|
||||||
|
{{- if .HasRedeploy }}
|
||||||
|
// --- Redeploy support (ARCHITECTURE.md) ---
|
||||||
|
// 1. Modify — только если изменились параметры сервиса
|
||||||
|
if hasServiceParamChanges {
|
||||||
|
{{- end }}
|
||||||
|
params := map[int]string{
|
||||||
|
{{- range .ModifyParams }}
|
||||||
|
{{- if not (IsNested .) }}
|
||||||
|
{{.ID}}: {{ParamFormat . (printf "plan.%s" (ToCamel .Code))}},
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
}
|
||||||
|
{{- range .ModifyParams }}
|
||||||
|
{{- if (IsNested .) }}
|
||||||
|
if plan.{{ToCamel .Code}} != nil {
|
||||||
|
params[{{.ID}}] = {{NestedJSONExpr . "plan"}}
|
||||||
|
}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
if err := resources_core.UpdateResourceWithTimeout(ctx, r.client, instanceID.ValueString(), params, operationTimeout); err != nil {
|
||||||
|
resp.Diagnostics.AddError("Ошибка клиента", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
{{- if .HasRedeploy }}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Redeploy — только если изменился git_revision
|
||||||
|
if redeployRequested {
|
||||||
|
redeployParams := map[int]string{}
|
||||||
|
{{- range .RedeployParams }}
|
||||||
|
{{- if (IsNested .) }}
|
||||||
|
if plan.{{ToCamel .Code}} != nil {
|
||||||
|
redeployParams[{{.ID}}] = {{NestedJSONExpr . "plan"}}
|
||||||
|
}
|
||||||
|
{{- else }}
|
||||||
|
redeployParams[{{.ID}}] = {{ParamFormat . (printf "plan.%s" (ToCamel .Code))}}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
if err := r.client.RunRedeployOperation(ctx, instanceID.ValueString(), operationTimeout, redeployParams); err != nil {
|
||||||
|
resp.Diagnostics.AddError("Ошибка клиента", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
plan.ID = instanceID
|
||||||
|
state, diags := resources_core.RefreshResourceState(ctx, r.client, instanceID.ValueString(), {{.ServiceID}}, plan, []resources_core.StateField{
|
||||||
|
{{- range .OutputParams }}
|
||||||
|
{Code: "{{.Code}}"},
|
||||||
|
{{- end }}
|
||||||
|
}, []resources_core.InputField{
|
||||||
|
{{- range .SchemaParams }}
|
||||||
|
{{- if eq .RefSvcId 0 }}
|
||||||
|
{Code: "{{.Code}}", Field: "{{ToCamel .Code}}", Type: "{{.Type}}"},
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
})
|
||||||
|
resp.Diagnostics.Append(diags...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *{{ToCamel .Name}}Resource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
|
||||||
|
var state {{ToCamel .Name}}Model
|
||||||
|
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if state.ID.IsNull() || state.ID.IsUnknown() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
{{- if .SupportsSuspendDestroy }}
|
||||||
|
deleteMode := "state_only"
|
||||||
|
if !state.SuspendOnDestroy.IsNull() && !state.SuspendOnDestroy.IsUnknown() && state.SuspendOnDestroy.ValueBool() {
|
||||||
|
deleteMode = "suspend"
|
||||||
|
}
|
||||||
|
{{- else }}
|
||||||
|
deleteMode := "delete"
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
operationTimeout := ""
|
||||||
|
if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() {
|
||||||
|
operationTimeout = state.OperationTimeout.ValueString()
|
||||||
|
}
|
||||||
|
if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() {
|
||||||
|
ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString())
|
||||||
|
}
|
||||||
|
if err := resources_core.DeleteResourceWithTimeout(ctx, r.client, state.ID.ValueString(), deleteMode, operationTimeout); err != nil {
|
||||||
|
resp.Diagnostics.AddError("Ошибка клиента", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *{{ToCamel .Name}}Resource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
|
||||||
|
resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *{{ToCamel .Name}}Resource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
|
||||||
|
if req.ProviderData == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
client, ok := req.ProviderData.(*core.UniversalClient)
|
||||||
|
if !ok {
|
||||||
|
resp.Diagnostics.AddError("Error", "Invalid client type")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r.client = client
|
||||||
|
}
|
||||||
|
`
|
||||||
@@ -0,0 +1,473 @@
|
|||||||
|
// Package loader — загрузка YAML-спеков и построение GenResource/GenSubresource/GenAction.
|
||||||
|
//
|
||||||
|
// LoadSpecs — главная функция. Для каждого YAML:
|
||||||
|
// 1. Парсит операции (create/modify/delete/suspend/resume/...)
|
||||||
|
// 2. Классифицирует их на instance/subresource/action
|
||||||
|
// 3. Сливает параметры, вычисляет CreateOnly и ForceNew
|
||||||
|
// 4. Строит GenResource/GenSubresource/GenAction
|
||||||
|
// 5. Сортирует результат по имени сервиса
|
||||||
|
//
|
||||||
|
// ValidateSpec — fail-fast валидация (паника при неизвестном Kind).
|
||||||
|
package loader
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
|
||||||
|
"resource-generator/internal/params"
|
||||||
|
"resource-generator/internal/types"
|
||||||
|
"tf-tools/lib"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LoadSpecs загружает все YAML-спеки из директории и строит модели всех ресурсов.
|
||||||
|
func LoadSpecs(dir string) ([]types.GenResource, []types.GenSubresource, []types.GenAction, []types.GenModifier, error) {
|
||||||
|
var services []types.GenResource
|
||||||
|
var subs []types.GenSubresource
|
||||||
|
var actions []types.GenAction
|
||||||
|
var modifiers []types.GenModifier
|
||||||
|
domainServiceIDsSet := map[int]struct{}{}
|
||||||
|
walkErr := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if d.IsDir() || !strings.HasSuffix(d.Name(), ".yaml") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var spec types.ServiceSpec
|
||||||
|
if err := yaml.Unmarshal(b, &spec); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// P1.4: fail-fast на неизвестных kind'ах и отсутствующих обязательных полях.
|
||||||
|
if err := ValidateSpec(path, &spec); err != nil {
|
||||||
|
return fmt.Errorf("%s: %w", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
createParams := []types.Param{}
|
||||||
|
modifyParams := []types.Param{}
|
||||||
|
supportsSuspendDestroy := false
|
||||||
|
for _, op := range spec.Operations {
|
||||||
|
if op.Kind == "modifier" {
|
||||||
|
name := strings.TrimSpace(op.Modifier)
|
||||||
|
if name == "" {
|
||||||
|
name = strings.TrimSpace(op.Action)
|
||||||
|
}
|
||||||
|
modifier := types.GenModifier{
|
||||||
|
ServiceName: spec.Name,
|
||||||
|
ServiceID: spec.ServiceID,
|
||||||
|
ModifierName: name,
|
||||||
|
OperationName: op.Action,
|
||||||
|
Params: ConvertParams(op.Params),
|
||||||
|
DeleteStrategy: normalizeDeleteStrategy(op.DeleteStrategy),
|
||||||
|
Idempotency: normalizeIdempotency(op.Idempotency),
|
||||||
|
}
|
||||||
|
modifier.DeleteParams = convertDeleteParams(op.DeleteParams)
|
||||||
|
modifier.SchemaParams = modifier.Params
|
||||||
|
for idx := range modifier.SchemaParams {
|
||||||
|
if modifier.SchemaParams[idx].HasSubParams {
|
||||||
|
modifier.SchemaParams[idx].IsJson = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
params.Analyze(&modifier.UsesBool, &modifier.UsesInt64, &modifier.UsesString, &modifier.HasDefaults, &modifier.NeedsBoolDefault, &modifier.NeedsInt64Default, &modifier.NeedsStringDefault, modifier.SchemaParams)
|
||||||
|
modifier.NeedsJsonPlanMod = params.AnalyzeJsonPlanMod(modifier.SchemaParams)
|
||||||
|
modifiers = append(modifiers, modifier)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if op.Kind != "instance" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch op.Action {
|
||||||
|
case "create":
|
||||||
|
createParams = ConvertParams(op.Params)
|
||||||
|
case "modify":
|
||||||
|
modifyParams = ConvertParams(op.Params)
|
||||||
|
case "suspend":
|
||||||
|
supportsSuspendDestroy = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
schemaParams := params.Merge(createParams, modifyParams)
|
||||||
|
createParams = params.AlignParamTypes(createParams, schemaParams)
|
||||||
|
modifyParams = params.AlignParamTypes(modifyParams, schemaParams)
|
||||||
|
createOnly := params.ComputeCreateOnly(createParams, modifyParams)
|
||||||
|
schemaParams = params.MarkCreateOnly(schemaParams, createOnly)
|
||||||
|
hasDomainParam := false
|
||||||
|
for _, param := range schemaParams {
|
||||||
|
if strings.EqualFold(strings.TrimSpace(param.Code), "domain") {
|
||||||
|
hasDomainParam = true
|
||||||
|
domainServiceIDsSet[spec.ServiceID] = struct{}{}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
adoptExistingOnCreate := false
|
||||||
|
|
||||||
|
suspendOnDestroy := true
|
||||||
|
if spec.Lifecycle.SuspendOnDestroyDefault != nil {
|
||||||
|
suspendOnDestroy = *spec.Lifecycle.SuspendOnDestroyDefault
|
||||||
|
}
|
||||||
|
|
||||||
|
gr := types.GenResource{
|
||||||
|
Name: spec.Name,
|
||||||
|
ServiceID: spec.ServiceID,
|
||||||
|
CreateParams: createParams,
|
||||||
|
ModifyParams: modifyParams,
|
||||||
|
SchemaParams: schemaParams,
|
||||||
|
CreateOnlyParams: params.FilterCreateOnly(schemaParams),
|
||||||
|
CreateOnlyRequiredParams: params.FilterCreateOnlyRequired(schemaParams),
|
||||||
|
OutputParams: NormalizeOutputParams(spec.Outputs.Params),
|
||||||
|
HasRefSvcParams: HasRefSvcParams(schemaParams),
|
||||||
|
SupportsSuspendDestroy: supportsSuspendDestroy,
|
||||||
|
SuspendOnDestroy: suspendOnDestroy,
|
||||||
|
AdoptExistingOnCreate: adoptExistingOnCreate,
|
||||||
|
HasDomainParam: hasDomainParam,
|
||||||
|
}
|
||||||
|
params.Analyze(&gr.UsesBool, &gr.UsesInt64, &gr.UsesString, &gr.HasDefaults, &gr.NeedsBoolDefault, &gr.NeedsInt64Default, &gr.NeedsStringDefault, gr.SchemaParams)
|
||||||
|
gr.NeedsBoolUseStateForUnknown, gr.NeedsInt64UseStateForUnknown = analyzeUseStateForUnknown(gr.SchemaParams)
|
||||||
|
// Анализируем nested sub-params для default-импортов
|
||||||
|
if params.AnalyzeNestedDefaults(&gr.NeedsBoolDefault, &gr.NeedsInt64Default, &gr.NeedsStringDefault, gr.SchemaParams) {
|
||||||
|
gr.NeedsFmtImport = true
|
||||||
|
}
|
||||||
|
gr.NeedsJsonPlanMod = params.AnalyzeJsonPlanMod(gr.SchemaParams)
|
||||||
|
// Строковый import нужен только для обычных строковых сравнений и
|
||||||
|
// обработок в шаблоне. Старый restore-хук для refSvc больше не
|
||||||
|
// генерируется, поэтому отдельный флаг под него не нужен.
|
||||||
|
gr.NeedsStringsImport = params.AnalyzeNeedsStrings(gr.ModifyParams) || params.AnalyzeNeedsStrings(gr.CreateOnlyRequiredParams) || params.AnalyzeNeedsStrings(gr.CreateOnlyParams)
|
||||||
|
gr.NeedsFmtImport = params.HasNestedParams(gr.SchemaParams)
|
||||||
|
|
||||||
|
// --- Action processing (ДО append, чтобы HasRedeploy попал в слайс) ---
|
||||||
|
for _, op := range spec.Operations {
|
||||||
|
if op.Kind != "action" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch op.Action {
|
||||||
|
case "redeploy":
|
||||||
|
gr.HasRedeploy = true
|
||||||
|
gr.RedeployParams = ConvertParams(op.Params)
|
||||||
|
case "restart", "recovery", "reconcile":
|
||||||
|
// Исключены
|
||||||
|
default:
|
||||||
|
act := types.GenAction{
|
||||||
|
ServiceName: spec.Name,
|
||||||
|
ServiceID: spec.ServiceID,
|
||||||
|
ActionName: op.Action,
|
||||||
|
OperationName: op.Name,
|
||||||
|
Params: ConvertParams(op.Params),
|
||||||
|
}
|
||||||
|
act.SchemaParams = act.Params
|
||||||
|
params.Analyze(&act.UsesBool, &act.UsesInt64, &act.UsesString, &act.HasDefaults, &act.NeedsBoolDefault, &act.NeedsInt64Default, &act.NeedsStringDefault, act.SchemaParams)
|
||||||
|
act.NeedsJsonPlanMod = params.AnalyzeJsonPlanMod(act.SchemaParams)
|
||||||
|
act.NeedsStringsImport = params.AnalyzeNeedsStrings(act.SchemaParams)
|
||||||
|
actions = append(actions, act)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
services = append(services, gr)
|
||||||
|
|
||||||
|
srByName := map[string]*types.GenSubresource{}
|
||||||
|
for _, op := range spec.Operations {
|
||||||
|
if op.Kind != "subresource" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
name := strings.TrimSpace(op.Subresource)
|
||||||
|
if name == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
sr := srByName[name]
|
||||||
|
if sr == nil {
|
||||||
|
sr = &types.GenSubresource{ServiceName: spec.Name, ServiceID: spec.ServiceID, SubName: name}
|
||||||
|
srByName[name] = sr
|
||||||
|
}
|
||||||
|
switch op.Action {
|
||||||
|
case "create":
|
||||||
|
sr.CreateOpName = op.Name
|
||||||
|
sr.CreateParams = ConvertParams(op.Params)
|
||||||
|
case "modify":
|
||||||
|
sr.ModifyOpName = op.Name
|
||||||
|
sr.ModifyParams = ConvertParams(op.Params)
|
||||||
|
case "delete":
|
||||||
|
sr.DeleteOpName = op.Name
|
||||||
|
sr.DeleteParams = ConvertParams(op.Params)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, sr := range srByName {
|
||||||
|
sr.SchemaParams = params.Merge(sr.CreateParams, sr.ModifyParams, sr.DeleteParams)
|
||||||
|
sr.CreateParams = params.AlignParamTypes(sr.CreateParams, sr.SchemaParams)
|
||||||
|
sr.ModifyParams = params.AlignParamTypes(sr.ModifyParams, sr.SchemaParams)
|
||||||
|
sr.DeleteParams = params.AlignParamTypes(sr.DeleteParams, sr.SchemaParams)
|
||||||
|
createOnly := params.ComputeCreateOnly(sr.CreateParams, sr.ModifyParams)
|
||||||
|
sr.SchemaParams = params.MarkCreateOnly(sr.SchemaParams, createOnly)
|
||||||
|
forceNewCodes := params.BuildSubresourceForceNewCodes(sr, createOnly)
|
||||||
|
sr.SchemaParams = params.MarkForceNew(sr.SchemaParams, forceNewCodes)
|
||||||
|
sr.IdentityParams = params.FilterForceNew(sr.SchemaParams)
|
||||||
|
if len(sr.IdentityParams) == 0 {
|
||||||
|
sr.IdentityParams = sr.SchemaParams
|
||||||
|
}
|
||||||
|
sr.HasRefSvcParams = HasRefSvcParams(sr.SchemaParams)
|
||||||
|
params.Analyze(&sr.UsesBool, &sr.UsesInt64, &sr.UsesString, &sr.HasDefaults, &sr.NeedsBoolDefault, &sr.NeedsInt64Default, &sr.NeedsStringDefault, sr.SchemaParams)
|
||||||
|
params.AnalyzePlanModifiers(&sr.NeedsBoolPlanMod, &sr.NeedsInt64PlanMod, &sr.NeedsStringPlanMod, sr.SchemaParams)
|
||||||
|
sr.NeedsJsonPlanMod = params.AnalyzeJsonPlanMod(sr.SchemaParams)
|
||||||
|
sr.NeedsStringsImport = params.AnalyzeNeedsStrings(sr.ModifyParams)
|
||||||
|
subs = append(subs, *sr)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if walkErr != nil {
|
||||||
|
return nil, nil, nil, nil, walkErr
|
||||||
|
}
|
||||||
|
|
||||||
|
domainServiceIDs := make([]int, 0, len(domainServiceIDsSet))
|
||||||
|
for serviceID := range domainServiceIDsSet {
|
||||||
|
domainServiceIDs = append(domainServiceIDs, serviceID)
|
||||||
|
}
|
||||||
|
sort.Ints(domainServiceIDs)
|
||||||
|
for idx := range services {
|
||||||
|
if len(domainServiceIDs) == 0 {
|
||||||
|
services[idx].DomainServiceIDs = nil
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
services[idx].DomainServiceIDs = append([]int(nil), domainServiceIDs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(services, func(i, j int) bool { return services[i].Name < services[j].Name })
|
||||||
|
sort.Slice(subs, func(i, j int) bool {
|
||||||
|
if subs[i].ServiceName == subs[j].ServiceName {
|
||||||
|
return subs[i].SubName < subs[j].SubName
|
||||||
|
}
|
||||||
|
return subs[i].ServiceName < subs[j].ServiceName
|
||||||
|
})
|
||||||
|
sort.Slice(actions, func(i, j int) bool {
|
||||||
|
if actions[i].ServiceName == actions[j].ServiceName {
|
||||||
|
return actions[i].ActionName < actions[j].ActionName
|
||||||
|
}
|
||||||
|
return actions[i].ServiceName < actions[j].ServiceName
|
||||||
|
})
|
||||||
|
sort.Slice(modifiers, func(i, j int) bool {
|
||||||
|
if modifiers[i].ServiceName == modifiers[j].ServiceName {
|
||||||
|
return modifiers[i].ModifierName < modifiers[j].ModifierName
|
||||||
|
}
|
||||||
|
return modifiers[i].ServiceName < modifiers[j].ServiceName
|
||||||
|
})
|
||||||
|
|
||||||
|
return services, subs, actions, modifiers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConvertParams конвертирует []ParamSpec → []Param с нормализацией типов.
|
||||||
|
func ConvertParams(params []types.ParamSpec) []types.Param {
|
||||||
|
out := make([]types.Param, 0, len(params))
|
||||||
|
for _, p := range params {
|
||||||
|
typeName := NormalizeParamType(p.DataType)
|
||||||
|
defVal := NormalizeDefault(p.Default)
|
||||||
|
ref := 0
|
||||||
|
if p.RefSvcID != nil {
|
||||||
|
ref = *p.RefSvcID
|
||||||
|
}
|
||||||
|
isNested := len(p.SubParams) > 0
|
||||||
|
// Рекурсивно конвертируем SubParams для map-fixed/array-map-fixed.
|
||||||
|
subParams := make([]types.Param, 0, len(p.SubParams))
|
||||||
|
for _, sp := range p.SubParams {
|
||||||
|
sub := ConvertParams([]types.ParamSpec{sp})
|
||||||
|
if len(sub) > 0 {
|
||||||
|
subParams = append(subParams, sub[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out = append(out, types.Param{
|
||||||
|
ID: p.ID,
|
||||||
|
Code: p.Code,
|
||||||
|
Type: typeName,
|
||||||
|
Required: p.Required,
|
||||||
|
Default: defVal,
|
||||||
|
RefSvcId: ref,
|
||||||
|
Descr: strings.TrimSpace(p.Descr),
|
||||||
|
Man: strings.TrimSpace(p.Man),
|
||||||
|
Sensitive: p.IsSensitive,
|
||||||
|
IsJson: strings.EqualFold(strings.TrimSpace(p.DataType), "json"),
|
||||||
|
HasSubParams: isNested,
|
||||||
|
SubParams: subParams,
|
||||||
|
IsModifiable: p.IsModifiable,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// NormalizeParamType нормализует строку data_type → bool | int64 | string | map-fixed | array-map-fixed.
|
||||||
|
func NormalizeParamType(value string) string {
|
||||||
|
v := strings.ToLower(strings.TrimSpace(value))
|
||||||
|
if v == "map-fixed" || v == "array-map-fixed" {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
if strings.Contains(v, "bool") {
|
||||||
|
return "bool"
|
||||||
|
}
|
||||||
|
if strings.Contains(v, "int") || strings.Contains(v, "number") {
|
||||||
|
return "int64"
|
||||||
|
}
|
||||||
|
return "string"
|
||||||
|
}
|
||||||
|
|
||||||
|
// NormalizeDefault нормализует значение по умолчанию в строку.
|
||||||
|
func NormalizeDefault(value interface{}) string {
|
||||||
|
if value == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
switch t := value.(type) {
|
||||||
|
case string:
|
||||||
|
return strings.TrimSpace(t)
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("%v", t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NormalizeOutputParams нормализует выходные параметры.
|
||||||
|
func NormalizeOutputParams(params []types.OutputParam) []types.OutputParam {
|
||||||
|
if len(params) == 0 {
|
||||||
|
return []types.OutputParam{}
|
||||||
|
}
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasRefSvcParams проверяет наличие ref_svc-параметров.
|
||||||
|
func HasRefSvcParams(params []types.Param) bool {
|
||||||
|
for _, p := range params {
|
||||||
|
if p.RefSvcId > 0 && strings.EqualFold(strings.TrimSpace(p.Type), "string") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// KnownKinds — допустимые значения Kind операций.
|
||||||
|
var KnownKinds = map[string]bool{
|
||||||
|
"instance": true,
|
||||||
|
"subresource": true,
|
||||||
|
"action": true,
|
||||||
|
"modifier": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateSpec проверяет YAML-спек на обязательные поля и неизвестные kinds.
|
||||||
|
// P1.4: fail-fast — паника при неизвестном kind вместо тихого игнорирования.
|
||||||
|
func ValidateSpec(path string, spec *types.ServiceSpec) error {
|
||||||
|
if spec.Name == "" {
|
||||||
|
return fmt.Errorf("missing required field: name")
|
||||||
|
}
|
||||||
|
if spec.ServiceID <= 0 {
|
||||||
|
return fmt.Errorf("missing required field: service_id")
|
||||||
|
}
|
||||||
|
for i, op := range spec.Operations {
|
||||||
|
if op.Kind == "" {
|
||||||
|
return fmt.Errorf("operation[%d] %q: missing required field: kind", i, op.Name)
|
||||||
|
}
|
||||||
|
if !KnownKinds[op.Kind] {
|
||||||
|
return fmt.Errorf("operation[%d] %q: unknown kind %q (valid: instance, subresource, action, modifier)", i, op.Name, op.Kind)
|
||||||
|
}
|
||||||
|
if op.Action == "" {
|
||||||
|
return fmt.Errorf("operation[%d] %q (kind=%s): missing required field: action", i, op.Name, op.Kind)
|
||||||
|
}
|
||||||
|
if op.Kind == "subresource" && op.Subresource == "" {
|
||||||
|
return fmt.Errorf("operation[%d] %q (kind=subresource): missing required field: subresource", i, op.Name)
|
||||||
|
}
|
||||||
|
if op.Kind == "modifier" && strings.TrimSpace(op.Action) == "" {
|
||||||
|
return fmt.Errorf("operation[%d] %q (kind=modifier): missing required field: action", i, op.Name)
|
||||||
|
}
|
||||||
|
if op.Kind == "modifier" {
|
||||||
|
if err := validateModifierOperation(i, &op); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// normalizeDeleteStrategy возвращает каноническое значение delete_strategy.
|
||||||
|
// Пусто → noop_warn; неизвестное — как есть (валидация в ValidateSpec отклонит раньше).
|
||||||
|
func normalizeDeleteStrategy(raw string) string {
|
||||||
|
v := strings.ToLower(strings.TrimSpace(raw))
|
||||||
|
if v == "" {
|
||||||
|
return "noop_warn"
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// normalizeIdempotency возвращает каноническое значение idempotency.
|
||||||
|
// Пусто → none.
|
||||||
|
func normalizeIdempotency(raw string) string {
|
||||||
|
v := strings.ToLower(strings.TrimSpace(raw))
|
||||||
|
if v == "" {
|
||||||
|
return "none"
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// convertDeleteParams переносит lib.DeleteParam → types.DeleteParam (wire-строки).
|
||||||
|
func convertDeleteParams(raw []lib.DeleteParam) []types.DeleteParam {
|
||||||
|
if len(raw) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := make([]types.DeleteParam, 0, len(raw))
|
||||||
|
for _, p := range raw {
|
||||||
|
out = append(out, types.DeleteParam{Code: strings.TrimSpace(p.Code), Value: p.Value})
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateModifierOperation — fail-fast для полей modifier-операции.
|
||||||
|
func validateModifierOperation(i int, op *lib.OperationSpec) error {
|
||||||
|
ds := normalizeDeleteStrategy(op.DeleteStrategy)
|
||||||
|
switch ds {
|
||||||
|
case "noop_warn", "inverse", "error":
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("operation[%d] %q (kind=modifier): unknown delete_strategy %q (valid: noop_warn, inverse, error)", i, op.Name, op.DeleteStrategy)
|
||||||
|
}
|
||||||
|
|
||||||
|
idem := normalizeIdempotency(op.Idempotency)
|
||||||
|
switch idem {
|
||||||
|
case "none", "check_before_run":
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("operation[%d] %q (kind=modifier): unknown idempotency %q (valid: none, check_before_run)", i, op.Name, op.Idempotency)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ds == "inverse" && len(op.DeleteParams) == 0 {
|
||||||
|
return fmt.Errorf("operation[%d] %q (kind=modifier): delete_strategy=inverse требует delete_params", i, op.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// каждый delete_params.code обязан существовать среди params (по lower-code)
|
||||||
|
paramCodes := make(map[string]struct{}, len(op.Params))
|
||||||
|
for _, p := range op.Params {
|
||||||
|
paramCodes[strings.ToLower(strings.TrimSpace(p.Code))] = struct{}{}
|
||||||
|
}
|
||||||
|
for _, dp := range op.DeleteParams {
|
||||||
|
if _, ok := paramCodes[strings.ToLower(strings.TrimSpace(dp.Code))]; !ok {
|
||||||
|
return fmt.Errorf("operation[%d] %q (kind=modifier): delete_params.code %q отсутствует в params", i, op.Name, dp.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// analyzeUseStateForUnknown определяет, нужны ли импорты boolplanmodifier/int64planmodifier:
|
||||||
|
// есть ли среди SchemaParams скалярный Optional+Computed (без Default) параметр
|
||||||
|
// соответствующего типа, для которого генерируется UseStateForUnknown().
|
||||||
|
func analyzeUseStateForUnknown(schemaParams []types.Param) (needsBool, needsInt64 bool) {
|
||||||
|
for _, p := range schemaParams {
|
||||||
|
if p.IsJson || p.Required || p.RefSvcId != 0 || p.Default != "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch strings.ToLower(p.Type) {
|
||||||
|
case "bool":
|
||||||
|
needsBool = true
|
||||||
|
case "int", "int64", "number":
|
||||||
|
needsInt64 = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return needsBool, needsInt64
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
# =============================================================================
|
||||||
|
# Ресурсы-модификаторы (операции modify, которых нет в create-схеме ресурсов)
|
||||||
|
#
|
||||||
|
# Порядок строго такой:
|
||||||
|
# орга (создана вручную в ЛК)
|
||||||
|
# -> nubes_vc_vdc.vdc
|
||||||
|
# -> nubes_vc_nsxt.edge
|
||||||
|
# -> nubes_vc_org_ip_allocation (выделение внешних IP на орге)
|
||||||
|
# -> nubes_vc_nsxt_snat (SNAT на эдже этим ipSpace)
|
||||||
|
#
|
||||||
|
# Почему аллокация ПОСЛЕ эджа: платформа строит список ipSpace из состояния
|
||||||
|
# `job.vcd.networkProvider` / `job.vcd.providerGateway`, то есть требует уже
|
||||||
|
# созданный vDC и Edge. Иначе modify на орге падает
|
||||||
|
# («Can't cast Complex Object Type Struct to String»).
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# 1. Внешние IP на организации (modify: vIPConfigure, массив перезаписывается целиком)
|
||||||
|
resource "nubes_vc_org_ip_allocation" "org_ip" {
|
||||||
|
organization = var.organization
|
||||||
|
|
||||||
|
vip_configure = jsonencode([
|
||||||
|
{
|
||||||
|
name = var.ip_space_name
|
||||||
|
count = var.ip_count
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
# false = при destroy отправить обратный modify с count=0 (квота обнулится)
|
||||||
|
keep_on_destroy = false
|
||||||
|
|
||||||
|
depends_on = [nubes_vc_nsxt.edge]
|
||||||
|
}
|
||||||
|
|
||||||
|
# 2. SNAT на эдже (modify: ipSpaceName)
|
||||||
|
resource "nubes_vc_nsxt_snat" "snat" {
|
||||||
|
nsxt_uid = nubes_vc_nsxt.edge.id
|
||||||
|
ip_space_name = var.ip_space_name
|
||||||
|
|
||||||
|
keep_on_destroy = false
|
||||||
|
|
||||||
|
# ipSpace должен быть уже выделен на организации
|
||||||
|
depends_on = [nubes_vc_org_ip_allocation.org_ip]
|
||||||
|
}
|
||||||
|
|
||||||
|
output "allocated_org_ip" {
|
||||||
|
description = "Выделено внешних IP на организации"
|
||||||
|
value = {
|
||||||
|
organization = var.organization
|
||||||
|
ip_space_name = var.ip_space_name
|
||||||
|
ip_count = var.ip_count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
output "snat_ip_space" {
|
||||||
|
description = "ipSpace, включённый как SNAT на эдже"
|
||||||
|
value = nubes_vc_nsxt_snat.snat.ip_space_name
|
||||||
|
}
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
# =============================================================================
|
||||||
|
# Kubernetes кластер Штурвал — сервис 150, ресурс nubes_k8s_sthutrval_cluster
|
||||||
|
# (НЕ 148 «Менеджмент Kubernetes кластер Штурвал» — это другой сервис)
|
||||||
|
#
|
||||||
|
# Всё, что относится к Штурвалу, лежит ТОЛЬКО в этом файле: переменные, их
|
||||||
|
# значения по умолчанию и сам ресурс. Чтобы выключить Штурвал — удалить файл
|
||||||
|
# или закомментировать ресурс.
|
||||||
|
#
|
||||||
|
# Порядок (чек-лист из инструкции на услугу в ЛК):
|
||||||
|
# 1) Организация в Cloud Director — создана вручную в ЛК
|
||||||
|
# 2) nubes_vc_vdc.vdc — есть
|
||||||
|
# 3) nubes_vc_nsxt.edge — есть, обязательно ALB + AVI VS >= 3
|
||||||
|
# 4) внешние адреса в организации — суммарно >= 3 (nubes_vc_org_ip_allocation)
|
||||||
|
# 5) SNAT на Edge — nubes_vc_nsxt_snat
|
||||||
|
# 6) Kubernetes кластер Штурвал — этот ресурс
|
||||||
|
#
|
||||||
|
# Минимальные требования к кластеру: мастер-нод >= 1, воркер-нод >= 1,
|
||||||
|
# 4 vCPU / 8 GB RAM / 50 GB диска на ноду.
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# --- Переменные Штурвала ---
|
||||||
|
|
||||||
|
variable "shturval_resource_name" {
|
||||||
|
type = string
|
||||||
|
default = "shturval-dev"
|
||||||
|
description = "Имя услуги «Kubernetes кластер Штурвал» в ЛК"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "shturval_cluster_name" {
|
||||||
|
type = string
|
||||||
|
default = "shturval-dev-00"
|
||||||
|
description = "Имя кластера внутри Штурвала"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "shturval_app_version" {
|
||||||
|
type = string
|
||||||
|
default = "2.14.0"
|
||||||
|
description = "Версия Штурвала (значение по умолчанию платформы — 2.14.0)"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "shturval_cp_sizing_policy" {
|
||||||
|
type = string
|
||||||
|
default = "TKG 4CPU 8RAM"
|
||||||
|
description = "Политика размера control plane: 4 vCPU / 8 GB (минимум по инструкции). Должна существовать в ресурсной платформе vDC — список политик берётся из услуги «Виртуальный датацентр»"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "shturval_cp_sizing_disk" {
|
||||||
|
type = number
|
||||||
|
default = 50
|
||||||
|
description = "Диск control plane, ГБ (минимум 50)"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "shturval_cp_count" {
|
||||||
|
type = number
|
||||||
|
default = 1
|
||||||
|
description = "Количество мастер-нод: 1, 3 или 5"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "shturval_worker_group_name" {
|
||||||
|
type = string
|
||||||
|
default = "workers-shturval-dev"
|
||||||
|
description = "Имя группы воркеров (уникальное в кластере; допустимы строчные латинские буквы, цифры и дефис)"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "shturval_worker_sizing_policy" {
|
||||||
|
type = string
|
||||||
|
default = "TKG 4CPU 8RAM"
|
||||||
|
description = "Политика размера воркеров: 4 vCPU / 8 GB (минимум по инструкции)"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "shturval_worker_sizing_disk" {
|
||||||
|
type = number
|
||||||
|
default = 50
|
||||||
|
description = "Диск воркеров, ГБ (минимум 50)"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "shturval_worker_count" {
|
||||||
|
type = number
|
||||||
|
default = 1
|
||||||
|
description = "Количество воркер-нод (минимум 1)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Значения, которые собираются из переменных ---
|
||||||
|
|
||||||
|
locals {
|
||||||
|
# Группы воркеров передаются JSON-строкой ВНУТРЬ услуги как есть, поэтому ключи
|
||||||
|
# должны быть ровно такими, как в манифесте услуги 150: groupName, sizingPolicy,
|
||||||
|
# sizingDisk, count, autoscale, labelDeck.
|
||||||
|
# ВНИМАНИЕ: в сгенерированном примере провайдера (docs → Example) ключи показаны
|
||||||
|
# в snake_case — это ошибка генератора, платформа на них падает с
|
||||||
|
# «Cannot invoke method split() on null object» (не находит groupName → null).
|
||||||
|
shturval_worker_config = jsonencode([
|
||||||
|
{
|
||||||
|
groupName = var.shturval_worker_group_name
|
||||||
|
sizingPolicy = var.shturval_worker_sizing_policy
|
||||||
|
sizingDisk = var.shturval_worker_sizing_disk
|
||||||
|
count = var.shturval_worker_count
|
||||||
|
autoscale = false # автоскейл выключен
|
||||||
|
labelDeck = true # разрешить разворачивать услуги из ЛК на этих нодах
|
||||||
|
}
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Ресурс Штурвала ---
|
||||||
|
|
||||||
|
resource "nubes_k8s_sthutrval_cluster" "shturval" {
|
||||||
|
resource_name = var.shturval_resource_name
|
||||||
|
|
||||||
|
# Кластер Штурвала уже существует (инстанс «shturval-dev») и в проде не
|
||||||
|
# удаляется неделями, поэтому ресурс должен УСЫНОВИТЬ существующий инстанс,
|
||||||
|
# а не падать с «РЕСУРС С ТАКИМ ИМЕНЕМ УЖЕ СУЩЕСТВУЕТ (SUSPEND)».
|
||||||
|
# Проверка/adopt выполняются в Create на apply (в plan будет «will be created»).
|
||||||
|
adopt_existing_on_create = true
|
||||||
|
|
||||||
|
# Штурвал создаётся долго (десятки минут) — поднимаем таймаут ожидания,
|
||||||
|
# иначе провайдер сдаётся на дефолтных 600 с.
|
||||||
|
operation_timeout = "60m"
|
||||||
|
|
||||||
|
startup_configuration = {
|
||||||
|
# vDC и Edge из этого же конфига (обязательные поля)
|
||||||
|
vdc_uid = nubes_vc_vdc.vdc.id
|
||||||
|
nsxt_uid = nubes_vc_nsxt.edge.id
|
||||||
|
|
||||||
|
cluster_name = var.shturval_cluster_name
|
||||||
|
|
||||||
|
# Дополнительные возможности кластера (в ЛК — галочки при создании)
|
||||||
|
ex_logging = true # логи в Loki (без него логи услуг не видны в ЛК)
|
||||||
|
ex_monitoring = true # метрики в VictoriaMetrics (без него метрик в ЛК нет)
|
||||||
|
ex_local_csi = true
|
||||||
|
ex_vip = true
|
||||||
|
ex_update = true
|
||||||
|
ex_ingress = true
|
||||||
|
ex_named_csi = true
|
||||||
|
}
|
||||||
|
|
||||||
|
cluster_configuration = {
|
||||||
|
app_version = var.shturval_app_version
|
||||||
|
}
|
||||||
|
|
||||||
|
control_plane_configuration = {
|
||||||
|
sizing_policy = var.shturval_cp_sizing_policy
|
||||||
|
sizing_disk = var.shturval_cp_sizing_disk
|
||||||
|
count = var.shturval_cp_count
|
||||||
|
}
|
||||||
|
|
||||||
|
worker_configuration = local.shturval_worker_config
|
||||||
|
|
||||||
|
access_configuration = {
|
||||||
|
need_external_address_api = true # внешний адрес для Kubernetes API (false недопустим)
|
||||||
|
access_ip_list_api = jsonencode([]) # пусто = доступ всем
|
||||||
|
need_external_address_ingress = true # внешний адрес для Ingress
|
||||||
|
access_ip_list_ingress = jsonencode([]) # пусто = доступ всем
|
||||||
|
}
|
||||||
|
|
||||||
|
# Кластер поднимается только после готовой сети: vDC -> Edge -> внешние IP -> SNAT
|
||||||
|
depends_on = [nubes_vc_nsxt_snat.snat]
|
||||||
|
}
|
||||||
@@ -0,0 +1,183 @@
|
|||||||
|
// Package types — структуры данных для генератора Terraform-провайдера.
|
||||||
|
//
|
||||||
|
// Моделирует три вида ресурсов:
|
||||||
|
// - GenResource — основной CRUD инстанса (nubes_{service})
|
||||||
|
// - GenSubresource — подресурсы (nubes_{service}_{user}, ...)
|
||||||
|
// - GenAction — действия (почти не используется, redeploy встроен в GenResource.HasRedeploy)
|
||||||
|
//
|
||||||
|
// Правила обработки action'ов из ARCHITECTURE.md:
|
||||||
|
// - redeploy → HasRedeploy=true, поле git_revision в основном ресурсе
|
||||||
|
// - restart/recovery/reconcile → исключены (ручные, только UI)
|
||||||
|
// - всё остальное → отдельный GenAction
|
||||||
|
package types
|
||||||
|
|
||||||
|
import "tf-tools/lib"
|
||||||
|
|
||||||
|
// ─── Алиасы к lib (общий YAML-контракт) ─────────────────────────────────────
|
||||||
|
|
||||||
|
type OutputParam = lib.OutputParam
|
||||||
|
type OperationSpec = lib.OperationSpec
|
||||||
|
type ParamSpec = lib.ParamSpec
|
||||||
|
|
||||||
|
// ─── Входные структуры (из YAML) — локальное определение ────────────────────
|
||||||
|
|
||||||
|
// ServiceSpec — локальное определение (отличается вложенными типами от lib).
|
||||||
|
// Использует *bool для Lifecycle (совместимость с YAML-парсингом).
|
||||||
|
type ServiceSpec struct {
|
||||||
|
Name string `yaml:"name"` // snake_case имя (postgres, s3bucket, ...)
|
||||||
|
ServiceID int `yaml:"service_id"` // числовой ID сервиса в Nubes
|
||||||
|
Outputs struct {
|
||||||
|
Params []OutputParam `yaml:"params"`
|
||||||
|
} `yaml:"outputs"`
|
||||||
|
Lifecycle struct {
|
||||||
|
SuspendOnDestroyDefault *bool `yaml:"suspend_on_destroy_default"`
|
||||||
|
AdoptExistingOnCreateDefault *bool `yaml:"adopt_existing_on_create_default"`
|
||||||
|
} `yaml:"lifecycle"`
|
||||||
|
Operations []OperationSpec `yaml:"operations"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Param — нормализованный параметр (после конвертации из ParamSpec).
|
||||||
|
type Param struct {
|
||||||
|
ID int
|
||||||
|
Code string
|
||||||
|
Type string
|
||||||
|
Required bool
|
||||||
|
Default string
|
||||||
|
RefSvcId int
|
||||||
|
Descr string
|
||||||
|
Man string
|
||||||
|
Sensitive bool
|
||||||
|
CreateOnly bool
|
||||||
|
ForceNew bool
|
||||||
|
// IsModifiable — флаг is_modifiable из API (можно менять в UI → можно менять в Terraform).
|
||||||
|
// nil означает «не указано» (как правило, изменяемо).
|
||||||
|
IsModifiable *bool
|
||||||
|
// IsJson — атрибут является JSON-строкой (data_type: json в YAML-спеке).
|
||||||
|
// При IsJson=true в схему добавляется JsonNormalize план-модификатор,
|
||||||
|
// чтобы план и API-ответ (компактный JSON) всегда совпадали.
|
||||||
|
IsJson bool
|
||||||
|
// HasSubParams — параметр имеет подполя (map-fixed или array-map-fixed).
|
||||||
|
HasSubParams bool
|
||||||
|
// SubParams — подполя для map-fixed/array-map-fixed параметров.
|
||||||
|
SubParams []Param
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenResource — ресурс инстанса (nubes_{service}).
|
||||||
|
type GenResource struct {
|
||||||
|
Name string
|
||||||
|
ServiceID int
|
||||||
|
CreateParams []Param
|
||||||
|
ModifyParams []Param
|
||||||
|
SchemaParams []Param
|
||||||
|
CreateOnlyParams []Param
|
||||||
|
CreateOnlyRequiredParams []Param
|
||||||
|
OutputParams []OutputParam
|
||||||
|
HasRefSvcParams bool
|
||||||
|
SupportsSuspendDestroy bool
|
||||||
|
SuspendOnDestroy bool
|
||||||
|
AdoptExistingOnCreate bool
|
||||||
|
UsesBool bool
|
||||||
|
UsesInt64 bool
|
||||||
|
UsesString bool
|
||||||
|
HasDefaults bool
|
||||||
|
NeedsBoolDefault bool
|
||||||
|
NeedsInt64Default bool
|
||||||
|
NeedsStringDefault bool
|
||||||
|
NeedsBoolUseStateForUnknown bool
|
||||||
|
NeedsInt64UseStateForUnknown bool
|
||||||
|
HasDomainParam bool
|
||||||
|
DomainServiceIDs []int
|
||||||
|
// HasRedeploy: сервис поддерживает redeploy (пересборка из git).
|
||||||
|
// Если true — в основной ресурс добавляется поле git_revision.
|
||||||
|
// При изменении git_revision вызывается redeploy вместо modify.
|
||||||
|
// Правило из ARCHITECTURE.md: только redeploy включается как inline action.
|
||||||
|
// restart, recovery, reconcile — исключены (ручные операции, только UI).
|
||||||
|
HasRedeploy bool
|
||||||
|
// RedeployParams — параметры redeploy-операции (если есть).
|
||||||
|
RedeployParams []Param
|
||||||
|
// NeedsJsonPlanMod: хотя бы один атрибут имеет data_type: json.
|
||||||
|
// Управляет добавлением импорта planmodifier в сгенерированный файл.
|
||||||
|
NeedsJsonPlanMod bool
|
||||||
|
// NeedsStringsImport: есть строковые ModifyParams (нужен EqualFold в Update).
|
||||||
|
NeedsStringsImport bool
|
||||||
|
// NeedsFmtImport: есть nested (map-fixed) параметры, нужен fmt.Sprintf.
|
||||||
|
NeedsFmtImport bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenSubresource — подресурс (nubes_{service}_{sub}).
|
||||||
|
type GenSubresource struct {
|
||||||
|
ServiceName string
|
||||||
|
ServiceID int
|
||||||
|
SubName string
|
||||||
|
CreateOpName string
|
||||||
|
ModifyOpName string
|
||||||
|
DeleteOpName string
|
||||||
|
CreateParams []Param
|
||||||
|
ModifyParams []Param
|
||||||
|
DeleteParams []Param
|
||||||
|
SchemaParams []Param
|
||||||
|
IdentityParams []Param
|
||||||
|
HasRefSvcParams bool
|
||||||
|
UsesBool bool
|
||||||
|
UsesInt64 bool
|
||||||
|
UsesString bool
|
||||||
|
HasDefaults bool
|
||||||
|
NeedsBoolDefault bool
|
||||||
|
NeedsInt64Default bool
|
||||||
|
NeedsStringDefault bool
|
||||||
|
NeedsBoolPlanMod bool
|
||||||
|
NeedsInt64PlanMod bool
|
||||||
|
NeedsStringPlanMod bool
|
||||||
|
NeedsJsonPlanMod bool
|
||||||
|
NeedsStringsImport bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenAction — action-ресурс (почти не используется, redeploy встроен в GenResource).
|
||||||
|
type GenAction struct {
|
||||||
|
ServiceName string
|
||||||
|
ServiceID int
|
||||||
|
ActionName string
|
||||||
|
OperationName string
|
||||||
|
Params []Param
|
||||||
|
SchemaParams []Param
|
||||||
|
UsesBool bool
|
||||||
|
UsesInt64 bool
|
||||||
|
UsesString bool
|
||||||
|
HasDefaults bool
|
||||||
|
NeedsBoolDefault bool
|
||||||
|
NeedsInt64Default bool
|
||||||
|
NeedsStringDefault bool
|
||||||
|
NeedsJsonPlanMod bool
|
||||||
|
NeedsStringsImport bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenModifier — отдельный ресурс для отложенной parent-level modify операции.
|
||||||
|
// Delete намеренно не содержит rollback: API-контракт обратного payload не подтверждён.
|
||||||
|
type GenModifier struct {
|
||||||
|
ServiceName string
|
||||||
|
ServiceID int
|
||||||
|
ModifierName string
|
||||||
|
OperationName string
|
||||||
|
Params []Param
|
||||||
|
SchemaParams []Param
|
||||||
|
// DeleteStrategy — noop_warn | inverse | error (нормализовано из YAML, пусто → noop_warn).
|
||||||
|
DeleteStrategy string
|
||||||
|
// Idempotency — none | check_before_run (нормализовано из YAML, пусто → none).
|
||||||
|
Idempotency string
|
||||||
|
// DeleteParams — обратные значения (wire-строки), только при DeleteStrategy==inverse.
|
||||||
|
DeleteParams []DeleteParam
|
||||||
|
UsesBool bool
|
||||||
|
UsesInt64 bool
|
||||||
|
UsesString bool
|
||||||
|
HasDefaults bool
|
||||||
|
NeedsBoolDefault bool
|
||||||
|
NeedsInt64Default bool
|
||||||
|
NeedsStringDefault bool
|
||||||
|
NeedsJsonPlanMod bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteParam — обратное значение параметра inverse-Delete (code → wire-строка).
|
||||||
|
type DeleteParam struct {
|
||||||
|
Code string
|
||||||
|
Value string
|
||||||
|
}
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
# =============================================================================
|
||||||
|
# Kubernetes кластер Штурвал — сервис 150, ресурс nubes_k8s_sthutrval_cluster
|
||||||
|
# (НЕ 148 «Менеджмент Kubernetes кластер Штурвал» — это другой сервис)
|
||||||
|
#
|
||||||
|
# Всё, что относится к Штурвалу, лежит ТОЛЬКО в этом файле: переменные, их
|
||||||
|
# значения по умолчанию и сам ресурс. Чтобы выключить Штурвал — удалить файл
|
||||||
|
# или закомментировать ресурс.
|
||||||
|
#
|
||||||
|
# Порядок (чек-лист из инструкции на услугу в ЛК):
|
||||||
|
# 1) Организация в Cloud Director — создана вручную в ЛК
|
||||||
|
# 2) nubes_vc_vdc.vdc — есть
|
||||||
|
# 3) nubes_vc_nsxt.edge — есть, обязательно ALB + AVI VS >= 3
|
||||||
|
# 4) внешние адреса в организации — суммарно >= 3 (nubes_vc_org_ip_allocation)
|
||||||
|
# 5) SNAT на Edge — nubes_vc_nsxt_snat
|
||||||
|
# 6) Kubernetes кластер Штурвал — этот ресурс
|
||||||
|
#
|
||||||
|
# Минимальные требования к кластеру: мастер-нод >= 1, воркер-нод >= 1,
|
||||||
|
# 4 vCPU / 8 GB RAM / 50 GB диска на ноду.
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# --- Переменные Штурвала ---
|
||||||
|
|
||||||
|
variable "shturval_resource_name" {
|
||||||
|
type = string
|
||||||
|
default = "shturval-dev"
|
||||||
|
description = "Имя услуги «Kubernetes кластер Штурвал» в ЛК"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "shturval_cluster_name" {
|
||||||
|
type = string
|
||||||
|
default = "shturval-dev-00"
|
||||||
|
description = "Имя кластера внутри Штурвала"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "shturval_app_version" {
|
||||||
|
type = string
|
||||||
|
default = "2.14.0"
|
||||||
|
description = "Версия Штурвала (значение по умолчанию платформы — 2.14.0)"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "shturval_cp_sizing_policy" {
|
||||||
|
type = string
|
||||||
|
default = "TKG 4CPU 8RAM"
|
||||||
|
description = "Политика размера control plane: 4 vCPU / 8 GB (минимум по инструкции). Должна существовать в ресурсной платформе vDC — список политик берётся из услуги «Виртуальный датацентр»"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "shturval_cp_sizing_disk" {
|
||||||
|
type = number
|
||||||
|
default = 50
|
||||||
|
description = "Диск control plane, ГБ (минимум 50)"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "shturval_cp_count" {
|
||||||
|
type = number
|
||||||
|
default = 1
|
||||||
|
description = "Количество мастер-нод: 1, 3 или 5"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "shturval_worker_group_name" {
|
||||||
|
type = string
|
||||||
|
default = "workers-shturval-dev"
|
||||||
|
description = "Имя группы воркеров (уникальное в кластере; допустимы строчные латинские буквы, цифры и дефис)"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "shturval_worker_sizing_policy" {
|
||||||
|
type = string
|
||||||
|
default = "TKG 4CPU 8RAM"
|
||||||
|
description = "Политика размера воркеров: 4 vCPU / 8 GB (минимум по инструкции)"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "shturval_worker_sizing_disk" {
|
||||||
|
type = number
|
||||||
|
default = 50
|
||||||
|
description = "Диск воркеров, ГБ (минимум 50)"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "shturval_worker_count" {
|
||||||
|
type = number
|
||||||
|
default = 1
|
||||||
|
description = "Количество воркер-нод (минимум 1)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Значения, которые собираются из переменных ---
|
||||||
|
|
||||||
|
locals {
|
||||||
|
# Группы воркеров передаются JSON-строкой ВНУТРЬ услуги как есть, поэтому ключи
|
||||||
|
# должны быть ровно такими, как в манифесте услуги 150: groupName, sizingPolicy,
|
||||||
|
# sizingDisk, count, autoscale, labelDeck.
|
||||||
|
# ВНИМАНИЕ: в сгенерированном примере провайдера (docs → Example) ключи показаны
|
||||||
|
# в snake_case — это ошибка генератора, платформа на них падает с
|
||||||
|
# «Cannot invoke method split() on null object» (не находит groupName → null).
|
||||||
|
shturval_worker_config = jsonencode([
|
||||||
|
{
|
||||||
|
groupName = var.shturval_worker_group_name
|
||||||
|
sizingPolicy = var.shturval_worker_sizing_policy
|
||||||
|
sizingDisk = var.shturval_worker_sizing_disk
|
||||||
|
count = var.shturval_worker_count
|
||||||
|
autoscale = false # автоскейл выключен
|
||||||
|
labelDeck = true # разрешить разворачивать услуги из ЛК на этих нодах
|
||||||
|
}
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Ресурс Штурвала ---
|
||||||
|
|
||||||
|
resource "nubes_k8s_sthutrval_cluster" "shturval" {
|
||||||
|
resource_name = var.shturval_resource_name
|
||||||
|
|
||||||
|
# Штурвал создаётся долго (десятки минут) — поднимаем таймаут ожидания,
|
||||||
|
# иначе провайдер сдаётся на дефолтных 600 с.
|
||||||
|
operation_timeout = "60m"
|
||||||
|
|
||||||
|
startup_configuration = {
|
||||||
|
# vDC и Edge из этого же конфига (обязательные поля)
|
||||||
|
vdc_uid = nubes_vc_vdc.vdc.id
|
||||||
|
nsxt_uid = nubes_vc_nsxt.edge.id
|
||||||
|
|
||||||
|
cluster_name = var.shturval_cluster_name
|
||||||
|
|
||||||
|
# Дополнительные возможности кластера (в ЛК — галочки при создании)
|
||||||
|
ex_logging = true # логи в Loki (без него логи услуг не видны в ЛК)
|
||||||
|
ex_monitoring = true # метрики в VictoriaMetrics (без него метрик в ЛК нет)
|
||||||
|
ex_local_csi = true
|
||||||
|
ex_vip = true
|
||||||
|
ex_update = true
|
||||||
|
ex_ingress = true
|
||||||
|
ex_named_csi = true
|
||||||
|
}
|
||||||
|
|
||||||
|
cluster_configuration = {
|
||||||
|
app_version = var.shturval_app_version
|
||||||
|
}
|
||||||
|
|
||||||
|
control_plane_configuration = {
|
||||||
|
sizing_policy = var.shturval_cp_sizing_policy
|
||||||
|
sizing_disk = var.shturval_cp_sizing_disk
|
||||||
|
count = var.shturval_cp_count
|
||||||
|
}
|
||||||
|
|
||||||
|
worker_configuration = local.shturval_worker_config
|
||||||
|
|
||||||
|
access_configuration = {
|
||||||
|
need_external_address_api = true # внешний адрес для Kubernetes API (false недопустим)
|
||||||
|
access_ip_list_api = jsonencode([]) # пусто = доступ всем
|
||||||
|
need_external_address_ingress = true # внешний адрес для Ingress
|
||||||
|
access_ip_list_ingress = jsonencode([]) # пусто = доступ всем
|
||||||
|
}
|
||||||
|
|
||||||
|
# Кластер поднимается только после готовой сети: vDC -> Edge -> внешние IP -> SNAT
|
||||||
|
depends_on = [nubes_vc_nsxt_snat.snat]
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
provider_installation {
|
||||||
|
dev_overrides {
|
||||||
|
"tf-registry.containerk8s.services.ngcloud.ru/nubes-dev/nubes" = "/home/naeel/TF/tf_provider/TMP/devbin"
|
||||||
|
}
|
||||||
|
direct {}
|
||||||
|
}
|
||||||
@@ -212,3 +212,34 @@ From the unified YAML, generate:
|
|||||||
- No manual edits to generated YAML or generated Go code.
|
- No manual edits to generated YAML or generated Go code.
|
||||||
- Any change must come from API or generator logic updates.
|
- Any change must come from API or generator logic updates.
|
||||||
- The generator must enforce these rules and fail fast on drift.
|
- 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}`.
|
Структура: `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
|
## docs-generator
|
||||||
|
|
||||||
`resources_yaml/*.yaml` → Markdown-документация в `docs/30_registry/resources/`
|
`resources_yaml/*.yaml` → Markdown-документация в `docs/30_registry/resources/`
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ TOKEN_FILE="secrets/dev.token"
|
|||||||
|
|
||||||
# Release versions
|
# Release versions
|
||||||
# Version
|
# Version
|
||||||
VERSION="2.0.0"
|
VERSION="2.0.17"
|
||||||
|
|
||||||
NAMESPACE="nubes-dev"
|
NAMESPACE="nubes-dev"
|
||||||
PROVIDER_NAME="nubes"
|
PROVIDER_NAME="nubes"
|
||||||
|
|||||||
@@ -6,17 +6,12 @@
|
|||||||
12 s3 # S3 Object Storage
|
12 s3 # S3 Object Storage
|
||||||
13 s3bucket # S3 бакет
|
13 s3bucket # S3 бакет
|
||||||
19 vcOrg # Организация в Cloud Director
|
19 vcOrg # Организация в Cloud Director
|
||||||
# 20 vcOrgSaas # Организация [DEPRECATED] — нет в UI
|
|
||||||
21 vc_vdc # Виртуальный датацентр (vDC)
|
21 vc_vdc # Виртуальный датацентр (vDC)
|
||||||
22 vc_nsxt # Сетевой шлюз периметра (Edge)
|
22 vc_nsxt # Сетевой шлюз периметра (Edge)
|
||||||
# 23 vc_vm # VM в Cloud Director (старый формат) (vc_vm) — нет в UI
|
|
||||||
# 24 vcNat # DEPRECATED Правила маршрутизации для VM (vc_nat)
|
|
||||||
25 vcexternalip # Публичные IP адреса
|
25 vcexternalip # Публичные IP адреса
|
||||||
26 vapp # Виртуальный каталог ВМ (vApp)
|
26 vapp # Виртуальный каталог ВМ (vApp)
|
||||||
# 27 vc_vm_v2 # VM в Cloud Director (vc_vmV2) — нет в DEV UI
|
|
||||||
28 vc_vm_v3 # Виртуальная машина
|
28 vc_vm_v3 # Виртуальная машина
|
||||||
29 vcVdcGroup # Группа датацентров
|
29 vcVdcGroup # Группа датацентров
|
||||||
# 32 vmpostgre # vc_vm_postgresql_std
|
|
||||||
50 nextcloud # Nextcloud
|
50 nextcloud # Nextcloud
|
||||||
81 superset # Apache Superset
|
81 superset # Apache Superset
|
||||||
82 harbor # Container Registry
|
82 harbor # Container Registry
|
||||||
@@ -34,13 +29,8 @@
|
|||||||
97 nodered # NodeRed
|
97 nodered # NodeRed
|
||||||
98 http # Простой HTTP контейнер
|
98 http # Простой HTTP контейнер
|
||||||
99 gitea # Gitea
|
99 gitea # Gitea
|
||||||
# 100 openwhisk # Serverless Openwhisk — нет в UI
|
|
||||||
109 zonesV2 # Управление DNS
|
109 zonesV2 # Управление DNS
|
||||||
# 110 dnszone # DNS зона — нет в UI
|
|
||||||
111 dnsrecord # DNS запись
|
111 dnsrecord # DNS запись
|
||||||
# 112 tenant # Тенант в Grafana — нет в UI
|
|
||||||
# 113 vcComplex # Быстрый старт — нет в UI
|
|
||||||
# 114 GiteaComplex # Комплексная услуга по созданию gitea — нет в UI
|
|
||||||
115 mariadb # Mariadb
|
115 mariadb # Mariadb
|
||||||
116 kafka # ApacheKafka
|
116 kafka # ApacheKafka
|
||||||
# 117 nifi # Nifi
|
# 117 nifi # Nifi
|
||||||
@@ -50,6 +40,5 @@
|
|||||||
149 valoTenant # VALO Cloud
|
149 valoTenant # VALO Cloud
|
||||||
150 k8sSthutrvalCluster # Kubernetes кластер Штурвал
|
150 k8sSthutrvalCluster # Kubernetes кластер Штурвал
|
||||||
151 k8sOpenbao # Vault
|
151 k8sOpenbao # Vault
|
||||||
# 153 nifi # Nifi (DEV)
|
153 nifi # Nifi (DEV)
|
||||||
163 llmAi # LLM
|
163 llmAi # LLM
|
||||||
# 175 k8sGo # Go — нет в UI
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user