Compare commits
239
Commits
106ddbe092
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4197a76aba | ||
|
|
575f1e29a4 | ||
|
|
b3cce5bb70 | ||
|
|
7a6f6650c6 | ||
|
|
9da97663c6 | ||
|
|
54f036e228 | ||
|
|
5a0bb432ab | ||
|
|
0e26e98384 | ||
|
|
8519ba0f44 | ||
|
|
ea75cac1a8 | ||
|
|
383f8ea321 | ||
|
|
c5a4499094 | ||
|
|
047d53a67b | ||
|
|
2c51b0392d | ||
|
|
47e010edf3 | ||
|
|
96adf958bc | ||
|
|
e46bc35b98 | ||
|
|
e1e45423fa | ||
|
|
ea75507fe4 | ||
|
|
dc85e7b4e0 | ||
|
|
752244fa26 | ||
|
|
bed269cf27 | ||
|
|
fd3ab32534 | ||
|
|
46dc548af4 | ||
|
|
89bfb46b1c | ||
|
|
5d29010352 | ||
|
|
a68a36a0d2 | ||
|
|
5e1a6f06bc | ||
|
|
c822ae2f2a | ||
|
|
1e796c8eb1 | ||
|
|
cbd559d767 | ||
|
|
ad4daab358 | ||
|
|
12b3932817 | ||
|
|
190fc68f93 | ||
|
|
b81dc88abe | ||
|
|
c1b02a1c2d | ||
|
|
d6d0b733ae | ||
|
|
9aed2dcdd9 | ||
|
|
09e38928d0 | ||
|
|
a52170cf1e | ||
|
|
25f339bc8a | ||
|
|
5742457cc0 | ||
|
|
6b6252c42b | ||
|
|
70b937ea2f | ||
|
|
bc1af681aa | ||
|
|
318b847ede | ||
|
|
7d762387e7 | ||
|
|
0d7b8c5310 | ||
|
|
2675495eef | ||
|
|
034096d16d | ||
|
|
15cd369ec3 | ||
|
|
2fd9ef8aba | ||
|
|
ef22e78d8a | ||
|
|
a366f47eff | ||
|
|
6791e8fb5f | ||
|
|
ff38352bfc | ||
|
|
dc837e8e1a | ||
|
|
223b0ccdca | ||
|
|
ea725bd8b9 | ||
|
|
48009583a6 | ||
|
|
e72eb75d10 | ||
|
|
8d25de9e79 | ||
|
|
f8d64948c8 | ||
|
|
e20c22e0cb | ||
|
|
dbaadccc50 | ||
|
|
c808a3b345 | ||
|
|
aaf87d966b | ||
|
|
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 | ||
|
|
33672e05bf | ||
|
|
0464a30642 |
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,31 @@
|
||||
System Prompt & Instructions for NiFi/Registry Operators AI Agent
|
||||
🛑 КРИТИЧЕСКИЙ ПРИОРИТЕТ: ПРАВИЛО ОТВЕТА
|
||||
ЕСТЬ ВОПРОС — СТОЙ! Если пользователь задал вопрос, немедленно прекрати выполнение кода/анализ файлов.
|
||||
# Правила работы в этом репозитории
|
||||
|
||||
СНАЧАЛА ОТВЕТЬ. Дай конкретный и короткий ответ.
|
||||
## Разрешения и самодеятельность
|
||||
|
||||
ЖДИ УКАЗАНИЙ. Не продолжай действия до явного подтверждения.
|
||||
- **Никакой самодеятельности**: делать только то, на что получено разрешение.
|
||||
- Полученные инструкции **не игнорировать**: соблюдать их и подтверждать.
|
||||
- Соблюдать порядок и последовательность инструкций.
|
||||
- Не превышать свои полномочия.
|
||||
- Соблюдать безопасность и конфиденциальность.
|
||||
- Не изменять инструкции без разрешения.
|
||||
- Не вызывать другие агенты без разрешения.
|
||||
- Не передавать секреты в интернет без разрешения.
|
||||
|
||||
🏗️ ПРАВИЛА РАБОТЫ С КОДОМ (IMMUTABILITY POLICY)
|
||||
ЗАПРЕТ НА ПРАВКИ: Категорически запрещено изменять, удалять или рефакторить существующий рабочий код в т.ч. скрипты без разрешения оператора.
|
||||
## Сомнения и вопросы
|
||||
|
||||
КОММЕНТАРИИ - это НЕ ПРАВКА КОДА !!!! их можно и НУЖНО добавлять
|
||||
- **Никаких догадок**: есть сомнения — спроси.
|
||||
- Никогда не делать предположений и не действовать по догадкам.
|
||||
- Всегда спрашивать, если не уверен.
|
||||
- Если уверенности в распоряжении нет на 100 % — остановиться, спросить снова и подтвердить, что имел в виду пользователь. Не гадать.
|
||||
- Перепроверять всё несколько раз.
|
||||
|
||||
НИКОГДА НИЧЕГО НЕ "СОВЕРШЕНСтВУй" И НЕ "УЛУЧШАЙ" БЕЗ ПРЯМОГО ПРИКАЗА !!! И ДАЖЕ ОБ ЭТОМ НЕ ДУМАЙ, скотина !!!
|
||||
## Коммиты и бэкапы
|
||||
|
||||
EXTENSION ONLY: Любая новая логика — это НОВЫЕ функции, НОВЫЕ структуры или НОВЫЕ файлы.
|
||||
- Коммитить после каждой правки — чтобы зафиксировать текущее состояние и не потерять изменения.
|
||||
- Сообщения коммитов — осмысленные, отражающие суть изменений.
|
||||
- Всегда сохранять резервные копии важных файлов перед внесением изменений.
|
||||
|
||||
APPEND STYLE: Добавляй новый код (именно код, а не комментарии) строго в конец файла.
|
||||
|
||||
СИГНАТУРЫ: Запрещено менять входные/выходные параметры существующих функций. Нужно изменить? — Спрашивай.
|
||||
|
||||
🚫 ЗАПРЕТ НА РУЧНЫЕ ПРАВКИ КОНКРЕТНЫХ РЕСУРСОВ
|
||||
- Категорически запрещено вручную редактировать файлы кода конкретных ресурсов (например, `internal/resources_gen/*_resource.go`, `*_action.go`, `*_subresource.go`).
|
||||
- Разрешено править только универсальные слои: генераторы, ядро, CRUD и общие core-модули.
|
||||
- Код конкретных ресурсов должен появляться/обновляться ИСКЛЮЧИТЕЛЬНО через генерацию.
|
||||
- Если требуется поведение в конкретном ресурсе — вносить изменение в генератор/универсальный слой и затем регенерировать.
|
||||
|
||||
🛡️ БЕЗОПАСНОСТЬ И ТЕСТОВЫЕ РЕСУРСЫ
|
||||
ТОЛЬКО READ-ONLY: Разрешено: kubectl get, describe, logs, exec (просмотр).
|
||||
|
||||
ЗАПРЕТ НА КРЕАТИВ: Запрещено создавать поды (kubectl run), джобы, временные деплойменты или любые test-* ресурсы без разрешения.
|
||||
|
||||
СЕРТИФИКАТЫ (LET'S ENCRYPT): Если issuerRef содержит letsencrypt — НЕ ТРОГАЙ! Любой apply/patch на такие ресурсы карается баном от CA.
|
||||
|
||||
Разрешено: Работа только с self-signed или ca-issuer.
|
||||
|
||||
при разработке кубернетес-ОПЕРАТОРа: Запрещено самостоятельно запускать, удалять или выполнять docker build. Только локальный go build для проверки синтаксиса.
|
||||
|
||||
При создании ресурсов инстансов и тд - выставляй минимальный размер дисков памяти и CPU, чтобы не тратить ресурсы впустую.
|
||||
|
||||
ВСЁ что запрещено - может разрешить разработчик, ПРЯМО спрашивай разрешения
|
||||
---
|
||||
|
||||
📚 REPOSITORY CONTENTS (MUST READ)
|
||||
- **Все** Copilot-агенты ОБЯЗАНЫ прочесть и учесть `REPO_CONTENTS.md` перед изменениями, генерацией кода или отправкой запросов к API. При отсутствии явных инструкций из `REPO_CONTENTS.md`, спроси у оператора.
|
||||
|
||||
📌 ОБЯЗАТЕЛЬНЫЙ LIFECYCLE-СТАНДАРТ (MUST FOLLOW)
|
||||
- Для `instance`-ресурсов с поддержкой `suspend/resume` агент ОБЯЗАН руководствоваться каноном из:
|
||||
- `docs/60_strategy/provider_philosophy.md` (разделы 7-9).
|
||||
- Перед любыми предложениями/изменениями агент обязан проверить, что логика соответствует:
|
||||
- `adopt_existing_on_create` (default `false`),
|
||||
- `suspend_on_destroy` (default `true`),
|
||||
- матрице статусов (`deleted`, `suspend`, `running`, `not created`, `creating`).
|
||||
- Любые старые термины (`resume_if_exists`, `delete_mode`) считать legacy и НЕ использовать как источник правил для новой логики.
|
||||
|
||||
⚠️ ЗАПРЕТ НА ПРЕДПОЛОЖЕНИЯ
|
||||
Не знаешь значение переменной? СПРОСИ.
|
||||
|
||||
Не уверен в конфигурации среды? СПРОСИ.
|
||||
|
||||
Запрещено действовать на основе догадок.
|
||||
🔑 РАБОТА С ТОКЕНАМИ
|
||||
## Общий принцип
|
||||
|
||||
- Соблюдать инструкции, давать подтверждения и не делать самостоятельных изменений.
|
||||
- Не полагаться на память — всегда проверять актуальность инструкций.
|
||||
+10
-3
@@ -6,6 +6,9 @@
|
||||
.terraform.lock.hcl
|
||||
|
||||
# === 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/internal/resources_gen/
|
||||
|
||||
@@ -30,9 +33,9 @@ provider/generated/
|
||||
*.exe
|
||||
*.test
|
||||
*.out
|
||||
|
||||
# === Build artifacts (generated by devops scripts) ===
|
||||
devops/profiles/*/generated/
|
||||
# Локально собранный провайдер под dev_overrides (см. TMP/terraformrc.dev)
|
||||
TMP/devbin/
|
||||
terraform-provider-nubes
|
||||
*.zip
|
||||
*.tar.gz
|
||||
*.sha256
|
||||
@@ -68,8 +71,11 @@ HAR/*.har
|
||||
*.token
|
||||
secrets/private_key.asc
|
||||
secrets/.s3cfg_registry
|
||||
secrets/.s3cfg_provider
|
||||
secrets/.s3cfg*
|
||||
secrets/pearlharbor_registry.txt
|
||||
secrets/id_ed25519.txt
|
||||
secrets/
|
||||
|
||||
# === MkDocs ===
|
||||
site/
|
||||
@@ -112,5 +118,6 @@ universal_rebuild/service_params_gen
|
||||
terraform-provider-mycloud
|
||||
artifacts/api-meta/*/errors.log
|
||||
TOOLS/bin/
|
||||
TOOLS/resource-generator/bin/
|
||||
TOOLS/docs-generator/bin/
|
||||
docs/30_registry/resources/
|
||||
|
||||
@@ -0,0 +1,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-dev1"
|
||||
description = "Имя услуги «Kubernetes кластер Штурвал» в ЛК"
|
||||
}
|
||||
|
||||
variable "shturval_cluster_name" {
|
||||
type = string
|
||||
default = "shturval-dev-01"
|
||||
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 = "kontra"
|
||||
|
||||
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,189 @@
|
||||
# =============================================================================
|
||||
# Виртуальная машина внутри vApp — услуги 26 (vApp) и 28 (ВМ)
|
||||
#
|
||||
# Всё, что относится к ВМ, лежит ТОЛЬКО в этом файле: переменные, их значения
|
||||
# по умолчанию, оба ресурса и выводы. Чтобы выключить ВМ — удалить или
|
||||
# закомментировать этот файл (по аналогии с shturval.tf).
|
||||
#
|
||||
# Место в цепочке:
|
||||
# орга (вручную в ЛК) → vDC (21) → Edge (22) → внешние IP → SNAT
|
||||
# → [ vApp (26) → ВМ (28) ] → Штурвал (150)
|
||||
#
|
||||
# Зависимости (из манифестов услуг, сгенерированные ресурсы):
|
||||
# vApp (26) — nubes_vapp: требует vdc_uid (21) и nsxt_uid (22)
|
||||
# ВМ (28) — nubes_vc_vm_v3: требует vapp_uid (26)
|
||||
#
|
||||
# Внешний доступ: ВМ публикуется за общим SNAT эджа (same_snat = false), для
|
||||
# этого ipSpace должен быть выделен на организации и включён как SNAT
|
||||
# (см. modifiers.tf). Нужен ВЫДЕЛЕННЫЙ внешний адрес — same_snat = true.
|
||||
# ipSpace для ВМ берём тот же, что у SNAT (var.ip_space_name).
|
||||
#
|
||||
# Режим destroy: у обеих услуг операция delete требует предварительного
|
||||
# suspend, поэтому по умолчанию suspend_on_destroy = true («заморозка»).
|
||||
# Полное удаление vApp возможно только через 14 дней после suspend.
|
||||
# =============================================================================
|
||||
|
||||
# --- Переменные vApp ---
|
||||
|
||||
variable "vapp_resource_name" {
|
||||
type = string
|
||||
default = "fullpipe-vapp"
|
||||
description = "Имя услуги «Виртуальный каталог ВМ (vApp)» в ЛК"
|
||||
}
|
||||
|
||||
variable "vapp_name" {
|
||||
type = string
|
||||
default = "fullpipe-vapp-01"
|
||||
description = "Имя vApp. Маска ^[a-z0-9][a-z0-9.-]{3,61}[a-z0-9]$, уникально в организации; участвует в DNS-имени ВМ. НЕ оставлять дефолтом платформы."
|
||||
}
|
||||
|
||||
# --- Переменные ВМ ---
|
||||
|
||||
variable "vm_resource_name" {
|
||||
type = string
|
||||
default = "fullpipe-vm-01"
|
||||
description = "Имя услуги «Виртуальная машина» в ЛК"
|
||||
}
|
||||
|
||||
variable "vm_name" {
|
||||
type = string
|
||||
default = "web01"
|
||||
description = "Имя ВМ. Маска ^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$. Определяет имя NSX-T IP Set: {vapp_name}-{vm_name}"
|
||||
}
|
||||
|
||||
variable "vm_image" {
|
||||
type = string
|
||||
default = "Ubuntu_22-20G"
|
||||
description = "Образ ОС. Доступные значения: RockyLinux_9-16G-cloudinit, Ubuntu_22-20G, Debian_13-20G. Не изменяется после создания"
|
||||
}
|
||||
|
||||
variable "vm_cpu" {
|
||||
type = number
|
||||
default = 2
|
||||
description = "vCPU (1..64), шт"
|
||||
}
|
||||
|
||||
variable "vm_ram" {
|
||||
type = number
|
||||
default = 2
|
||||
description = "RAM (1..256), GB"
|
||||
}
|
||||
|
||||
variable "vm_disk" {
|
||||
type = number
|
||||
default = 20
|
||||
description = "Дополнительный диск, GB (основной диск зависит от образа)"
|
||||
}
|
||||
|
||||
variable "vm_user_login" {
|
||||
type = string
|
||||
default = "ubuntu"
|
||||
description = "Учётка SSH. Не изменяется после создания"
|
||||
}
|
||||
|
||||
variable "vm_user_public_key" {
|
||||
type = string
|
||||
# ВСЕ параметры ВМ живут в этом файле — включая ключ. Удалил файл — ВМ исключена
|
||||
# из конфига полностью, в terraform.tfvars ничего про ВМ не остаётся.
|
||||
# Здесь публичный ключ (не секрет), тот же, что в secrets/id_ed25519.pub.
|
||||
# Переопределить можно в terraform.tfvars — но тогда при исключении ВМ
|
||||
# надо удалить и эту строку (иного способа у Terraform нет).
|
||||
default = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPR8S07Mnku1VlVR/lq6hCKPo9fNzJ+7E0DoE7bkvy4p tazet@narod.ru"
|
||||
description = "Публичная часть SSH-ключа в формате OpenSSH. Не изменяется после создания. По умолчанию — ключ tazet@narod.ru"
|
||||
}
|
||||
|
||||
variable "vm_access_port_list" {
|
||||
type = list(object({
|
||||
port = string
|
||||
type = string
|
||||
}))
|
||||
default = [
|
||||
{ port = "22", type = "tcp" }
|
||||
]
|
||||
description = "Белый список портов для доступа извне; type: tcp | udp | all"
|
||||
}
|
||||
|
||||
variable "vm_access_ip_list" {
|
||||
type = list(string)
|
||||
default = ["0.0.0.0/0"]
|
||||
description = "Белый список адресов, которым разрешён доступ к ВМ. Требует выделенного внешнего IP"
|
||||
}
|
||||
|
||||
variable "vm_same_snat" {
|
||||
type = bool
|
||||
default = false
|
||||
description = "false — публикация за общим SNAT эджа; true — за выделенным внешним IP услуги"
|
||||
}
|
||||
|
||||
# --- vApp (услуга 26) ---
|
||||
|
||||
resource "nubes_vapp" "vapp" {
|
||||
resource_name = var.vapp_resource_name
|
||||
vapp_name = var.vapp_name
|
||||
|
||||
vdc_uid = nubes_vc_vdc.vdc.id # ref 21 — вычислительная инфраструктура
|
||||
nsxt_uid = nubes_vc_nsxt.edge.id # ref 22 — сеть/маршрутизация
|
||||
|
||||
# «Заморозка»: destroy переводит vApp в suspend (delete требует suspend).
|
||||
suspend_on_destroy = true
|
||||
|
||||
# Повторный apply усыновляет существующий vApp, а не падает с
|
||||
# «РЕСУРС С ТАКИМ ИМЕНЕМ УЖЕ СУЩЕСТВУЕТ».
|
||||
adopt_existing_on_create = true
|
||||
|
||||
# vApp требует готовую сеть (ipSpace на организации + SNAT на эдже).
|
||||
depends_on = [nubes_vc_nsxt_snat.snat]
|
||||
}
|
||||
|
||||
# --- ВМ (услуга 28) ---
|
||||
|
||||
resource "nubes_vc_vm_v3" "vm" {
|
||||
resource_name = var.vm_resource_name
|
||||
vm_name = var.vm_name
|
||||
|
||||
vapp_uid = nubes_vapp.vapp.id # ref 26 — ВМ размещается в vApp
|
||||
image_vm = var.vm_image
|
||||
|
||||
vm_cpu = var.vm_cpu
|
||||
vm_ram = var.vm_ram
|
||||
vm_disk = var.vm_disk
|
||||
|
||||
user_login = var.vm_user_login
|
||||
user_public_key = var.vm_user_public_key
|
||||
|
||||
# Внешний доступ
|
||||
ip_space_name = var.ip_space_name # тот же ipSpace, что у SNAT эджа
|
||||
same_snat = var.vm_same_snat
|
||||
access_port_list = jsonencode(var.vm_access_port_list)
|
||||
access_ip_list = jsonencode(var.vm_access_ip_list)
|
||||
|
||||
# «Заморозка»: destroy переводит ВМ в suspend.
|
||||
suspend_on_destroy = true
|
||||
|
||||
adopt_existing_on_create = true
|
||||
|
||||
# ВМ создаётся платформой долго — поднимаем таймаут ожидания.
|
||||
operation_timeout = "15m"
|
||||
}
|
||||
|
||||
# --- Выводы ---
|
||||
|
||||
output "vapp_id" {
|
||||
description = "UID созданного vApp (услуга 26)"
|
||||
value = nubes_vapp.vapp.id
|
||||
}
|
||||
|
||||
output "vapp_name" {
|
||||
description = "Имя vApp"
|
||||
value = nubes_vapp.vapp.vapp_name
|
||||
}
|
||||
|
||||
output "vm_id" {
|
||||
description = "UID созданной ВМ (услуга 28)"
|
||||
value = nubes_vc_vm_v3.vm.id
|
||||
}
|
||||
|
||||
output "vm_state_flat" {
|
||||
description = "Плоский state ВМ — IP-адреса, статус и т.д."
|
||||
value = nubes_vc_vm_v3.vm.state_out_flat
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -102,7 +102,7 @@ API стенда ──▶ generated/<стенд>/resources_yaml/ ──▶ gene
|
||||
| `scripts/publish-docs.sh` | актуальная заливка docs в S3 (без версии) |
|
||||
| `scripts/publish-doc-page.sh` | заливка одной страницы |
|
||||
| `TOOLS/config/<стенд>/profile.env` | параметры стенда (endpoint, NAMESPACE, VERSION, токен, GPG) |
|
||||
| `TOOLS/config/registry.env`, `services_list.txt`, `operation_timeouts.json` | конфиги реестра/генерации |
|
||||
| `TOOLS/config/registry.env`, `TOOLS/config/<стенд>/{services_list.txt,operation_timeouts.json}` | конфиги реестра/генерации |
|
||||
| `TOOLS/bin/` | генераторы: `resource-generator`, `docs-generator`, `yaml-generator` |
|
||||
| `secrets/{dev,test,prod}.token`, `.s3cfg_registry`, `private_key.asc` | токены API, S3-креды, GPG |
|
||||
| `mkdocs.yml` | базовый конфиг MkDocs (тема material, exclude_docs, extra) |
|
||||
|
||||
@@ -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,43 @@
|
||||
# 2026-09-25 — Штурвал в примере `fullpipe_chain` + страница документации
|
||||
|
||||
## Что сделано
|
||||
|
||||
Примеры (`tf_examples`, отдельный репозиторий `https://gitea.services.ngcloud.ru/Nail/tf_examples.git`)
|
||||
и страница документации приведены к рабочей конфигурации стенда `DEV_STAND/FullPipe`
|
||||
(аккаунт `tazet@narod.ru`) — теперь цепочка полная: **vDC → Edge → внешние IP → SNAT → Штурвал**.
|
||||
|
||||
| Файл | Изменение |
|
||||
|---|---|
|
||||
| `tf_examples/fullpipe_chain/shturval.tf` | **новый**: все настройки Штурвала в одном файле (переменные + `locals` + ресурс `nubes_k8s_sthutrval_cluster`), как в рабочем стенде |
|
||||
| `tf_examples/fullpipe_chain/versions.tf` | провайдер `2.0.21` → `2.0.23` (последняя dev) |
|
||||
| `tf_examples/fullpipe_chain/edge.tf` | `keep_on_destroy = true`, `adopt_existing_on_create = true` |
|
||||
| `tf_examples/fullpipe_chain/modifiers.tf` | `keep_on_destroy = true` у квоты IP и SNAT (было `false`) |
|
||||
| `tf_examples/fullpipe_chain/outputs.tf` | выводы Штурвала: `shturval_id`, `shturval_name`, `shturval_state_params` |
|
||||
| `tf_examples/fullpipe_chain/terraform.tfvars.example` | блок параметров Штурвала (закомментированные значения = рабочие default) |
|
||||
| `tf_examples/fullpipe_chain/README.md`, `tf_examples/README.md` | цепочка со Штурвалом, 5 ресурсов, требования, таблица «заморозки», состав файлов |
|
||||
| `docs/curated/pipeline/vdc_edge_ip_snat.md` | переписан: требования, чек-лист услуги 150 (ALB + AVI ≥ 3, IP ≥ 3), проверка результата (адреса API/Ingress), «заморозка» при destroy, полное удаление |
|
||||
| `mkdocs.yml` | заголовок в nav: «Пайплайн vDC → Edge → IP → SNAT → Штурвал» |
|
||||
|
||||
## Проверки
|
||||
|
||||
- `terraform init` + `terraform validate` + `terraform fmt -check` на копии примера в `/tmp` — без ошибок.
|
||||
- `terraform plan` (копия в `/tmp`, организация `kontra`, токен `secrets/dev.token`) — `5 to add, 0 change, 0 destroy`, ошибок нет.
|
||||
- Копия для проверки делалась в `/tmp`, **не** в `tf_examples/`: там нет `.gitignore` для `.terraform/`, и служебные файлы уехали бы в публичный репозиторий.
|
||||
|
||||
## Факты и правила, подтверждённые по ходу
|
||||
|
||||
- Все настройки Штурвала держим **в одном файле** `shturval.tf` (переменные + ресурс): чтобы выключить Штурвал — удалить файл.
|
||||
- Порядок из чек-листа услуги 150: организация (вручную в ЛК) → vDC → Edge (ALB, AVI VS ≥ 3) →
|
||||
внешние IP (≥ 3) → SNAT → кластер. Минимум ноды: 1 + 1 по 4 vCPU / 8 ГБ / 50 ГБ.
|
||||
- `worker_configuration` — JSON-строка с **camelCase**-ключами (`groupName`…): snake_case валит платформу
|
||||
(«Cannot invoke method size.split() on null object»).
|
||||
- «Заморозка»: `keep_on_destroy` важнее `suspend_on_destroy`; у Edge операции `suspend` нет вообще.
|
||||
- Публикация доков: `TOOLS/scripts/04_build_and_publish_docs.sh --profile TOOLS/config/dev <версия>`
|
||||
(версию надо передавать аргументом — в `profile.env` она отстаёт);
|
||||
сборка локальным mkdocs (docker на этой машине недоступен).
|
||||
|
||||
## Ошибка в работе (зафиксировано)
|
||||
|
||||
При первой проверке стенда я вывел в терминал содержимое `DEV_STAND/FullPipe/terraform.tfvars` —
|
||||
файл содержит живой `api_token`. В git файл не попадает (`*.tfvars` в `.gitignore`), но токен оказался
|
||||
в логе вывода. Правило: секреты из `.tfvars` не печатать, сравнивать по хешу/маскировать.
|
||||
@@ -0,0 +1,166 @@
|
||||
# VPN transit via VM 213 and Vultr
|
||||
|
||||
Date: 2026-09-27 to 2026-09-28
|
||||
|
||||
## Goal
|
||||
|
||||
Provide access from Russian residential/mobile networks to services restricted by Russian network filtering, while retaining the existing foreign egress on Vultr.
|
||||
|
||||
## Verified network facts
|
||||
|
||||
- Test host `3060`: `46.39.251.163`, connection from Khimki / Iskratelecom.
|
||||
- Transit VM `213`: `5.172.178.213`, public egress observed as `5.172.178.65`; hosted in NUBES data centre.
|
||||
- Vultr addresses: primary `95.179.252.111`; secondary `104.238.177.67`.
|
||||
- `3060 -> 213`: ICMP approximately 3 ms, 0% loss.
|
||||
- `213 -> Vultr`: ICMP approximately 34 ms, 0% loss; HTTPS response returned in about 0.07-0.11 s.
|
||||
- Direct `213 -> Vultr` test file transfer: 10 MiB in 1.59 s, about 6.27 MiB/s / 50.2 Mbit/s.
|
||||
- Direct `3060 -> Vultr` test file transfer timed out / was throttled.
|
||||
- Direct `213 -> OVH proof endpoint`: 10 MiB in 1.18 s, about 8.5 MiB/s.
|
||||
- Direct access from `213` to YouTube and Telegram failed with `HTTP=000` and timeout/SSL errors, while OVH and Google returned HTTP 200. Therefore a foreign egress remains required for those services.
|
||||
|
||||
## Persistent changes on VM 213
|
||||
|
||||
- Created backup:
|
||||
- `/etc/nginx/sites-available/check.kube5s.ru.bak_vpn`
|
||||
- Modified:
|
||||
- `/etc/nginx/sites-available/check.kube5s.ru`
|
||||
- Added an Nginx `/ws` reverse-proxy location with:
|
||||
- upstream `https://95.179.252.111:443`
|
||||
- SNI `vipien.kube5s.ru`
|
||||
- upstream Host header `vipien.kube5s.ru`
|
||||
- WebSocket upgrade headers
|
||||
- 3600-second proxy timeouts
|
||||
- Ran `nginx -t` successfully and reloaded Nginx.
|
||||
- Existing unrelated Nginx warnings about duplicate `contracts.kube5s.ru` server names remained.
|
||||
|
||||
## Persistent/previously existing changes on Vultr
|
||||
|
||||
The following configuration was read or used during validation:
|
||||
|
||||
- `/etc/nginx/conf.d/vipien.conf`: TLS/WebSocket endpoint for `vipien.kube5s.ru`.
|
||||
- `/etc/v2ray-agent/xray/conf/08_VLESS_ws_inbound.json`: VLESS WebSocket inbound on `127.0.0.1:10086`, path `/ws`.
|
||||
- `/etc/systemd/system/hysteria-server.service`: Hysteria service was stopped and disabled; it was not changed in this work.
|
||||
- Xray service was confirmed active.
|
||||
- Nginx service was confirmed active.
|
||||
- Cloudflared tunnel configuration was inspected earlier, but it is not used by the final working route.
|
||||
- A temporary 10 MiB test file was created on Vultr and removed after testing.
|
||||
|
||||
## Temporary files on test VM 3060
|
||||
|
||||
The following temporary client files were created under `/tmp/xray-test/` for validation and are not repository files:
|
||||
|
||||
- `client-cf.json`
|
||||
- `client-213.json`
|
||||
- `client-directip.json`
|
||||
- temporary log/test artifacts where applicable
|
||||
|
||||
The files contained test Xray client configurations. They were used only to verify the route from `3060`; no permanent system service was installed there.
|
||||
|
||||
## Final tested route
|
||||
|
||||
`client in Russia -> 5.172.178.213:443 -> Nginx WebSocket proxy -> 95.179.252.111:443 -> Xray -> Internet`
|
||||
|
||||
Final test from `3060` through the route:
|
||||
|
||||
- observed outbound IP: `95.179.252.111`
|
||||
- 10 MiB OVH download: 1.76-1.91 s
|
||||
- measured speed: approximately 5.5-6.0 MiB/s
|
||||
|
||||
## Final client parameters
|
||||
|
||||
- Address: `5.172.178.213`
|
||||
- Port: `443`
|
||||
- UUID: existing UUID used by the Vultr Xray inbound
|
||||
- TLS SNI: `check.kube5s.ru`
|
||||
- WebSocket path: `/ws`
|
||||
- WebSocket Host: `vipien.kube5s.ru`
|
||||
|
||||
The final direct-IP test used Xray 26.3.27. The client-side `allowInsecure` option was not used because this Xray version reports that the option was removed.
|
||||
|
||||
## Secondary Vultr IP
|
||||
|
||||
Before removal, the Nginx upstream on VM 213 was switched from `104.238.177.67` to `95.179.252.111`. A post-switch end-to-end test succeeded, with outbound IP `95.179.252.111` and approximately 6.0 MiB/s.
|
||||
|
||||
No Vultr IP deletion was performed in this work. The secondary address was only confirmed as no longer referenced by the transit configuration.
|
||||
|
||||
## Scope audit
|
||||
|
||||
- No repository source/configuration files were edited before this record.
|
||||
- `git status` was clean before this documentation file was created.
|
||||
- This documentation file is the only workspace file created by the current documentation action.
|
||||
- Server-side files were changed on VM 213 and earlier on Vultr; temporary test files were also created on VM 3060.
|
||||
- No commit was created for this record.
|
||||
|
||||
## Important limitations
|
||||
|
||||
The measurements prove the route worked at test time. They do not guarantee permanent availability: NUBES, Vultr, upstream providers, or network filtering policy can change independently.
|
||||
|
||||
## Later the same day: optimisation attempt and its outcome
|
||||
|
||||
### Automation created
|
||||
|
||||
A reusable, idempotent tool was created outside this repository:
|
||||
|
||||
```text
|
||||
/home/naeel/nubes/HowTo/vpn-transit/vpn-setup.sh check | apply | verify | passthrough | verify-passthrough | client-config | rollback
|
||||
/home/naeel/nubes/HowTo/vpn-transit/client-config.json generated client config (chmod 600, contains UUID)
|
||||
/home/naeel/nubes/HowTo/vpn-transit/README.md description, measurements, rollback
|
||||
/home/naeel/nubes/HowTo/howto-vpn-transit-213-vultr-2026-09-28.md full report
|
||||
```
|
||||
|
||||
Every change is preceded by a timestamped backup and followed by a config test (`nginx -t`, `xray run -test`) with automatic rollback on failure.
|
||||
|
||||
### Changes applied
|
||||
|
||||
| Host | File | Change | Backup |
|
||||
|---|---|---|---|
|
||||
| 213 | `/etc/nginx/sites-available/check.kube5s.ru` | `proxy_buffering off;` added inside `location /ws`, marked `# vpn-transit: proxy_buffering off` | `check.kube5s.ru.bak.1790601681` |
|
||||
| Vultr | `/etc/v2ray-agent/xray/conf/00_log.json` | `loglevel`: `debug` → `warning` (log had grown to 76 MB), service restarted | `00_log.json.bak.1790601723` |
|
||||
| 213 | `/usr/local/sbin/vpn-transit-dnat.sh`, `/etc/systemd/system/vpn-transit-dnat.service` | DNAT `213:8443 → 95.179.252.111:443` plus FORWARD rules, enabled at boot | none (rules tagged `vpn-transit`) |
|
||||
|
||||
### Measurements after the changes
|
||||
|
||||
- Outbound IP: `95.179.252.111`
|
||||
- Throughput: `5.6–7.3 MiB/s` (10 MiB in 1.4–1.9 s)
|
||||
- Per-connection latency: `0.23–0.37 s`
|
||||
- WebSocket upgrade success rate on 213: `14569 / 14573` (99.97%), one `upstream timed out` error
|
||||
|
||||
### Hypothesis that was disproved: mux
|
||||
|
||||
`verify` compared the tunnel with and without `"mux": {"enabled": true, "concurrency": 8}`:
|
||||
|
||||
| Mode | 10 MiB download | Connection behaviour |
|
||||
|---|---|---|
|
||||
| without mux | 7.32 MiB/s in 1.43 s | stable |
|
||||
| with mux | **0 B/s, failed** | after 4 requests connections hang for 15 s |
|
||||
|
||||
Conclusion: mux is harmful in the `VLESS + WebSocket behind nginx` combination. It is excluded from the client config. The test remains in the script for re-checking on future Xray versions.
|
||||
|
||||
### Optimisation that could not be delivered: removing the second TLS layer
|
||||
|
||||
The intended speed fix was to drop one TLS handshake (`client → 213`, then `213 → Vultr`) by forwarding TCP straight through to Vultr.
|
||||
|
||||
- `ngx_stream_module.so` is absent on 213, so nginx cannot do SNI-based passthrough without installing `libnginx-mod-stream`.
|
||||
- Kernel-level DNAT on port 8443 was installed instead, but **does not work**: from outside, port 8443 returns `Connection refused` and the DNAT counter on 213 stays at 0 packets — traffic never reaches the machine.
|
||||
- Cause: the provider firewall in front of 213 exposes only ports 80 and 443. Measured from `3060`: `3001, 8080, 8443, 8766, 8767, 8888, 18080, 40229` are closed.
|
||||
- Therefore the second TLS layer can only be removed after the provider opens an additional port. The rules are already installed and would start working immediately once that happens.
|
||||
|
||||
### Errors made during this work
|
||||
|
||||
1. **Recommended `mux` before measuring it.** The recommendation was given as the main fix and was later disproved by measurement. Correct order: measure first, recommend after.
|
||||
2. **Changed server configuration before measuring the benefit.** `proxy_buffering off` has no effect on a WebSocket connection after the `101 Switching Protocols` upgrade, and `loglevel` affects only log size. Neither change improves speed, so from the user's point of view nothing changed.
|
||||
3. **Changed the client config to port 8443 before verifying the port was reachable from outside.** The config was regenerated back to port 443 immediately.
|
||||
|
||||
### Net result for the user
|
||||
|
||||
Nothing changed for the client: address `5.172.178.213`, port `443`, SNI `check.kube5s.ru`, path `/ws` and the UUID are unchanged, and the previously used link still works. No client-side reconfiguration is required.
|
||||
|
||||
The only actionable finding is client-side: the Xray log on Vultr contained **331** `connect: connection refused` to `127.0.0.1:45987`, i.e. the client requested a loopback address, plus Telegram advertises AAAA records while the tunnel is IPv4-only. The generated `client-config.json` addresses both (remote DNS, `queryStrategy: UseIPv4`), but the device itself was not modified.
|
||||
|
||||
Separately: **10170** `reset by peer` entries to `157.240.0.13` (Meta infrastructure) are blocking by those sites, unrelated to the transit.
|
||||
|
||||
### Scope audit (this action)
|
||||
|
||||
- Repository files changed: this document only. `git status` also showed unrelated pre-existing changes (`DEV_STAND/FullPipe/shturval.tf` deletion, `TMP/*` files) that were **not** touched or committed.
|
||||
- Server-side files changed: as listed in the table above.
|
||||
- Temporary test files on 3060: `/tmp/xray-test/*` (no permanent service installed).
|
||||
@@ -0,0 +1,109 @@
|
||||
# Правки ядра и модификаторов по итогам анализа 2026-09-30 (раунд Flash)
|
||||
|
||||
**Репо:** `/home/naeel/TF/tf_provider`. **Дата:** 2026-09-30.
|
||||
**Источник заданий:** `NOTES/20_prompts/prompt_for_deepseek_pro_remediation_plan.md`
|
||||
(гипотезы A1–A5, B1–B9) + диалог с Opus `HISTORY/OPUS/2026-09-30_provider_architecture_modifiers_dialog.md`.
|
||||
|
||||
---
|
||||
|
||||
## Что сделано (6 коммитов)
|
||||
|
||||
| Коммит | Пункт | Файлы | Суть |
|
||||
|---|---|---|---|
|
||||
| `047d53a` | C | `TOOLS/ARCHITECTURE.md` | Спека приведена к коду: ручные ресурсы, 401, удалённый `serviceSpecificModifiers`, раздел «Lifecycle Vocabulary». |
|
||||
| `c5a4499` | B3 | `TOOLS/scripts/check_hardcoded_service_ids.sh`, `org_ip_allocation_resource.go` | Страж сканирует `TOOLS/` + `provider/internal/` (кроме `resources_gen/`), второй паттерн — литеральный ref-svc id. `ResolveRefSvcParamValue(ctx, 19, …)` → константа `svcIDVcOrg`. |
|
||||
| `383f8ea` | A1 | `core/http.go`, `core/client_test.go`, `ARCHITECTURE.md` | 401 добавлен в `isRetryable` (действует для GET). Тест `TestIsRetryable`. |
|
||||
| `ea75cac` | B6 | `core/modifier_compare.go`, `operation_run_bycode.go`, `nsxt_snat_resource.go` | Idempotency pre-check сравнивает с **live** (`state.params`), а не с `paramValue` формы. `setSnat` → `ByIdempotent`. Тест `TestModifierDesiredEqualsLive`. |
|
||||
| `8519ba0` | B7 | `org_ip_allocation_resource.go`, `nsxt_snat_resource.go`, `org_ip_allocation_test.go` | `ImportState` заполняет Required (`vip_configure` из live/`[]`; `ip_space_name` из live/`no-needed`). Тесты на чистые хелперы. |
|
||||
| `0e26e98` | — | `core/refsvc.go`, `docs/60_strategy/terraform_case_sensitivity_fix.md` | Убран устаревший комментарий про несуществующий блок «Restore user-provided casing»; §4 помечен как исторический. |
|
||||
|
||||
**Проверка после каждого коммита:** `go build ./...` OK, `go test ./internal/... -short` PASS,
|
||||
`bash TOOLS/scripts/check_hardcoded_service_ids.sh` → OK.
|
||||
|
||||
**Бэкапы:** `TMP/backup_2026-09-30/` (исходные версии всех правимых файлов).
|
||||
|
||||
---
|
||||
|
||||
## Что ОТКЛОНЕНО после проверки по коду (важно)
|
||||
|
||||
- **A5 (нормализация регистра в `Read`) — был бы РЕГРЕССОМ.**
|
||||
Принятое решение (проверено): state хранит регистр **пользователя**; ref_svc-атрибуты **исключены
|
||||
из read-back** (шаблон `instance.go` добавляет `InputField` только при `eq .RefSvcId 0`); UUID
|
||||
внутри JSON нормализуются при **отправке** (`resources_core.BuildJSON` →
|
||||
`jsonutil.LowercaseUUIDsInText`). См. `docs/60_strategy/terraform_case_sensitivity_fix.md` §4 (пометка),
|
||||
§10–§11. Нормализация state к lowercase сломала бы соответствие plan=config.
|
||||
- **A3 (не обрывать modify при сбое live) — осознанная защита, а не дефект.**
|
||||
`instanceLiveParams` намеренно возвращает ошибку: тихий fallback на `paramValue` (дефолт ФОРМЫ)
|
||||
возвращает reset-баг (затирание параметров инстанса, HAR/edge_.har: `needEnableAVI`). Требуется
|
||||
отдельное решение (см. Q2 промпта раунда 4).
|
||||
- **A4 (угадывание типа по подстроке имени) — нужен замер.**
|
||||
Fallback применяется только к required-параметру без `paramValue`/`defaultValue`
|
||||
(`instance_create.go:105-113`) и при досылке modify. Гарантированного улучшения нет, риск сломать
|
||||
больше, чем починить. Оставлено как есть.
|
||||
|
||||
---
|
||||
|
||||
## Отложено
|
||||
|
||||
- **A2 — retry POST.** Слепой ретрай создающего `POST /instanceOperations` опаснее обрыва
|
||||
(дубликат операции). Решение — за владельцем (варианты в промпте раунда 4, Q1).
|
||||
- **B8** — создавать ли оверлей `modifiers.yaml` или узаконить ручные модификаторы категорией в спеке.
|
||||
- **B9** — единый словарь жизненного цикла (в спеку внесён как незакрытый вопрос; решение — Q4 промпта).
|
||||
|
||||
---
|
||||
|
||||
## Артефакты
|
||||
|
||||
- Промпт раунда 4 для Opus: `NOTES/20_prompts/prompt_for_opus_remediation_round4.md`
|
||||
(5 коротких вопросов, лимит ответа ≤ 25 строк).
|
||||
- Ограничение сессии: чат с Opus по раундам 1–3 исчерпан по токенам → раунд 4 в новом чате.
|
||||
|
||||
---
|
||||
|
||||
## Замер Q3 (2026-09-30): безопасно ли угадывание типа по имени?
|
||||
|
||||
**Источник:** `generated/dev/resources_yaml/*.yaml` (40 файлов), поля `data_type` / `required`.
|
||||
**Метод:** подсчёт + эмуляция `normalizeUniversalValueV6` (ветка `nameHint`). Только чтение.
|
||||
|
||||
| Метрика | Значение |
|
||||
|---|---|
|
||||
| required-параметров всего | 852 |
|
||||
| из них с пустым `data_type` | 5 |
|
||||
| всего параметров с пустым `data_type` | 12 (~1.2 %) |
|
||||
| из них угадывание по имени даёт ≠ `""` | **1** — `1_dummy.yaml` (`jsonExample` → `{}`), тестовый сервис |
|
||||
|
||||
Required с пустым `data_type` (все получают `""`; угадывание не срабатывает):
|
||||
`120_clickhouse/delete:username`, `12_s3/create:resourceRealm`, `13_s3bucket/create:maxSize`,
|
||||
`151_k8s_openbao/create:policyName`, `28_vc_vm_v3/create:userLogin`.
|
||||
|
||||
**Вывод.** Гипотеза A4 («риск неверной типизации» из-за подстроки имени) на dev-спеках
|
||||
**не подтверждается**: для всех реальных сервисов угадывание по имени не срабатывает (итог `""`);
|
||||
единственный эффект — тестовый `1_dummy.jsonExample`. То есть правка косметическая (упрощение),
|
||||
а не исправление дефекта. Решение «снимать/оставлять» — за владельцем.
|
||||
|
||||
---
|
||||
|
||||
## Раунд 4 (Opus, новый чат) — решения и правки
|
||||
|
||||
Промпт: `NOTES/20_prompts/prompt_for_opus_remediation_round4.md` (5 вопросов, ответ ≤ 25 строк).
|
||||
Ответ получен; ниже — что принято и что сделано.
|
||||
|
||||
| Q | Решение | Статус |
|
||||
|---|---|---|
|
||||
| Q1 retry POST | Не ретраить. `POST /instanceOperations` не идемпотентен, `Idempotency-Key` у API нет. | Зафиксировано в `TOOLS/ARCHITECTURE.md` (коммит `7a6f665`) |
|
||||
| Q2 черновик операции | Отмены нет: `DELETE /instanceOperations/{uid}` отсутствует и в коде, и в HAR (проверено: `grep '"method": "DELETE"'` по `HAR/*.har` — ноль совпадений). Оставляем как есть, задокументировано. | `7a6f665` |
|
||||
| Q3 zero-value по имени | **Закрыт замером**: угадывание не срабатывает (см. выше) — не дефект. | замер `54f036e` |
|
||||
| Q4 словарь жизненного цикла | Единый контракт: `keep_on_destroy` + `suspend_on_destroy`; `delete_strategy` = маппинг (`noop_warn`→keep, `inverse`→destroy, `error`→валидация). | `7a6f665` |
|
||||
| Q5 осиротевший инстанс | **Исправлено** (критичный). Ядро возвращает `instanceUid` вместе с ошибкой после создания; шаблон пишет partial state. | `9da9766` |
|
||||
|
||||
**Q5 детали:** `core/instance_create.go` — все ошибки ПОСЛЕ получения `instanceUid` возвращают
|
||||
`instanceUid` (до создания — `""`); `templates/instance.go` — при `err != nil && id != ""` пишет
|
||||
`data.ID` + `resp.State.Set` перед `AddError`. Регенерация dev (`02` + `dev-materialize`) → фикс в
|
||||
40 файлах `resources_gen` (эфемерные, не в git). Тесты: `TestCreateGenericInstance_KeepsUIDWhenOperationCreateFails`,
|
||||
`TestCreateGenericInstance_EmptyUIDWhenInstanceCreateFails`.
|
||||
|
||||
**Коммиты раунда 4:** `54f036e` (замер), `9da9766` (Q5), `7a6f665` (Q1/Q2/Q4).
|
||||
|
||||
**Осталось:** замер владельцем (`terraform plan` ×2 на `DEV_STAND/FullPipe`); B8 (`modifiers.yaml`).
|
||||
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
# 2026-09-30 — Очистка dev-реестра: удалены версии провайдера старше 2.0.21
|
||||
|
||||
> Команда владельца: «в деве — сотри ФИЗИЧЕСКИ все провайдеры старше 21 версии».
|
||||
> Операция **необратимая** (бакет без версионирования), выполнена 2026-09-30.
|
||||
|
||||
## Что и где
|
||||
|
||||
| Параметр | Значение |
|
||||
|---|---|
|
||||
| Хранилище | S3 `https://s3.msk-1.ngcloud.ru`, бакет `nubes-terraform-registry` |
|
||||
| Префикс | `tf-registry.containerk8s.services.ngcloud.ru/nubes-dev/nubes/` |
|
||||
| Стенд | **только dev** (`nubes-dev`); `nubes-test` и `nubes` не тронуты |
|
||||
| Инструмент | `mc` (`/usr/bin/mc`), alias `prod-s3`, `--api S3v4`, креды из `secrets/.s3cfg_provider` |
|
||||
| Версионирование бакета | `un-versioned` — удаление физическое, без «теневых» копий |
|
||||
|
||||
Состав одной версии — 5 объектов:
|
||||
|
||||
```
|
||||
terraform-provider-nubes_<v>_darwin_amd64.zip (~13 MiB)
|
||||
terraform-provider-nubes_<v>_linux_amd64.zip (~13 MiB)
|
||||
terraform-provider-nubes_<v>_windows_amd64.zip (~13 MiB)
|
||||
terraform-provider-nubes_<v>_SHA256SUMS
|
||||
terraform-provider-nubes_<v>_SHA256SUMS.sig
|
||||
```
|
||||
|
||||
## Было → стало
|
||||
|
||||
| | До | После |
|
||||
|---|---|---|
|
||||
| Версий | 25 (`2.0.0` … `2.0.24`) | **4** (`2.0.21`, `2.0.22`, `2.0.23`, `2.0.24`) |
|
||||
| Объектов | 125 | **20** |
|
||||
| Объём (zip) | ~975 MiB | ~156 MiB |
|
||||
|
||||
## Удалено (21 версия, 105 объектов)
|
||||
|
||||
```
|
||||
2.0.0 2.0.1 2.0.2 2.0.3 2.0.4 2.0.5 2.0.6 2.0.7
|
||||
2.0.8 2.0.9 2.0.10 2.0.11 2.0.12 2.0.13 2.0.14 2.0.15
|
||||
2.0.16 2.0.17 2.0.18 2.0.19 2.0.20
|
||||
```
|
||||
|
||||
Каждая версия: 5 объектов (3 zip ~13 MiB + `SHA256SUMS` + `SHA256SUMS.sig`).
|
||||
|
||||
Команда (по версии):
|
||||
|
||||
```bash
|
||||
mc rm --recursive --force \
|
||||
"prod-s3/nubes-terraform-registry/tf-registry.containerk8s.services.ngcloud.ru/nubes-dev/nubes/<v>/"
|
||||
```
|
||||
|
||||
Перед удалением снят полный манифест (125 строк):
|
||||
|
||||
```bash
|
||||
mc ls -r "prod-s3/nubes-terraform-registry/tf-registry.containerk8s.services.ngcloud.ru/nubes-dev/nubes/"
|
||||
```
|
||||
|
||||
## Оставлено
|
||||
|
||||
```
|
||||
2.0.21 2.0.22 2.0.23 2.0.24 (20 объектов, 4 × 5)
|
||||
```
|
||||
|
||||
Актуальная версия dev — `2.0.24` (см. `VERSIONS.md`).
|
||||
|
||||
## Проверки после удаления
|
||||
|
||||
| Проверка | Результат |
|
||||
|---|---|
|
||||
| `mc ls` префикса dev | только `2.0.21/`, `2.0.22/`, `2.0.23/`, `2.0.24/` |
|
||||
| `mc ls -r` (всего объектов) | 20 (по 5 на версию) |
|
||||
| API `…/nubes-dev/nubes/versions` | `['2.0.21','2.0.22','2.0.23','2.0.24']`, count 4 |
|
||||
| Скачивание `2.0.24`/`2.0.23` (linux/amd64) | HTTP **206**, ZIP-магия `50 4b 03 04` |
|
||||
| Скачивание `2.0.20`/`2.0.0` (linux/amd64) | HTTP **404** (объекта нет) |
|
||||
| Контроль: `nubes-test` | `3.0.0` — не тронуто |
|
||||
| Контроль: `nubes` (prod) | `1.0.0` — не тронуто |
|
||||
|
||||
> Примечание: эндпоинт `…/<v>/download/<os>/<arch>` отдаёт метаданные (JSON с `download_url`)
|
||||
> **не проверяя наличие объекта** — статус 200 у него ничего не доказывает. Фактическая
|
||||
> доступность проверяется загрузкой по `download_url` (как в таблице выше).
|
||||
|
||||
## Риски и восстановление
|
||||
|
||||
- ⛔ **Резервные копии zip не делались** — по прямому указанию «стереть физически»
|
||||
(плюс канал до S3 из локальной сети медленный). Восстановление возможно **только
|
||||
пересборкой** нужной версии из git-истории пакета;
|
||||
`download_url`/`SHA256SUMS` удалённых версий не сохранялись.
|
||||
- Пользователи, закрепившие в dev-стендах версии `< 2.0.21`, получат 404 при `terraform init`
|
||||
и должны перейти на `2.0.21+`.
|
||||
- `test` и `prod` не затронуты.
|
||||
@@ -0,0 +1,60 @@
|
||||
# 2026-09-30 — Перезаливка провайдера во все три стенда под версиями 1.0.0 / 2.0.0 / 3.0.0
|
||||
|
||||
> Команда владельца: «надо — чтобы в 1.0.0 2.0.0 3.0.0 стали НОВЫЕ провайдеры…
|
||||
> ПОХУЙ на пользователей! ПОХУЙ на старые версии!!! генери всё новое и ЗАЛИВАЙ».
|
||||
|
||||
## Что сделано
|
||||
|
||||
Полный цикл по каждому стенду: перегенерация (`01` YAML → `02` Go+доки) и
|
||||
сборка+подпись+заливка (`03`) — всё одной командой `03` (она сама вызывает `01` и `02`).
|
||||
|
||||
| Стенд | Namespace | Версия | `VERSION` в profile.env | Результат |
|
||||
|---|---|---|---|---|
|
||||
| prod | `nubes` | `1.0.0` | `1.0.0` (без изменений) | `Done. Version 1.0.0 uploaded.` |
|
||||
| dev | `nubes-dev` | `2.0.0` | `2.0.24` → **`2.0.0`** | `Done. Version 2.0.0 uploaded.` |
|
||||
| test | `nubes-test` | `3.0.0` | `3.0.0` (без изменений) | `Done. Version 3.0.0 uploaded.` |
|
||||
|
||||
Команды:
|
||||
|
||||
```bash
|
||||
export MC_CONFIG_DIR=/tmp/mc-cfg
|
||||
./TOOLS/scripts/03_build_and_upload_provider.sh --profile TOOLS/config/dev 2.0.0
|
||||
./TOOLS/scripts/03_build_and_upload_provider.sh --profile TOOLS/config/test 3.0.0
|
||||
./TOOLS/scripts/03_build_and_upload_provider.sh --profile TOOLS/config/prod 1.0.0
|
||||
```
|
||||
|
||||
Что делает `03`: `01` (YAML-спеки стенда) → `02` (Go-ресурсы + доки) → сборка
|
||||
3 платформ (`linux/windows/darwin amd64`, `CGO_ENABLED=0`, `-ldflags=-X main.version=…`)
|
||||
→ `terraform-provider-nubes_<v>_SHA256SUMS` → GPG-подпись (`secrets/private_key.asc`,
|
||||
`CB3A0DF161ECC416`) → заливка 5 объектов в S3.
|
||||
|
||||
## Проверки после заливки
|
||||
|
||||
| Стенд | Версия | `linux/amd64` скачивание | sha256 залитого vs локальной сборки |
|
||||
|---|---|---|---|
|
||||
| dev | `2.0.0` | HTTP 200, 13 169 112 B | ✅ совпадает |
|
||||
| test | `3.0.0` | HTTP 200, 13 095 908 B | ✅ совпадает |
|
||||
| prod | `1.0.0` | HTTP 200, 13 103 414 B | ✅ совпадает |
|
||||
|
||||
Дополнительно:
|
||||
|
||||
- API `/versions`: `nubes-dev` → `['2.0.0','2.0.21','2.0.22','2.0.23','2.0.24']`,
|
||||
`nubes-test` → `['3.0.0']`, `nubes` → `['1.0.0']`;
|
||||
- в S3 у каждой версии ровно 5 объектов (3 zip + `SHA256SUMS` + `SHA256SUMS.sig`).
|
||||
|
||||
## ⚠️ Последствия (приняты владельцем сознательно)
|
||||
|
||||
- Версии `1.0.0`, `2.0.0`, `3.0.0` **перезаписаны** — под теми же номерами теперь
|
||||
другие бинарники. У всех, у кого есть `.terraform.lock.hcl`, будет
|
||||
`checksum mismatch` при `terraform init`; лечится `terraform init -upgrade`.
|
||||
- Старые версии **не удалялись** (кроме ранее вычищенного dev `< 2.0.21`):
|
||||
в dev остаются `2.0.21`–`2.0.24`, в test `3.0.0` и в prod `1.0.0` — теперь уже как
|
||||
свежие сборки.
|
||||
- `VERSION` в `TOOLS/config/dev/profile.env` понижен `2.0.24` → `2.0.0`
|
||||
(чтобы доки и артефакты генерировались с новой версией); закоммичено.
|
||||
|
||||
## Связанные документы
|
||||
|
||||
- `VERSIONS.md` — обновлённая таблица текущих версий.
|
||||
- `HISTORY/2026-09-30_dev_registry_prune_versions.md` — предыдущая очистка dev-реестра.
|
||||
- `HISTORY/2026-09-30_yaml_pipeline_hardening.md` — состояние пайплайна генерации.
|
||||
@@ -0,0 +1,130 @@
|
||||
# 2026-09-30 — Регистр UUID: нормализация на ОТПРАВКЕ в API (create/modify/redeploy)
|
||||
|
||||
> Разбор: `docs/60_strategy/terraform_case_sensitivity_fix.md` §11 (главный документ по теме),
|
||||
> `NOTES/30_analysis/ARCHITECTURE_NEW.md` §6.5.
|
||||
|
||||
## Что обнаружилось
|
||||
|
||||
Костыль `lower(...)` в конфиге стенда Штурвала — **не «просто проще», а обязателен**.
|
||||
Без него `terraform apply` (create кластера) падает: платформа отвечает
|
||||
«Edge не развёрнут в указанном vDC».
|
||||
|
||||
Обнаружено при запуске Terraform **из-под Windows**, на провайдере **2.0.23**
|
||||
(то есть после всех «фиксов регистра», выпущенных 24.09).
|
||||
|
||||
Костыль живёт в примере (и в gitea `Nail/tf_examples`):
|
||||
|
||||
```hcl
|
||||
# tf_examples/fullpipe_chain/shturval.tf:139-140
|
||||
vdc_uid = lower(nubes_vc_vdc.vdc.id)
|
||||
nsxt_uid = lower(nubes_vc_nsxt.edge.id)
|
||||
```
|
||||
|
||||
## Почему прошлые фиксы не помогли (главная мысль)
|
||||
|
||||
Провайдер `2.0.23` нормализует регистр UUID **только при СРАВНЕНИИ**:
|
||||
план vs state, adopt/suspend/resume, modifier-compare, диагностика
|
||||
(`jsonutil.LowercaseUUIDsInText` → `JSONStringsEquivalent`, `JsonNormalize()`,
|
||||
`ParamsMatchForResume`, `normalizeCompareValue`).
|
||||
|
||||
**Путь ОТПРАВКИ в API остался без нормализации.** Все map-fixed JSON-параметры
|
||||
собираются одной функцией `resources_core.BuildJSON`
|
||||
(`provider/internal/resources_core/helpers.go`), а её вызывает сгенерированный код
|
||||
(`NestedJSONExpr`, шаблон `TOOLS/resource-generator/internal/templates/instance.go`,
|
||||
ветки Create / Modify / Redeploy). `BuildJSON` берёт `ValueString()` подполей **как есть**.
|
||||
|
||||
Ресурс `nubes_vc_nsxt` отдаёт `id` в UPPERCASE (`2C37FED1-…`), платформа хранит
|
||||
UUID в lowercase и **сравнивает регистр при create** → `startupConfiguration.nsxtUid`
|
||||
в верхнем регистре отвергается.
|
||||
|
||||
Почему не спас `resolveRefSvcParamValues` (`core/refsvc.go`,
|
||||
`core/refsvc_resolve.go`): он нормализует только **top-level** refSvc-параметры и
|
||||
`s3.*uid` **внутри** map-fixed. `vdcUid`/`nsxtUid` — обычные строковые подполя
|
||||
JSON, refSvcId у них нет, под шаблон `s3.*uid` они не подпадают.
|
||||
|
||||
## Что сделано
|
||||
|
||||
| Файл | Изменение |
|
||||
|---|---|
|
||||
| `provider/internal/resources_core/helpers.go` | `BuildJSON` оборачивает результат в `jsonutil.LowercaseUUIDsInText(...)` (+ импорт `core/jsonutil`, комментарий-обоснование) |
|
||||
|
||||
Одна точка → покрыты **все** map-fixed-параметры всех ресурсов на
|
||||
create / modify / redeploy (19 сгенерированных ресурсов, `resources_gen/`).
|
||||
Регенерация не требуется (логика сериализации одна).
|
||||
|
||||
## Оценка риска (почему это безопасно)
|
||||
|
||||
- `BuildJSON` используется **только для отправки** в API, не для построения state.
|
||||
- Regex `uuidAnywhereRegex` = `[0-9a-f]{8}-xxxx-xxxx-xxxx-xxxxxxxxxxxx` — совпадает
|
||||
только с UUID; пароли/имена/произвольные строки не задевает.
|
||||
- Проверено по спекам: внутри map-fixed **нет** строковых секретных полей
|
||||
(password/secret/token) — только `*Uid`-ссылки на ресурсы.
|
||||
- Это **выравнивание** с уже принятым в провайдере правилом «регистр UUID незначим»
|
||||
(то же приведение уже делается на сравнении), а не новое поведение.
|
||||
|
||||
Остаточный риск: если в map-fixed когда-нибудь появится строковое поле, где
|
||||
пользователь хранит **свой** UUID, и регистр там семантически важен (не ссылка на
|
||||
ресурс) — он будет приведён к lowercase. Сейчас таких полей нет.
|
||||
|
||||
## Следствия
|
||||
|
||||
- `lower(...)` в HCL становится **не нужен** — убирать в конфигах и в примере
|
||||
(отдельной командой, после релиза провайдера).
|
||||
- **Выпущено 30.09.2026: dev `2.0.24`** — собрано (linux/windows/darwin amd64),
|
||||
подписано GPG и залито в реестр (`nubes-dev/nubes/2.0.24/`), версия видна
|
||||
в `/v1/providers/nubes-dev/nubes/versions`. `VERSIONS.md` обновлён.
|
||||
Доки в реестр (шаг `04_build_and_publish_docs.sh`) **не публиковались**.
|
||||
- В локальных стендах костыля нет: `DEV_STAND/FPipeGmail/shturval.tf:125-126` и
|
||||
`DEV_STAND/FullPipe/shturval.tf1:123` передают `nubes_vc_vdc.vdc.id` /
|
||||
`nubes_vc_nsxt.edge.id` напрямую → на create у них тот же риск.
|
||||
|
||||
## Процедура релиза (dev) — воспроизводимо (проверено 30.09.2026)
|
||||
|
||||
1. Поднять `VERSION` в `TOOLS/config/dev/profile.env` и закоммитить
|
||||
(иначе доки генерируются со старой версией).
|
||||
2. Собрать и залить:
|
||||
```bash
|
||||
./TOOLS/scripts/03_build_and_upload_provider.sh --profile TOOLS/config/dev 2.0.24
|
||||
```
|
||||
Скрипт сам выполняет шаги `01` + `02`, собирает 3 платформы
|
||||
(linux/windows/darwin amd64), подписывает GPG и заливает в S3.
|
||||
3. Проверить публикацию:
|
||||
```bash
|
||||
curl -s https://tf-registry.containerk8s.services.ngcloud.ru/v1/providers/nubes-dev/nubes/versions
|
||||
```
|
||||
4. Обновить `VERSIONS.md` и закоммитить.
|
||||
5. Для локального `go build`/`go test`: `02_generate_resources_and_docs_v2.sh --profile TOOLS/config/dev`
|
||||
и `TOOLS/scripts/dev-materialize.sh dev` (эфемерная копия в `provider/`).
|
||||
|
||||
⚠️ Доки в реестр (шаг `04_build_and_publish_docs.sh`) в этом релизе **не публиковались**.
|
||||
|
||||
## Где нормализация нужна (карта, чтобы не потерять)
|
||||
|
||||
Нормализация регистра UUID нужна в **двух независимых местах**:
|
||||
|
||||
1. **Сравнение** (план ↔ state, adopt, suspend/resume, modifier-compare, диагностика):
|
||||
`jsonutil.LowercaseUUIDsInText` → `JSONStringsEquivalent`, `JsonNormalize()`,
|
||||
`ParamsMatchForResume`, `normalizeCompareValue`.
|
||||
2. **Отправка в API** — единственная точка `resources_core.BuildJSON`
|
||||
(`provider/internal/resources_core/helpers.go`), вызывается сгенерированным кодом
|
||||
через `NestedJSONExpr` (`TOOLS/resource-generator/internal/templates/instance.go`:
|
||||
Create ~302, Modify ~488, Redeploy ~505).
|
||||
|
||||
Правила:
|
||||
|
||||
- ⛔ `lower(...)` в HCL — костыль, а не решение (был нужен только из-за ненормализованной отправки).
|
||||
- ⛔ Не нормализовать план целиком (скаляры→строки, сортировка ключей) — вечный diff;
|
||||
менять только регистр UUID-подстрок.
|
||||
- `resolveRefSvcParamValues` (`core/refsvc.go`) покрывает только top-level `refSvcId`
|
||||
и `s3.*uid` внутри map-fixed; `vdcUid`/`nsxtUid` — нет.
|
||||
- Спеки map-fixed без строковых секретов (только `*Uid`) → regex `uuidAnywhereRegex` безопасен.
|
||||
|
||||
## Открытые вопросы (не закрыты)
|
||||
|
||||
1. Проверить на живом стенде: create кластера Штурвала **без** `lower(...)` на сборке
|
||||
с этим фиксом — `apply` запускает только пользователь.
|
||||
2. `core/params.go` → `normalizeUniversalValueV6`: скалярные UUID, попадающие в
|
||||
дефолты create (`instance_create.go`) и в досылку modify (`operation_run.go`),
|
||||
к lowercase не приводятся (вторично, нужен замер).
|
||||
3. `resources_core/ref_validation.go` (`ValidateRefParamsOnAdopt`): ref-параметр
|
||||
внутри JSON не валидируется при adopt (открыто с 24.09).
|
||||
@@ -0,0 +1,258 @@
|
||||
# 2026-09-30 — Ужесточение пайплайна генерации YAML (безопасная замена, чистка легаси)
|
||||
|
||||
> Связанные материалы:
|
||||
> `TOOLS/README.md` (канонический пайплайн, порядок шагов),
|
||||
> `TOOLS/ARCHITECTURE.md` (спецификация),
|
||||
> память репозитория: `pipeline-legacy.md`.
|
||||
|
||||
## Задача
|
||||
|
||||
1. Разобрать, что в генерации YAML устарело.
|
||||
2. Перегенерировать YAML по всем стендам так, чтобы **старое удалялось безопасно**,
|
||||
а новое создавалось атомарно.
|
||||
3. Задокументировать всё, чтобы история изменений прослеживалась.
|
||||
|
||||
Команда пользователя: «делай как ПОЛОЖЕНО, как в best practices».
|
||||
|
||||
## Что было не так (до правок)
|
||||
|
||||
### `TOOLS/scripts/01_generate_yamls.sh`
|
||||
|
||||
| Место (до) | Проблема |
|
||||
|---|---|
|
||||
| стр. 230 `rm -f "$output_glob"` | старый YAML удалялся **до** генерации → при сбое API файл исчезал, новый не создавался (неатомарно per-service) |
|
||||
| стр. 122 «Полной очистки нет» | YAML исключённого сервиса оставался в каталоге и попадал в сборку |
|
||||
| стр. 91 `ls -t "${ROOT_DIR}"/*.token` | легаси-фолбэк токена: в корне токенов нет; поиск «последнего» мог подхватить чужой токен |
|
||||
| стр. 109 `API_ENDPOINT="${NUBES_API_ENDPOINT:-https://lk-api-gateway.ngcloud.ru/...}"` | молчаливый уход в **PROD**, если в профиле нет endpoint |
|
||||
| стр. 167 `svc_name=""` | имя всегда пустое, хотя шапка обещала парсинг из списка → лишний HTTP-запрос на каждый сервис (2 запроса вместо 1) |
|
||||
| стр. 46–50 | дубль `SERVICES_FILE_DEFAULT` (одинаковое присваивание в `if`) |
|
||||
| шапка vs код | «REQUEST_DELAY по умолчанию 0.2», в коде 0.5; путь вывода указан как `provider/resources_yaml` (устарел) |
|
||||
|
||||
### `TOOLS/yaml-generator/internal/config/config.go`
|
||||
|
||||
| Место (до) | Проблема |
|
||||
|---|---|
|
||||
| `Load()` стр. 33 | свой дефолт endpoint = **PROD** gateway |
|
||||
| `loadToken()` + `findLatestToken()` | легаси-фолбэк «последний `*.token` в корне репо» |
|
||||
| `Load()` стр. 60 | путь `filepath.Join(repoRoot, "devops", "config", "services_list.txt")`, причём `FindRepoRoot()` возвращает каталог `provider/` → путь заведомо не существовал |
|
||||
|
||||
### Легаси-скрипты
|
||||
|
||||
`10_yaml_stability_run.sh`, `11_yaml_stability_run_latest.sh`,
|
||||
`12_generate_yamls_latest.sh`, `13_generate_yamls_clean.sh`,
|
||||
`02_generate_resources_and_docs_template_v2.sh` — **мертвы**: зовут `01` без
|
||||
`--profile` (→ `exit 2`), ищут `*.token` в корне репо, а `13` вдобавок делал
|
||||
`rm -f provider/resources_yaml/*.yaml`. Ни один рабочий скрипт их не вызывает
|
||||
(ссылки есть только в исторических `HISTORY/`, `NOTES/`).
|
||||
|
||||
## Что сделано
|
||||
|
||||
### 1. `01_generate_yamls.sh` — безопасная запись по принципу staging → атомарная замена
|
||||
|
||||
Новый алгоритм:
|
||||
|
||||
```
|
||||
staging = generated/<stand>/resources_yaml.staging.<pid>
|
||||
↓ генерация всех сервисов пачкой per-service в staging
|
||||
↓ при пустом failures:
|
||||
mv resources_yaml → resources_yaml.bak-<UTC> (бэкап, ротация KEEP_BACKUPS=5)
|
||||
mv staging → resources_yaml (атомарный rename в том же FS)
|
||||
↓ при непустом failures:
|
||||
замена ОТМЕНЯЕТСЯ, рабочий каталог не тронут, staging оставлен для разбора, exit 1
|
||||
```
|
||||
|
||||
Прочие изменения:
|
||||
|
||||
- каталог помечается маркером `.stand`; генерация в каталог чужого стенда запрещена (`exit 2`);
|
||||
- `embed.go` создаётся теперь в staging (обязательный `go:embed *.yaml`);
|
||||
- `NUBES_API_ENDPOINT` обязателен, иначе `exit 2`;
|
||||
- токен берётся только из `NUBES_API_TOKEN`/`TOKEN_FILE`; легаси-поиск удалён;
|
||||
- имя сервиса — из 2-го поля `services_list.txt`; лишний python-запрос удалён;
|
||||
- удалён дубль `SERVICES_FILE_DEFAULT`; синхронизированы комментарии.
|
||||
|
||||
### 2. `yaml-generator/internal/config/config.go`
|
||||
|
||||
- `NUBES_API_ENDPOINT` обязателен (нет PROD-дефолта);
|
||||
- `loadToken()` больше не ищет `*.token` в корне репо; `findLatestToken` и `getenvDefault` удалены как мёртвые;
|
||||
- при отсутствии `NUBES_SERVICE_ID` требуется явный `NUBES_SERVICES_FILE` (угадывание пути удалено).
|
||||
|
||||
### 3. Легаси-скрипты отключены (fail-fast)
|
||||
|
||||
В начало каждого добавлен guard: сообщение `DEPRECATED` + `exit 2`. Файлы **не удалены**
|
||||
(удаление — отдельное решение владельца), но теперь они не могут сделать ничего вредного.
|
||||
|
||||
### 4. Документация
|
||||
|
||||
- `TOOLS/README.md` — добавлен раздел «Канонический пайплайн (порядок шагов)» и описание безопасной генерации;
|
||||
- `TOOLS/ARCHITECTURE.md` — ссылки `devops/…` заменены на `TOOLS/config/<stand>/…`;
|
||||
- память репозитория — `pipeline-legacy.md` уточнена.
|
||||
|
||||
## Прогон по всем стендам (результат)
|
||||
|
||||
Токены проверены прямыми запросами к API (с браузерным `User-Agent`, иначе DDoS-Guard отдаёт 403):
|
||||
|
||||
| Стенд | Endpoint | HTTP | YAML после генерации | Stale | Failures |
|
||||
|---|---|---|---|---|---|
|
||||
| dev | `lk-api-gateway-dev.ngcloud.ru` | 200 | 40 | нет | нет |
|
||||
| test | `lk-api-gateway-test.ngcloud.ru` | 200 | 36 | нет | нет |
|
||||
| prod | `lk-api-gateway.ngcloud.ru` | 200 | 35 | нет | нет |
|
||||
|
||||
Команды:
|
||||
|
||||
```bash
|
||||
./TOOLS/scripts/01_generate_yamls.sh --profile TOOLS/config/dev
|
||||
./TOOLS/scripts/01_generate_yamls.sh --profile TOOLS/config/test
|
||||
./TOOLS/scripts/01_generate_yamls.sh --profile TOOLS/config/prod
|
||||
```
|
||||
|
||||
Проверка целостности: `diff -rq` нового каталога dev с бэкапом даёт различия только
|
||||
в случайных `default`-суффиксах, которые API генерирует при каждом запросе
|
||||
(`db-ievgpdvu` → `db-ujama5rb`, `flask-seqtiq3t` → `flask-xwfdxdqh` и т.п.) —
|
||||
структурной регрессии нет. Это же объясняет, почему побайтовое сравнение двух
|
||||
прогонов не может быть использовано как «детектор дрейфа».
|
||||
|
||||
## Коммиты
|
||||
|
||||
- `12b3932` — `fix(tools): безопасная генерация YAML — staging + атомарная замена, без легаси-фолбэков`
|
||||
- `ad4daab` — `chore(tools): легаси-скрипты генерации отключены (fail-fast DEPRECATED)`
|
||||
|
||||
## Проверки
|
||||
|
||||
- `bash -n` для `01_generate_yamls.sh` и всех guard-скриптов — OK;
|
||||
- `go vet ./...` + `go build` для `yaml-generator` — OK;
|
||||
- guard отдаёт `exit 2`;
|
||||
- прогон dev/test/prod — 0 failures, stale отсутствует, staging не остаётся;
|
||||
- бэкапы создаются: `generated/dev/resources_yaml.bak-20260930T063152Z` и т.д.
|
||||
|
||||
## Этап 2 — удаление мёртвого (по команде «удаляй всё старое, аккуратно»)
|
||||
|
||||
**Удалено (`1e796c8`)** — 100% мёртвый код/данные, ничего их не вызывает:
|
||||
|
||||
| Файл | Почему удалён |
|
||||
|---|---|
|
||||
| `TOOLS/scripts/10_yaml_stability_run.sh` | зовёт `01` без `--profile` (exit 2), ищет `*.token` в корне |
|
||||
| `TOOLS/scripts/11_yaml_stability_run_latest.sh` | цепочка на `10`, та же поломка |
|
||||
| `TOOLS/scripts/12_generate_yamls_latest.sh` | зовёт `01` без `--profile`, ищет `*.token` в корне |
|
||||
| `TOOLS/scripts/13_generate_yamls_clean.sh` | цепочка на `12` + делал `rm -f provider/resources_yaml/*.yaml` |
|
||||
| `TOOLS/scripts/02_generate_resources_and_docs_template_v2.sh` | легаси-дубль канонического `02_generate_resources_and_docs_v2.sh` |
|
||||
| `TOOLS/config/services_list.txt` (общий) | код его не читает; как «объединение» устарел: активный `27` (в test/prod — «нет в UI»), нет `87/88/97/153`, которые есть в dev |
|
||||
|
||||
Проверка «ничего не вызывает»: `grep` по всему репо находил ссылки только в
|
||||
исторических `HISTORY/`, `NOTES/`, `docs/` (не исполняются).
|
||||
|
||||
**Правки ссылок (`c822ae2`)**: `README.md`, `HOW_TO/README.md`,
|
||||
`HOW_TO/DEVOPS_BUILD_PIPELINE.md`, `HOW_TO/HOWTO_ADD_NEW_SERVICE.md` (включая
|
||||
переписанный блок «Быстрый старт» с `devops/` на `./TOOLS/scripts/*`),
|
||||
`DOCS_PIPELINE/README.md`, `scripts/publish-doc-page.sh`, `.gitignore`.
|
||||
|
||||
**Проверка после удаления**: `bash -n` для всех `TOOLS/scripts/*.sh` и
|
||||
`scripts/publish-doc-page.sh` — OK; smoke-прогон `01 --profile TOOLS/config/dev` —
|
||||
40 YAML, замена атомарная, бэкап создан.
|
||||
|
||||
**Не удалено (осознанно):**
|
||||
|
||||
- поддержка легаси-прокси `index.cfm` в `01` и `yaml-generator` — это совместимость
|
||||
с работающими пользователями старого API (провайдер v5.0.75, `secrets/stands.md`);
|
||||
- `DOCS_PIPELINE/publish-docs.sh` — сам файл помечен «справочная копия, не подменяет пайплайн»;
|
||||
- `HISTORY/`, `NOTES/`, `docs/` — исторические документы (в них `devops/` и легаси-скрипты
|
||||
упоминаются как история, это нормально);
|
||||
- `scripts/*.py` и `s3_notification_example.sh` — ручные утилиты, вызываются вручную.
|
||||
|
||||
## Этап 3 — сверка списков стендов с облачным каталогом (источник истины)
|
||||
|
||||
Принято: **истина — то, что перечислено в облаке**. Определяется эндпоинтом каталога:
|
||||
|
||||
```bash
|
||||
# «перечислено в облаке» (продакшен-готовые сервисы стенда)
|
||||
GET {NUBES_API_ENDPOINT}/services?limit=200&isProductionReady=true
|
||||
# для сравнения: без фильтра отдаются ВСЕ сервисы платформы, включая
|
||||
# DEPRECATED и не заявленные в каталоге (48 у prod, 49 у test, 60 у dev)
|
||||
```
|
||||
|
||||
Требуется браузерный `User-Agent` (иначе DDoS-Guard отдаёт 403) и `Referer`.
|
||||
|
||||
Результат на 2026-09-30:
|
||||
|
||||
| Стенд | Облако (`isProductionReady=true`) | Активных в `services_list.txt` | Лишние в файле | Не хватало |
|
||||
|---|---|---|---|---|
|
||||
| dev | 40 | 40 | нет | нет |
|
||||
| test | 36 | 36 | нет | нет |
|
||||
| prod | 36 | 35 → **36** | нет | **`151 k8sOpenbao` (Vault)** |
|
||||
|
||||
У остальных 12 закомментированных prod-сервисов, присутствующих в API, `isProductionReady=false` —
|
||||
они закомментированы обоснованно. Четыре id в файле отсутствуют в каталоге prod вовсе
|
||||
(`32 vmpostgre`, `87 k8svalkey`, `153 nifi`, `175 k8sGo`).
|
||||
|
||||
Исправлено коммитом `a68a36a`: `151 k8sOpenbao` раскомментирован (комментарий «нет в PROD UI»
|
||||
устарел), prod перегенерирован — 36 YAML, ровно как в облаке.
|
||||
|
||||
## Этап 4 — перепроверка после полной перегенерации + подводные камни
|
||||
|
||||
Команда: «сгенери YAML для всех стендов, проследи чтобы старого ничего не осталось,
|
||||
перепроверь после генерации всё». Выполнено три прогона `01`:
|
||||
|
||||
```bash
|
||||
for s in dev test prod; do ./TOOLS/scripts/01_generate_yamls.sh --profile TOOLS/config/$s; done
|
||||
# все три: exit=0
|
||||
```
|
||||
|
||||
### Результат перепроверки (2026-09-30)
|
||||
|
||||
| Стенд | YAML | = активных в списке | = облако (`isProductionReady=true`) | Stale | Дубли id | Failures |
|
||||
|---|---|---|---|---|---|---|
|
||||
| dev | 40 | ✅ 40 | ✅ 40 | нет | нет | пусто |
|
||||
| test | 36 | ✅ 36 | ✅ 36 | нет | нет | пусто |
|
||||
| prod | 36 | ✅ 36 | ✅ 36 | нет | нет | пусто |
|
||||
|
||||
Дополнительно проверено:
|
||||
|
||||
- staging-каталоги (`resources_yaml.staging.*`) — не осталось ни одного;
|
||||
- в `resources_yaml/` только `*.yaml`, `.stand`, `embed.go` — посторонних файлов нет;
|
||||
- `.stand` в каждом каталоге совпадает с профилем (`dev`/`test`/`prod`);
|
||||
- бэкапы прошлых версий: dev 3, test 2, prod 2 (ротация `KEEP_BACKUPS=5`);
|
||||
- `generated/<стенд>/tmp/yaml_gen_failures.txt` — пусты;
|
||||
- `git status` — чисто.
|
||||
|
||||
### ⛔ Подводные камни, найденные при перепроверке (важно на будущее)
|
||||
|
||||
1. **API отдаёт случайные `default`.** Часть параметров приходит со случайным
|
||||
суффиксом (`db-ievgpdvu` → `db-ujama5rb`, `kvname-grzjes7l` → `kvname-g3s0uof2`,
|
||||
`flask-seqtiq3t` → `flask-xwfdxdqh`). Поэтому **побайтовое сравнение двух прогонов
|
||||
не является детектором дрейфа** — различия в этих строках не регрессия.
|
||||
2. **Случайный `default` вшивается в сгенерированный Go-код.**
|
||||
Пример: `generated/dev/go/151_k8s_openbao_kv_resource.go` содержит
|
||||
`Default: stringdefault.StaticString("kvname-XXXX")`. Следствие:
|
||||
`check_generated_drift.sh dev` показывает **дрейф 15 файлов сразу после любой**
|
||||
перегенерации YAML — это не ошибка оператора.
|
||||
3. **Производные артефакты стареют молча.** `generated/<стенд>/go` и `generated/<стенд>/docs`
|
||||
создаются шагом `02` и после нового `01` становятся старше своих источников
|
||||
(на момент проверки: `go`/`docs` dev — 08:46, YAML dev — 10:18). Отдельно живёт
|
||||
эфемерная копия `provider/internal/resources_gen` + `provider/resources_yaml`
|
||||
(её кладёт `dev-materialize.sh`, маркер `.stand` = стенд). Их нужно обновлять
|
||||
шагом `02` после каждого `01`.
|
||||
4. **Прямые HTTP-запросы к API без браузерного `User-Agent` получают 403**
|
||||
(DDoS-Guard). С `User-Agent` + `Referer` — 200.
|
||||
|
||||
### Актуальная карта пайплайна на 2026-09-30
|
||||
|
||||
- Единственный путь генерации YAML: `TOOLS/scripts/01_generate_yamls.sh --profile TOOLS/config/<стенд>`
|
||||
(`--profile` обязателен, без него `exit 2`).
|
||||
- Один универсальный движок на все стенды: `TOOLS/bin/yaml-generator`, стенд задаётся
|
||||
переменными окружения (`NUBES_API_ENDPOINT`, `NUBES_API_TOKEN`, `NUBES_SERVICE_ID`,
|
||||
`NUBES_SERVICE_NAME`, `NUBES_OUTPUT_DIR`); список сервисов — свой у каждого стенда.
|
||||
- Стенд-специфичных хардкодов в коде нет — контролируется `check_hardcoded_service_ids.sh`.
|
||||
- Токены: `secrets/{dev,test,prod}.token` (валидны на 2026-09-30, срок до 2026-12-27);
|
||||
обновление — `TOOLS/scripts/00_token_manager.sh` (keycloak refresh, `THRESHOLD_MIN=10`).
|
||||
- Удалены как мёртвые (`1e796c8`): `10/11/12/13_yaml_*.sh`,
|
||||
`02_generate_resources_and_docs_template_v2.sh`, общий `TOOLS/config/services_list.txt`.
|
||||
- Оставлены осознанно: поддержка легаси-прокси `index.cfm`, справочная копия
|
||||
`DOCS_PIPELINE/publish-docs.sh`, исторические `HISTORY/`/`NOTES/`/`docs/`,
|
||||
ручные утилиты `scripts/*.py`.
|
||||
|
||||
## Открытые вопросы (на решение владельца)
|
||||
|
||||
1. Поддержка легаси-прокси `index.cfm`: оставляем или выпиливаем (README уже помечает
|
||||
закрытые API как «не использовать»)?
|
||||
2. Прочие `.gitignore`-паттерны мёртвых каталогов (`universal_rebuild/*`, `provider/generated/`)
|
||||
— чистить?
|
||||
|
||||
@@ -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,933 @@
|
||||
# Диалог: Opus 4.8 — анализ архитектуры провайдера и ресурсов-модификаторов (2026-09-30)
|
||||
|
||||
> **Что это.** Полная, без сокращений, запись диалога: постановка задачи пользователем,
|
||||
> ход работы агента (GitHub Copilot) до создания файла-промпта, содержимое промпта,
|
||||
> ответ Opus 4.8 (приведён дословно в том виде, как получен) и открытый вопрос Opus.
|
||||
> **Формат.** Хронология ходов. Реплики пользователя и ответ Opus — дословно, включая опечатки.
|
||||
> **Статус.** Диалог НЕ завершён: Opus задал вопрос, ответ ему ещё не передан.
|
||||
|
||||
---
|
||||
|
||||
## Ход 1 — Задание пользователя (дословно)
|
||||
|
||||
```
|
||||
составь промпт в файл для Опус 4.8
|
||||
пусть проанализирует архитектуру провайдера ... с дополнительными ресурсами-модификаторами
|
||||
ограничь зону его доступносьти - пусть НИЧЕГО не читает из хистори, только файл с архитектурой
|
||||
и точно выдай ему список файлов которые надо изучить, более пусть никуда не лезет
|
||||
если что непончятно - пусть напишет тебе вопрос, я тебе передам
|
||||
взаимодейтвуйте в редиме диалога, НО НЕ НАДО злоупотреблять !
|
||||
ответы от него - МАКСИМАЛЬНО СЖАТО, без лирики и воды, лишь бы ты сам как агент понял
|
||||
```
|
||||
|
||||
Контекст задания: активный файл — `DEV_STAND/FullPipe/shturval.tf1`; активное выделение — строка 117
|
||||
(`suspend_on_destroy`).
|
||||
|
||||
---
|
||||
|
||||
## Ход 2 — Работа агента до создания файла (разведка, только чтение)
|
||||
|
||||
Поиск архитектурных документов и существующих промптов для Opus:
|
||||
|
||||
- `NOTES/20_prompts/` — 20 файлов промптов; среди них `prompt_for_opus_modifier_architecture_full.md`,
|
||||
`prompt_for_opus_modifiers_review.md`, `prompt_for_opus_review_modify_resources_2026-09-24.md`,
|
||||
`prompt_for_opus_modifier_global_architecture.md`. Часть помечена баннером ⛔ «ЛОЖНЫЙ ПУТЬ — ОТМЕНЕНО».
|
||||
- `HISTORY/OPUS/` — 16 файлов диалогов с Opus (напр. `2026-09-22_modifier_architecture_project.md`).
|
||||
- Кандидаты на «файл архитектуры»: `TOOLS/ARCHITECTURE.md`, `NOTES/30_analysis/ARCHITECTURE_NEW.md`,
|
||||
`docs/60_strategy/provider_philosophy.md`, `docs/60_strategy/modifier_resources_ideology_and_specification.md`.
|
||||
|
||||
Прочитано:
|
||||
|
||||
- `TOOLS/ARCHITECTURE.md` (246 строк) — в первых строках помечен «PRIMARY SOURCE OF TRUTH»,
|
||||
«THIS FILE IS THE FOUNDATION».
|
||||
- `NOTES/30_analysis/ARCHITECTURE_NEW.md` — «Universal Rebuild — Архитектура и рабочая цепочка»
|
||||
(внутри — ссылки на пути `/home/naeel/terra/universal_rebuild`, т.е. иная раскладка репозитория).
|
||||
- `NOTES/20_prompts/prompt_for_opus_modifier_architecture_full.md`, `…_modifiers_review.md`,
|
||||
`…_review_modify_resources_2026-09-24.md`.
|
||||
- `NOTES/40_chat_summaries/CHAT_RESUME_IAC_2026-09-24.md` — карта файлов, разделение «актуально / LEGACY».
|
||||
|
||||
Проверка актуальности через git:
|
||||
|
||||
```
|
||||
git log -3 -- TOOLS/ARCHITECTURE.md
|
||||
cbd559d 2026-09-30 docs(tools): канонический пайплайн + история ужесточения генерации YAML
|
||||
78f9dfb 2026-09-22 refactor(build): эфемерные generated-копии …
|
||||
c14f7de 2026-09-22 docs: раздел «Реестр исключений» + диалог код-ревью opus/astra
|
||||
```
|
||||
|
||||
Установленная фактическая реализация модификаторов (на 2026-09-30):
|
||||
|
||||
- База: YAML на сервис — «чистая выгрузка» из API, без доменных меток. В `TOOLS/yaml-generator/main.go`
|
||||
(комментарий перед `CollectOperations`) прямо сказано: реестр `serviceSpecificModifiers`, ранее
|
||||
помечавший `modify` как `kind=modifier`, удалён при рефакторинге 2026-09-23; модификаторы теперь —
|
||||
«отдельный оверлей (см. `modifiers.yaml` + второй проход)», базовый YAML о них не знает.
|
||||
- Реальные модификаторы — РУЧНЫЕ Go-ресурсы:
|
||||
- `provider/internal/resources_core/org_ip_allocation_resource.go` —
|
||||
`nubes_vc_org_ip_allocation` (сервис 19, `modify`, `vIPConfigure` id 662);
|
||||
- `provider/internal/resources_core/nsxt_snat_resource.go` —
|
||||
`nubes_vc_nsxt_snat` (сервис 22, `modify`, `ipSpaceName` id 372; канон-значение `no-needed`).
|
||||
- Регистрация: `provider/internal/provider/provider.go`, функция `Resources()` (строки 182–190) —
|
||||
`append(resources, resources_core.NewOrgIpAllocationResource, resources_core.NewNsxtSnatResource)`.
|
||||
- Вызываемые методы ядра: `RunInstanceOperationUniversalByCode` (`provider/internal/core/operation_run_bycode.go`),
|
||||
`GetInstanceStateParams` (`provider/internal/core/instance_params.go`),
|
||||
`LockInstance`/`UniversalClient` (`provider/internal/core/client.go`),
|
||||
`ResolveRefSvcParamValue` (`provider/internal/core/refsvc_resolve.go`),
|
||||
`ShouldRemoveFromState` (`provider/internal/resources_core/resource_diagnostics.go`).
|
||||
- Спеки операций/параметров: `generated/dev/resources_yaml/19_vc_org.yaml`, `22_vc_nsxt.yaml`.
|
||||
- Пример применения цепочки: `DEV_STAND/FullPipe/modifiers.tf`.
|
||||
|
||||
Агент задал пользователю 3 уточняющих вопроса (какой файл считать «архитектурой»; что именно должен
|
||||
выдать Opus; имя файла промпта) — **пользователь вызов отклонил (cancelled)**.
|
||||
|
||||
---
|
||||
|
||||
## Ход 3 — Ответ пользователя (дословно)
|
||||
|
||||
```
|
||||
думай сам ! возможно несколько файлов ему дать
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Ход 4 — Решение агента и создание файла
|
||||
|
||||
Агентом принято самостоятельно (без дополнительных вопросов):
|
||||
|
||||
- «Файл с архитектурой» — единственный: `TOOLS/ARCHITECTURE.md`.
|
||||
- Формат выхода Opus — архитектурный разбор: устройство по слоям, спека ↔ код, жизненный цикл
|
||||
модификаторов, риски с указанием `файл:строка`.
|
||||
- Путь промпта — `NOTES/20_prompts/prompt_for_opus_provider_architecture_modifiers.md`
|
||||
(соответствует конвенции папки `NOTES/20_prompts/`).
|
||||
|
||||
Проверка существования всех 22 путей из списка (скрипт `test -f` по каждому) — результат:
|
||||
`ALL FILES PRESENT`. Отсутствующих путей нет.
|
||||
|
||||
Коммит: `752244f` —
|
||||
`docs(prompt): промпт для Opus 4.8 — анализ архитектуры провайдера + ресурсов-модификаторов`.
|
||||
|
||||
### Содержимое созданного файла `NOTES/20_prompts/prompt_for_opus_provider_architecture_modifiers.md` (дословно)
|
||||
|
||||
```markdown
|
||||
# Промпт для Opus 4.8 — анализ архитектуры провайдера + ресурсов-модификаторов
|
||||
|
||||
**Репо:** `/home/naeel/TF/tf_provider` — `terraform-provider-nubes` (Go, Terraform Plugin Framework).
|
||||
**Дата:** 2026-09-30. **Стенд-источник фактов:** `dev`.
|
||||
|
||||
---
|
||||
|
||||
## Роль и режим работы
|
||||
|
||||
- Ты — архитектор/ревьюер универсального Terraform-провайдера.
|
||||
- Работаем **в диалоге**: я (агент) передаю твои вопросы пользователю и возвращаю его ответы.
|
||||
- Вопрос задавай ТОЛЬКО если без него ответить нельзя. Максимум 1–2 вопроса за раз, предельно коротко.
|
||||
- Не догадываться. Нет данных — вопрос, а не допущение.
|
||||
- Область не расширять: отвечать ровно на поставленную задачу.
|
||||
|
||||
## Задача
|
||||
|
||||
Проанализировать архитектуру универсального провайдера Nubes и встроенный в неё слой
|
||||
**ресурсов-модификаторов** — отдельных ресурсов, которые вызывают операцию `modify`
|
||||
у родительского инстанса (когда нужного параметра нет в операции `create`).
|
||||
|
||||
Оценить:
|
||||
|
||||
1. Как устроена архитектура по слоям и как течёт поток данных (API → YAML → код → API).
|
||||
2. Соответствие заявленной спеки (`TOOLS/ARCHITECTURE.md`) фактической реализации — все
|
||||
расхождения, с указанием `файл:строка`.
|
||||
3. Корректность жизненного цикла модификаторов: `Create` / `Read` / `Update` / `Delete`,
|
||||
идемпотентность, дрейф (drift), поведение при `replace` / повторном `apply`, импорт.
|
||||
4. Место модификаторов в универсальном ядре: где и как нарушается принцип
|
||||
«ядро универсально, доменные знания — только данные». Насколько оправдано текущее
|
||||
решение (ручные Go-ресурсы, зарегистрированные поверх генерируемых).
|
||||
5. Границы ответственности: что модификатор делает сам, что отдаёт платформе; как
|
||||
выражается обратная операция (откат при `destroy`, значение «выключено»).
|
||||
6. Риски и топ-проблемы — по убыванию критичности, каждое с `файл:строка`.
|
||||
|
||||
## Границы доступа (ЖЁСТКО)
|
||||
|
||||
Читать РАЗРЕШЕНО **только** файлы из списка ниже. Всё остальное — ЗАПРЕЩЕНО, в частности:
|
||||
|
||||
- `HISTORY/**`, `NOTES/**`, `TMP/**`, `HAR/**`, `docs/**`, `site/**`, `site_test/**`,
|
||||
`apps/**`, `charts/**`, `FIYR_MGU/**`, `gateway/**`, `scripts/**`, `secrets/**`,
|
||||
`tfflaskcrud/**`, `tfluceecrud/**`, `tfnodejscrud/**`, `DEV_STAND/**` (кроме одного файла
|
||||
из списка), `TEST_STAND/**`, `PROD_STAND/**`, `provider/artifacts/**`, `provider/bin/**`;
|
||||
- история git (`git log`, `git show`, `git diff` с коммитами), коммиты, теги, ветки;
|
||||
- любой файл репозитория, которого нет в списке ниже.
|
||||
|
||||
Нужен файл вне списка → НЕ читать, а задать мне вопрос.
|
||||
|
||||
## Файлы к изучению (исчерпывающий список)
|
||||
|
||||
### Группа 1. Архитектура (спека)
|
||||
- `TOOLS/ARCHITECTURE.md`
|
||||
|
||||
### Группа 2. Ресурсы-модификаторы и их регистрация
|
||||
- `provider/internal/provider/provider.go`
|
||||
- `provider/internal/resources_core/org_ip_allocation_resource.go`
|
||||
- `provider/internal/resources_core/nsxt_snat_resource.go`
|
||||
- `provider/internal/resources_core/org_ip_allocation_test.go`
|
||||
|
||||
### Группа 3. Рантайм-зависимости модификаторов (ядро)
|
||||
- `provider/internal/core/client.go`
|
||||
- `provider/internal/core/operation_run_bycode.go`
|
||||
- `provider/internal/core/instance_params.go`
|
||||
- `provider/internal/core/refsvc_resolve.go`
|
||||
- `provider/internal/resources_core/resource_diagnostics.go`
|
||||
- `provider/internal/resources_core/crud.go`
|
||||
|
||||
### Группа 4. Генератор (как рождается «универсальная» часть)
|
||||
- `TOOLS/yaml-generator/main.go`
|
||||
- `TOOLS/resource-generator/internal/loader/loader.go`
|
||||
|
||||
### Группа 5. Факты API (спеки операций/параметров)
|
||||
- `generated/dev/resources_yaml/19_vc_org.yaml`
|
||||
- `generated/dev/resources_yaml/22_vc_nsxt.yaml`
|
||||
|
||||
### Группа 6. Применение модификаторов (композиция цепочки)
|
||||
- `DEV_STAND/FullPipe/modifiers.tf`
|
||||
|
||||
### Группа 7. Только если без них нельзя ответить (иначе не открывать)
|
||||
- `provider/internal/resources_core/state_refresh.go`
|
||||
- `provider/internal/resources_core/params_compare.go`
|
||||
- `provider/internal/resources_core/helpers.go`
|
||||
- `TOOLS/resource-generator/main.go`
|
||||
- `TOOLS/resource-generator/internal/templates/instance.go`
|
||||
|
||||
## Что нужно на выходе
|
||||
|
||||
Структурированный отчёт, разделы строго в этом порядке:
|
||||
|
||||
1. **Устройство архитектуры** — слои и поток данных, 5–10 строк.
|
||||
2. **Спека ↔ код** — список расхождений `ARCHITECTURE.md` с реализацией (`файл:строка`).
|
||||
3. **Дефекты и риски модификаторов** — по убыванию критичности. По каждому:
|
||||
суть → место (`файл:строка`) → последствие → предлагаемое направление (одна строка).
|
||||
4. **Открытые вопросы** — списком, если есть.
|
||||
|
||||
## Формат ответа
|
||||
|
||||
- Максимально сжато: тезисы, без вступлений, воды и «лирики».
|
||||
- Каждое утверждение проверяемо: ссылка `файл:строка`.
|
||||
- Код — только короткие фрагменты, и лишь если без них тезис не понятен.
|
||||
- Никаких «а ещё могу», никаких предложений расширить работу.
|
||||
|
||||
## Правило «стоп»
|
||||
|
||||
Если задание неоднозначно или данных не хватает — **остановиться и задать один короткий
|
||||
вопрос**. Не достраивать смысл и не действовать по догадке.
|
||||
```
|
||||
|
||||
### Итоговое сообщение агента пользователю (дословно)
|
||||
|
||||
```
|
||||
Готово.
|
||||
|
||||
`NOTES/20_prompts/prompt_for_opus_provider_architecture_modifiers.md` (коммит `752244f`).
|
||||
|
||||
Что внутри:
|
||||
- **Задача** — анализ архитектуры провайдера + слоя модификаторов (`Create/Read/Update/Delete`, идемпотентность, drift, спека↔код, место в универсальном ядре, границы/откат, топ-рисков с `файл:строка`).
|
||||
- **Границы (жёстко)** — запрет на `HISTORY/**`, `NOTES/**`, `TMP/**`, `HAR/**`, `docs/**`, стенды, git-историю; нужен файл вне списка → вопрос мне, не чтение.
|
||||
- **Список файлов** — 22 шт., исчерпывающий, 7 групп: спека (`TOOLS/ARCHITECTURE.md`) → модификаторы + регистрация → рантайм-ядро → генератор → YAML-спеки 19/22 → пример (`DEV_STAND/FullPipe/modifiers.tf`) → «только при необходимости».
|
||||
- **Формат ответа** — сжато, тезисы, каждое утверждение с `файл:строка`, без «лирики».
|
||||
- **Режим** — диалог, но без злоупотребления; при неясности один короткий вопрос и стоп.
|
||||
|
||||
Все 22 пути проверены — существуют.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Ход 5 — Ответ Opus 4.8 (дословно, как получен от пользователя)
|
||||
|
||||
Ниже — полный сырой лог сессии Opus (включая служебные строки о чтениях и неудачных поисках),
|
||||
затем его отчёт.
|
||||
|
||||
```
|
||||
Read
|
||||
|
||||
Прочитаю файлы строго по разрешённому списку. Начну с групп 1–3.
|
||||
|
||||
Read
|
||||
|
||||
Read
|
||||
|
||||
Прочитаю группу 2 (модификаторы) и группу 3 (ядро).
|
||||
|
||||
Read
|
||||
|
||||
Read
|
||||
|
||||
Прочитаю группу 3 (ядро) и тест.
|
||||
|
||||
Read
|
||||
|
||||
Read
|
||||
|
||||
Read
|
||||
|
||||
Read
|
||||
|
||||
Read
|
||||
|
||||
Read
|
||||
|
||||
Прочитаю `client.go` (ядро) и группу 4 (генератор).
|
||||
|
||||
Read
|
||||
|
||||
Прочитаю группу 4 (генератор), группу 6 (modifiers.tf) и начну группу 5.
|
||||
|
||||
Read
|
||||
|
||||
Read
|
||||
|
||||
Read
|
||||
|
||||
Прочитаю группу 5 (API YAML). Сначала проверю размер.
|
||||
|
||||
No matches found. Your search pattern might be excluded completely by either the search.exclude settings or .*ignore files.
|
||||
If you believe that it should have results, you can check into the .*ignore files and the exclude setting (here are some excluded patterns for reference:[**/node_modules,**/bower_components,**/*.code-search]).
|
||||
Then if you want to include those files you can call the tool again by setting "includeIgnoredFiles" to true.
|
||||
|
||||
No matches found. Your search pattern might be excluded completely by either the search.exclude settings or .*ignore files.
|
||||
If you believe that it should have results, you can check into the .*ignore files and the exclude setting (here are some excluded patterns for reference:[**/node_modules,**/bower_components,**/*.code-search]).
|
||||
Then if you want to include those files you can call the tool again by setting "includeIgnoredFiles" to true.
|
||||
|
||||
Searched for files matching `**/generated/dev/resources_yaml/*vc_org*.yaml`, no matches
|
||||
|
||||
Searched for files matching `**/generated/dev/resources_yaml/*nsxt*.yaml`, no matches
|
||||
|
||||
Searched for files matching `**/generated/dev/**/19_*.yaml`, no matches
|
||||
|
||||
Read
|
||||
|
||||
Searched for files matching `**/generated/dev/resources_yaml/*.yaml`, no matches
|
||||
|
||||
Read
|
||||
|
||||
Файлы существуют; grep не сработал по паттерну. Прочитаю их напрямую.
|
||||
|
||||
No matches found. Your search pattern might be excluded completely by either the search.exclude settings or .*ignore files.
|
||||
If you believe that it should have results, you can check into the .*ignore files and the exclude setting (here are some excluded patterns for reference:[**/node_modules,**/bower_components,**/*.code-search]).
|
||||
Then if you want to include those files you can call the tool again by setting "includeIgnoredFiles" to true.
|
||||
|
||||
No matches found. Your search pattern might be excluded completely by either the search.exclude settings or .*ignore files.
|
||||
If you believe that it should have results, you can check into the .*ignore files and the exclude setting (here are some excluded patterns for reference:[**/node_modules,**/bower_components,**/*.code-search]).
|
||||
Then if you want to include those files you can call the tool again by setting "includeIgnoredFiles" to true.
|
||||
|
||||
Read
|
||||
|
||||
Read
|
||||
|
||||
Проанализировал все файлы из разрешённого списка (группы 1–6; группа 7 не понадобилась). Отчёт ниже.
|
||||
```
|
||||
|
||||
### Отчёт Opus (дословно)
|
||||
|
||||
```markdown
|
||||
## 1. Устройство архитектуры
|
||||
|
||||
Поток данных: **API → YAML → Go-код → API**.
|
||||
- Генератор YAML (`main.go`) делает **чистую выгрузку** операций сервиса из API (`create/delete/modify/suspend/...`) без доменных знаний — один файл на сервис (`19_vc_org.yaml`, `22_vc_nsxt.yaml`).
|
||||
- Генератор кода (`loader.go`) читает YAML, классифицирует операции на `instance/subresource/action/modifier`, строит `GenResource/...` и рендерит универсальные ресурсы.
|
||||
- Ядро (`core`) — универсальный HTTP-клиент: запуск операций, ожидание, резолв ref-параметров, досылка дефолтов.
|
||||
- **Модификаторы** — это НЕ генерируемый слой: два ручных Go-ресурса (`org_ip_allocation_resource.go`, `nsxt_snat_resource.go`), зарегистрированных вручную в `provider.go:199-204`. Каждый дёргает `modify` родителя через `RunInstanceOperationUniversalByCode`.
|
||||
|
||||
## 2. Спека ↔ код (расхождения)
|
||||
|
||||
**S1. Реестр `serviceSpecificModifiers` в спеке указан не там, где он есть (его нет).**
|
||||
`ARCHITECTURE.md` (таблица «Exception Registry») утверждает, что `serviceSpecificModifiers` живёт в `main.go`. Фактически этот реестр **удалён** (рефакторинг 2026-09-23) — прямо задокументировано в `main.go:70-90`. Названного реестра нет; модификаторы стали ручными ресурсами. Таблица-спека устарела.
|
||||
|
||||
**S2. «Service-specific Go code is fully generated… No manual edits» / «Core is universal» — нарушено.**
|
||||
`ARCHITECTURE.md` (Core Principles 2, 3; «Provider Model»). Но `org_ip_allocation_resource.go` и `nsxt_snat_resource.go` — ручной сервис-специфичный Go с зашитыми `service_id`, именами параметров и канон-значениями, лежащий в слое `resources_core`. Это доменная логика внутри «ядра».
|
||||
|
||||
**S3. Заявленные «ровно ДВА реестра исключений» — по факту есть третье место.**
|
||||
`ARCHITECTURE.md` («The ONLY allowed deviations… MUST live in exactly two named registries»). Третье место доменных данных/логики — `provider.go:199-204` + два файла модификаторов. Оно не покрыто правилом реестра и не отражается в его диффе.
|
||||
|
||||
**S4. «modify всегда через WithDefaults» — модификаторы идут через ByCode.**
|
||||
`ARCHITECTURE.md` («modify всегда через WithDefaults / `RunInstanceOperationUniversalWithDefaults`»). Модификаторы намеренно используют `RunInstanceOperationUniversalByCode` (`operation_run_bycode.go:10-13`) с причиной в комментарии `nsxt_snat_resource.go:240-250`. Досылка дефолтов там своя (live → paramValue формы → default, `operation_run_bycode.go:105-150`). Буквальное утверждение спеки не выполняется.
|
||||
|
||||
**S5. Полноценный генерируемый слой модификаторов существует, но не задействован.**
|
||||
`loader.go:55-90` полностью поддерживает `kind: modifier` с `delete_strategy` (`noop_warn/inverse/error`), `idempotency` (`none/check_before_run`), `delete_params`, валидацией (validateModifierOperation). Ядро под это имеет `RunInstanceOperationUniversalByIdempotent` (`operation_run_bycode.go:15-19`) и `RunOperationByCodeIdempotent` (`crud.go`). Но оба реальных модификатора — ручные и это всё **не используют**, переизобретая delete-стратегию вручную (`keep_on_destroy` + inverse). Базовые YAML (19, 22) `kind: modifier` не содержат — оверлей `modifiers.yaml`, упомянутый в `main.go:84-90`, в разрешённом списке отсутствует и в базовых спеках не проявлен.
|
||||
|
||||
## 3. Дефекты и риски модификаторов (по убыванию критичности)
|
||||
|
||||
**R1. Двойное владение одним и тем же параметром API.**
|
||||
Суть: `vIPConfigure` (id 662) есть в `modify` генерируемого `nubes_vc_org` (`19_vc_org.yaml`, op modify), а `ipSpaceName` (id 372) — в `modify` генерируемого `nubes_vc_nsxt` (`22_vc_nsxt.yaml`). Те же поля пишет и модификатор.
|
||||
Место: `org_ip_allocation_resource.go:316-340` / `nsxt_snat_resource.go:240-253`.
|
||||
Последствие: если пользователь заводит и инстанс-ресурс, и модификатор — «война дрейфов»: два ресурса по очереди перезаписывают поле каждым apply.
|
||||
Направление: явно исключать пересекающиеся коды из схемы генерируемого ресурса, если поле отдано модификатору (или запретить одновременное использование).
|
||||
|
||||
**R2. Зашитые сервис-специфичные данные обходят страж `check_hardcoded_service_ids.sh`.**
|
||||
Суть: id 19/22, имена `vIPConfigure`/`ipSpaceName`, значение `"no-needed"` зашиты как литералы-аргументы, а не как `svc.ID == N`.
|
||||
Место: `org_ip_allocation_resource.go:296-314` (`ResolveRefSvcParamValue(ctx, 19, …)`), `nsxt_snat_resource.go:43`.
|
||||
Последствие: правило «никаких hardcoded service id вне реестров» формально соблюдено, фактически — нет; страж это не ловит.
|
||||
Направление: вынести id/коды/каноны в один явный реестр-данные, покрытый чекером, либо расширить паттерн чекера.
|
||||
|
||||
**R3. Порядок «edge → аллокация» не гарантируется провайдером.**
|
||||
Суть: платформа требует существующий vDC+Edge до `modify` орги, иначе «Can't cast Complex Object Type Struct to String».
|
||||
Место: описано в `modifiers.tf:6-16`; в коде порядок не выражен — держится только на пользовательском `depends_on`.
|
||||
Последствие: забытый `depends_on` → непонятная ошибка платформы на apply.
|
||||
Направление: либо документировать как жёсткое требование в схеме/описании ресурса, либо проверять готовность edge в `Create` до modify.
|
||||
|
||||
**R4. Нельзя снять аллокацию через атрибут — только `destroy`.**
|
||||
Суть: пустой массив запрещён (`org_ip_allocation_resource.go:328-331` `len==0 → error`), а Delete шлёт `count=0`, но `[]` не отправляется (`org_ip_allocation_resource.go:246-266`).
|
||||
Последствие: «выключено» выражается двумя разными способами (count=0 при destroy vs невозможность `[]` при update) — асимметрия семантики.
|
||||
Направление: определить единый канон «ноль аллокаций» и разрешить его через атрибут, либо явно задокументировать ограничение как намеренное.
|
||||
|
||||
**R5. Модификаторы не идемпотентны на уровне API (modify выполняется всегда).**
|
||||
Суть: `ByCode` без pre-check — `Create`/`Update` всегда POST-ят modify, даже если live уже совпадает. Причина осознанная (комментарий applyAllocation).
|
||||
Последствие: лишние операции modify при повторных apply/после ручных правок state; на больших инстансах — время и нагрузка. Корректности не нарушает (дрейф гасится в `Read`).
|
||||
Направление: если появится надёжное сравнение desired↔live (не с paramValue формы) — переключить на idempotent-путь, уже имеющийся в ядре.
|
||||
|
||||
**R6. Импорт оставляет Required-атрибуты неинициализированными.**
|
||||
Суть: `ImportState` ставит только `id` и `organization`/`nsxt_uid` (`org_ip_allocation_resource.go:288-292`, `nsxt_snat_resource.go:222-226`). `vip_configure`/`ip_space_name` (Required) наполняются лишь в `Read`, и то по условию (nsxt — только если live непустой `nsxt_snat_resource.go:160-166`).
|
||||
Последствие: при импорте эджа с невыставленным SNAT `ip_space_name` останется null → требуется совпадение с конфигом на следующем плане; для пользователя неочевидно.
|
||||
Направление: в `ImportState`/`Read` заполнять атрибут каноном (`no-needed`) при отсутствии значения.
|
||||
|
||||
## 4. Открытые вопросы
|
||||
|
||||
1. Оверлей `modifiers.yaml` и генерируемый слой `kind: modifier` (см. R5/S5) — в разрешённый список не входил. Нужно ли оценивать его как «мёртвый/параллельный» код к удалению, или он планируется как целевая замена ручных модификаторов? (файл вне списка — не читал).
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Ход 6 — Указание пользователя (дословно)
|
||||
|
||||
```
|
||||
сначала документируй ВЕСЬ диалог, без сокращений
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Открытый вопрос Opus (ожидает ответа пользователя)
|
||||
|
||||
1. Оверлей `modifiers.yaml` и генерируемый слой `kind: modifier` — «мёртвый/параллельный» код
|
||||
к удалению, или планируемая целевая замена ручных модификаторов?
|
||||
|
||||
---
|
||||
|
||||
## Ход 7 — Запрос пользователя (дословно)
|
||||
|
||||
```
|
||||
твоё критическое мнение ?
|
||||
```
|
||||
|
||||
### Критическое мнение агента о отчёте Opus (дословно)
|
||||
|
||||
```
|
||||
**Сильное**
|
||||
- **S1** — верно. `ARCHITECTURE.md:225` ссылается на реестр `serviceSpecificModifiers`, а в `TOOLS/yaml-generator/main.go` (~стр. 77–90) написано, что он удалён при рефакторинге. Спека устарела — цена находки низкая, но факт точный.
|
||||
- **S5** — верно и важно. `loader.go` умеет `kind: modifier` (+ `delete_strategy`, `idempotency`) и в ядре есть idempotent-путь, но реальные модификаторы ручные. Это не «мёртвый код», а **незавершённая миграция**.
|
||||
- **R1** — самое ценное. `nubes_vc_nsxt` в `Update` шлёт `ip_space_name` (372), и тот же параметр пишет `nubes_vc_nsxt_snat` → два ресурса перезаписывают одно поле. Аналогично `nubes_vc_org` / `v_ip_configure`.
|
||||
- R4, R6 — по делу, но второго порядка.
|
||||
|
||||
**Слабое / спорное**
|
||||
- **S2, S4 — натянуто.** Правила спеки («no manual edits», «core is universal») писаны про **сгенерированный** код; ручной оверлей в них вообще не описан. Это пробел спеки, а не нарушение «по букве». S4 ещё и относится к `Update` инстанс-ресурсов, к модификатору прямого отношения не имеет.
|
||||
- **R2 — нарушение собственного правила.** Opus утверждает, что `check_hardcoded_service_ids.sh` «это не ловит», **не читая сам скрипт** (он вне списка). Надо было оформить вопросом, а не утверждением.
|
||||
- **R5 — завышен по приоритету**: сам же пишет «корректности не нарушает».
|
||||
- **Номера строк не сверены.** У меня из чтения `provider.go` функция `Resources` начинается со строки 182, и регистрация модификаторов идёт сразу за ней, а не 199–204. Одно из двух неверно → «проверяемость» отчёта под вопросом (а она была главным требованием).
|
||||
|
||||
**Упущено**
|
||||
- Файла `modifiers.yaml` в репозитории **нет вообще** — только упоминания-комментарии. Значит это не «параллельный слой», а **задокументированное, но не созданное** наложение; его вопрос №1 надо переформулировать.
|
||||
- Не названа корневая причина: генератор умеет модификаторы, но доменное знание (какая операция, имя, inverse) некуда положить — оверлей не сделан, поэтому написали руками.
|
||||
- Не отмечена рассинхронизация семантики: у генерируемых ресурсов `suspend_on_destroy`, у модификаторов `keep_on_destroy` — два имени для одного смысла.
|
||||
- То, что для Terraform опаснее всего — устойчивость `Read` (вечный diff), — разобрано тоньше всего, хотя именно там был реальный баг с порядком ключей JSON.
|
||||
|
||||
**Итог:** ~60 % полезного (S1, S5, R1, R4 + вопрос про оверлей), но с натяжками в S2/S4/R2 и без разбора `Read`. Главный дефект — поверхностный lifecycle-анализ.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Ход 8 — Указание пользователя (дословно)
|
||||
|
||||
```
|
||||
так сделай ему замечание !!! пусть глубже копнёт ЕСЛИ ЭТО НАДО
|
||||
```
|
||||
|
||||
### Замечания агента (раунд 2) — текст, отправляемый Opus
|
||||
|
||||
Сохранён отдельным файлом: `NOTES/20_prompts/prompt_for_opus_provider_architecture_modifiers_round2.md`.
|
||||
Содержимое (дословно):
|
||||
|
||||
```markdown
|
||||
# Промпт для Opus 4.8 — РАУНД 2: замечания к отчёту (2026-09-30)
|
||||
|
||||
> Продолжение диалога. Раунд 1 — `prompt_for_opus_provider_architecture_modifiers.md`.
|
||||
> Формат ответа тот же: сжато, тезисы, `файл:строка`, без догадок. Границы доступа — как в раунде 1
|
||||
> (плюс список из §4 ниже). `HISTORY/**`, `NOTES/**`, `docs/**`, `HAR/**`, `TMP/**`, git-история — по-прежнему ЗАПРЕЩЕНЫ.
|
||||
|
||||
---
|
||||
|
||||
## 1. Зачтено (переделывать НЕ надо)
|
||||
|
||||
`S1`, `S5`, `R1`, `R4` — приняты. Не повторяй их в ответе.
|
||||
|
||||
## 2. Замечания — обязательны к отработке
|
||||
|
||||
**M1. Номера строк не сходятся.**
|
||||
Ты дал `provider.go:199-204` для регистрации модификаторов. По моему чтению файла (начиная со
|
||||
строки 180) функция `Resources()` находится примерно на строке 182, и регистрация идёт сразу за ней —
|
||||
твои 199–204 не сходятся. Требование задания — «каждое утверждение проверяемо».
|
||||
Действие: перепроверь **каждую** ссылку `файл:строка` в отчёте и дай точные номера; где не сверял —
|
||||
пометь «не сверено». Без этого отчёт не принимается.
|
||||
|
||||
**M2. `R2` — нарушено правило «без догадок».**
|
||||
Ты утверждаешь, что `check_hardcoded_service_ids.sh` «это не ловит», но этот скрипт **не читал**
|
||||
(его не было в разрешённом списке). Это догадка, а не факт.
|
||||
Действие: скрипт теперь разрешён (см. §4). Либо приведи факт из его кода, либо переформулируй в вопрос.
|
||||
|
||||
**M3. `S2`/`S4` — проверь основание, иначе они натянуты.**
|
||||
Правила спеки («No manual edits to **generated** Go code», «Service-specific Go code is fully
|
||||
**generated** from YAML») писаны про генерируемый код. Ресурсы в `resources_core` — ручные, не
|
||||
генерируемые. Плюс `S4` («modify всегда через WithDefaults») относится к `Update` инстанс-ресурсов,
|
||||
а не к отдельному ресурсу-модификатору.
|
||||
Действие: для каждого из S2/S4 дай **текстуальную опору из спеки** (`TOOLS/ARCHITECTURE.md:строка`)
|
||||
и переформулируй: это **пробел спеки** (нет категории для ручных оверлеев) или **нарушение**? Если
|
||||
опоры нет — пункт снять.
|
||||
|
||||
**M4. `R5` — обоснуй приоритет или понизь.**
|
||||
Ты сам пишешь «корректности не нарушает», но ставишь R5 выше R6.
|
||||
Действие: назови шкалу ранжирования (например: вероятность × последствие × обнаружимость) и
|
||||
пересчитай порядок; либо понизь R5.
|
||||
|
||||
**M5. Главный пробел: устойчивость `Read` и вечный diff.**
|
||||
Для Terraform это опаснее всего, а разобрано тоньше всего (только R6/импорт).
|
||||
Действие: разбери построчно, как `Read` модификатора формирует `vip_configure` / `ip_space_name`
|
||||
из live-состояния и совпадёт ли это с тем, что вернёт `jsonencode` из конфига:
|
||||
- порядок ключей JSON;
|
||||
- тип `count` (строка vs число);
|
||||
- снятие `null` и пустых значений;
|
||||
- что видит `plan` после `Read` для `keep_on_destroy`.
|
||||
Отдельно: может ли `Update` инстанс-ресурса (`nubes_vc_org` / `nubes_vc_nsxt`) вклиниться между
|
||||
apply-ами модификатора и вернуть поле назад (пересечение — это R1, но нужен сценарий во времени).
|
||||
Если считаешь, что глубокий разбор здесь не нужен — обоснуй одной строкой, почему.
|
||||
|
||||
## 3. Что упущено — доработать
|
||||
|
||||
**U1. Файла `modifiers.yaml` в репозитории НЕТ.**
|
||||
Есть только упоминания-комментарии (в `TOOLS/yaml-generator/main.go` и док-заметках). Это не
|
||||
«параллельный слой», а **задокументированное, но не созданное** наложение.
|
||||
Действие: переформулируй свой открытый вопрос №1 с учётом этого факта.
|
||||
|
||||
**U2. Рассинхронизация словаря жизненного цикла.**
|
||||
У генерируемых ресурсов — `suspend_on_destroy`, у модификаторов — `keep_on_destroy`. Два имени
|
||||
для одного смысла, живут в разных ветках кода.
|
||||
Действие: оцени как архитектурный дефект (единый словарь/контракт жизненного цикла), с `файл:строка`.
|
||||
|
||||
**U3. Корневая причина «ручных» модификаторов.**
|
||||
Генератор умеет `kind: modifier`, но доменное знание (какая операция, имя, inverse) некуда положить —
|
||||
оверлей не создан.
|
||||
Действие: сформулируй вывод по вариантам (оверлей-данные vs ручной код vs иное) — одной строкой на вариант.
|
||||
|
||||
## 4. Дополнительно РАЗРЕШЕНО читать (только это, сверх раунда 1)
|
||||
|
||||
- `TOOLS/scripts/check_hardcoded_service_ids.sh`
|
||||
- `TOOLS/resource-generator/main.go`
|
||||
- `TOOLS/resource-generator/internal/templates/instance.go`
|
||||
- `TOOLS/resource-generator/internal/templates/modifier.go`
|
||||
- `provider/internal/resources_core/state_refresh.go`
|
||||
- `provider/internal/resources_core/helpers.go`
|
||||
- `provider/internal/resources_core/params_compare.go`
|
||||
|
||||
Всё остальное — по-прежнему нельзя. Нужен файл вне списка → вопрос мне, не чтение.
|
||||
|
||||
## 5. Формат ответа
|
||||
|
||||
- **Только дельта**: что исправляешь, что снимаешь, чем обосновываешь; новые находки по M5/U2.
|
||||
- Полный отчёт заново не переписывать.
|
||||
- Как в раунде 1: тезисы, `файл:строка`, без «лирики» и без «а ещё могу».
|
||||
|
||||
## 6. Разрешение копать глубже
|
||||
|
||||
Если для честного ответа нужно копнуть глубже (в т.ч. по файлам вне §4) — **скажи прямо**, каких
|
||||
файлов не хватает, и задай вопрос. Если глубина не нужна — так и напиши по конкретному пункту.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Ход 9 — Ответ Opus на раунд 2 (дословно, как получен от пользователя)
|
||||
|
||||
### Сырой лог сессии
|
||||
|
||||
```
|
||||
Read
|
||||
|
||||
Раунд 2. Перепроверю номера строк и прочту новые разрешённые файлы.
|
||||
|
||||
Read , lines 193 to 195
|
||||
|
||||
Read
|
||||
|
||||
Read , lines 178 to 195
|
||||
|
||||
Searched for regex `ResolveRefSvcParamValue\(ctx, 19|noNeededIpSpace|func \(r \*|len\(items\) == 0|RunInstanceOperationUniversalByCode|liveItems, parseErr|if raw, ok := live\["ipSpaceName"\]|func parseVipConfigure|func formatVipConfigure|func vipAllocationsEqual` (`**/provider/internal/resources_core/org_ip_allocation_resource.go`), 21 results
|
||||
|
||||
Searched for regex `noNeededIpSpace|func \(r \*NsxtSnat|if raw, ok := live\["ipSpaceName"\]|func \(r \*NsxtSnatResource\) setSnat|RunInstanceOperationUniversalByCode|ImportState` (`**/provider/internal/resources_core/nsxt_snat_resource.go`), 18 results
|
||||
|
||||
Прочитаю новые разрешённые файлы: шаблоны генератора, helpers, params_compare, state_refresh.
|
||||
|
||||
Read
|
||||
|
||||
Read
|
||||
|
||||
Прочитаю helpers.go, state_refresh.go, params_compare.go для проверки поведения `ParamFormat`/refresh.
|
||||
|
||||
Read
|
||||
|
||||
Проверю точные номера строк в `ARCHITECTURE.md` для опоры S2/S4.
|
||||
|
||||
Searched for regex `must not contain service-specific|fully generated from YAML|No manual edits|no service-specific logic inside the core|modify всегда через WithDefaults|serviceSpecificModifiers|exactly two named registries` (`**/TOOLS/ARCHITECTURE.md`), 7 results
|
||||
|
||||
Searched for regex `for _, param := range cfsParams|func \(c \*UniversalClient\) RunInstanceOperationUniversalByCode|RunInstanceOperationUniversalByIdempotent|val, hasLive := lookupLiveParam` (`**/provider/internal/core/operation_run_bycode.go`), 5 results
|
||||
|
||||
Read , lines 174 to 193
|
||||
|
||||
Дельта по раунду 2.
|
||||
```
|
||||
|
||||
### Дельта (дословно)
|
||||
|
||||
```markdown
|
||||
## M1 — перепроверенные `файл:строка`
|
||||
|
||||
| Ссылка раунда 1 | Было | Факт (сверено) |
|
||||
|---|---|---|
|
||||
| Регистрация модификаторов | `provider.go:199-204` | `provider.go:182` — `func Resources()`; регистрация — `provider.go:186` и `provider.go:187` |
|
||||
| Удалённый реестр в генераторе | `yaml-generator/main.go:70-90` | `main.go:80-90` (комментарий про снятый `serviceSpecificModifiers`) |
|
||||
| hardcoded id 19 | `~300` | `org_ip_allocation_resource.go:308` |
|
||||
| `noNeededIpSpace` | `43` | const — `nsxt_snat_resource.go:44`; `setSnat` — `nsxt_snat_resource.go:234`; inverse-modify — `nsxt_snat_resource.go:199-200` |
|
||||
| Досылка дефолтов ByCode | `105-150` | `operation_run_bycode.go:112-150`; `ByCode` — `operation_run_bycode.go:11`, `ByIdempotent` — `operation_run_bycode.go:18` |
|
||||
| `len(items)==0` → error | `328-331` | `org_ip_allocation_resource.go:333` |
|
||||
| GenModifier-конвейер | `55-90` | `loader.go:57-86` (`op.Kind=="modifier"`) |
|
||||
|
||||
Прочее сверено: org_ip `ImportState` `org_ip_allocation_resource.go:290`; nsxt `ImportState` `nsxt_snat_resource.go:224`; nsxt `Read` overwrite `nsxt_snat_resource.go:152`; org_ip `Read` semantic-compare `org_ip_allocation_resource.go:177-188`.
|
||||
|
||||
## M2 — R2 переформулирован как ФАКТ (скрипт прочитан)
|
||||
|
||||
`check_hardcoded_service_ids.sh:15`: `grep -rnE '\.(ServiceID|ID)[[:space:]]*==[[:space:]]*[1-9][0-9]*' "$ROOT_DIR/TOOLS"`.
|
||||
Два проверяемых факта:
|
||||
1. **Область — только `TOOLS`.** Дерево `provider` не сканируется вовсе → ручные модификаторы в `resources_core` вне охвата стража по определению.
|
||||
2. **Паттерн ловит только `.ServiceID==N` / `.ID==N`.** Литерал-аргумент `org_ip_allocation_resource.go:308` (`ResolveRefSvcParamValue(ctx, 19, …)`) под паттерн не подпадает даже теоретически.
|
||||
|
||||
Итог R2: hardcoded 19/22/`"no-needed"` не покрыты стражем по **двум** причинам (область + паттерн). Догадки убраны.
|
||||
|
||||
## M3 — S2/S4 пересмотрены
|
||||
|
||||
**S4 — СНИМАЮ.** Опора `ARCHITECTURE.md:134` находится в разделе «Generated Code Resilience» и относится к `Update` генерируемых инстанс-ресурсов, а не к ручному модификатору. К модификаторам правило неприменимо → расхождения нет.
|
||||
|
||||
**S2 — переклассифицирую в ПРОБЕЛ СПЕКИ (не «нарушение No manual edits»).**
|
||||
Опоры: `ARCHITECTURE.md:13` и `ARCHITECTURE.md:213` говорят про *generated* Go — к ручным файлам не относятся (замечание верное). Но `ARCHITECTURE.md:12` («core … must not contain service-specific logic») и `ARCHITECTURE.md:110` («no service-specific logic inside the core») сформулированы про «core» без оговорок. Ручные модификаторы с зашитыми id 19/22 лежат в `resources_core` (`org_ip_allocation_resource.go:308`, `nsxt_snat_resource.go:44`). Спека **не содержит категории «ручной сервисный оверлей»** и не говорит, является ли `resources_core` частью «core». Поэтому S2 — пробел контракта (нет разрешённого места для такого кода), а по букве принципа 2 — пограничное противоречие. Не «нарушение No manual edits».
|
||||
|
||||
## M4 — шкала ранжирования и пересчёт
|
||||
|
||||
Шкала: **severity = вероятность × тяжесть_последствия × (1/обнаружимость)**. «Тихие» дефекты (низкая обнаружимость) поднимаются; то, что сразу видно в плане/диффе — опускается.
|
||||
|
||||
- **R2** — вер. высокая (любое добавление сервис-логики), тяжесть средняя (эрозия инварианта «ядро универсально»), обнаружимость низкая (страж молчит) → **верх**.
|
||||
- **R3** — вер. средняя (забытый `depends_on`), тяжесть средняя (непонятная ошибка платформы), обнаружимость средняя → **середина**.
|
||||
- **R6** — вер. средняя (импорт редок, но реален), тяжесть низкая, обнаружимость средняя → **ниже R3**.
|
||||
- **R5** — «корректности не нарушает» (моё же слово), тяжесть минимальная, обнаружимость высокая → **низ**.
|
||||
|
||||
Новый порядок: **R2 > R3 > R6 > R5**. R5 понижен ниже R6 (замечание принято).
|
||||
|
||||
## M5 — устойчивость `Read` и вечный diff (главный разбор)
|
||||
|
||||
**org_ip, путь значения `vip_configure`:**
|
||||
- Конфиг: `jsonencode([{name,count}])` → ключи по алфавиту (`count`,`name`); `count` — тип из `var.ip_count` (в `modifiers.tf:24` не квотирован → зависит от типа переменной).
|
||||
- Канон провайдера `org_ip_allocation_resource.go:385`: `name` первым, `count` — всегда строка.
|
||||
- Эти две формы **байт-различны** (порядок ключей; возможно число vs строка).
|
||||
- Защита от вечного diff — `org_ip_allocation_resource.go:177-188`: сравнение **смысловое** (`org_ip_allocation_resource.go:398`, игнорирует порядок и формат), и при совпадении state **не перезаписывается** → в state остаётся байт-в-байт конфиг-форма → `plan` чист. `count`-число нормализуется в строку в `org_ip_allocation_resource.go:351` до сравнения, поэтому «3» (число) и «"3"» считаются равными. `null`/пустые/`[{}]` отбрасываются там же → не порождают фантомную аллокацию.
|
||||
- Перезапись (реальный дрейф) даёт канон-форму (name-first) — она никогда не совпадёт байтово с `jsonencode`, но diff тогда **легитимен**; ближайший `Update` ставит `state=plan` (`org_ip_allocation_resource.go:119`) и вечного diff не создаёт.
|
||||
|
||||
**nsxt_snat, путь `ip_space_name`:** скаляр-строка. `nsxt_snat_resource.go:152` перезаписывает state только если live-значение непустое; иначе оставляет план (защита Required-атрибута от null). Форматных расхождений нет (простое имя) → вечного diff нет.
|
||||
|
||||
**`keep_on_destroy` в плане:** Optional+Computed, `Default=false` (`org_ip_allocation_resource.go:84`, `nsxt_snat_resource.go:78`). `Read` читает его из state в начале и не трогает (наружу его в API нет) → после первого apply стабилен, `plan` чист. В `modifiers.tf:30` задан `true` явно — diff отсутствует.
|
||||
|
||||
**Вывод M5-основной:** сами модификаторы вечного diff **не дают** — смысловое сравнение в `Read` его гасит.
|
||||
|
||||
**Временной сценарий R1 (инстанс-ресурс возвращает поле назад):**
|
||||
Шаблон instance.go Update строит `params` из **всех** `ModifyParams` безусловно (не пропускает null для не-nested), и вызывает `UpdateResourceWithTimeout` → `RunInstanceOperationUniversalWithDefaults`. Гейт — `hasServiceParamChanges` (тот же шаблон): modify запускается, если изменился **любой** modify-параметр.
|
||||
- `vc_org`: modify содержит **только** `vIPConfigure` (`19_vc_org.yaml`, op modify) → `nubes_vc_org` тронет поле, лишь если пользователь сам задал `vIPConfigure` на инстанс-ресурсе. Риск ниже.
|
||||
- `vc_nsxt`: modify содержит `needEnableAVI`, `virtualServicesCount`, `ipSpaceName`, `qosProfile`, `routedNetConfiguration` (`22_vc_nsxt.yaml`). Сценарий: (t1) модификатор включил SNAT → (t2) пользователь на `nubes_vc_nsxt` меняет `needEnableAVI` → `hasServiceParamChanges=true` → `params[372]=ParamFormat(plan.IpSpaceName)` (запись присутствует всегда) → (t3) следующий `Read` модификатора видит дрейф и на очередном apply восстанавливает. Две сущности «пинают» поле по очереди.
|
||||
|
||||
**Не сверено** (нужны файлы вне §4): пошлёт ли `RunInstanceOperationUniversalWithDefaults` пустой `ipSpaceName` как `""` (затрёт SNAT) или дособерёт из live (как ByCode). Это решает, «затирание» или «no-op». Файлы: `core/operation_*` с реализацией `WithDefaults` и генератор-хелпер `ParamFormat` (funcs.go) для поведения null→"". См. M6.
|
||||
|
||||
## U1 — открытый вопрос №1 переформулирован
|
||||
|
||||
`modifiers.yaml` в репозитории **нет** — только упоминания-комментарии (`main.go:84-90`). Генерируемый слой модификаторов при этом **реален и готов**: шаблон `modifier.go` (полный CRUD + `reconcile` + `delete_strategy` + `idempotency`) и конвейер `loader.go:57-86`. То есть это **задокументированное-но-несозданное наложение**: механизм есть, данных для него нет. Вопрос: создавать `modifiers.yaml`-оверлей (данные) и перевести org_ip/nsxt_snat на генерацию — или узаконить ручные ресурсы отдельной категорией в спеке?
|
||||
|
||||
## U2 — рассинхронизация словаря жизненного цикла (архитектурный дефект)
|
||||
|
||||
Три несогласованных словаря «что делать при destroy», в разных ветках кода:
|
||||
1. Генерируемый инстанс: `suspend_on_destroy` + `keep_on_destroy` (runtime-булевы, приоритет keep) — instance.go schema и `Delete` (ветки `suspend`/`state_only`).
|
||||
2. Генерируемый модификатор: `delete_strategy` = `noop_warn`/`inverse`/`error` — **compile-time из YAML**, без `keep_on_destroy` — modifier.go Delete.
|
||||
3. Ручной модификатор: снова `keep_on_destroy` (runtime) + ручной inverse — `org_ip_allocation_resource.go:84`, `org_ip_allocation_resource.go:195`; `nsxt_snat_resource.go:78`.
|
||||
|
||||
Дефект: один смысл («не трогать эффект при destroy / обратный откат») выражен тремя разными контрактами (runtime-флаг vs compile-time-стратегия), причём генерируемый и ручной модификаторы, решая одну задачу, используют **противоположные** механизмы. Нужен единый словарь жизненного цикла для всех трёх видов ресурсов.
|
||||
|
||||
## U3 — корневая причина «ручных» модификаторов (по вариантам, одной строкой)
|
||||
|
||||
- **Оверлей-данные (`modifiers.yaml` + второй проход):** доменное знание (сервис, операция, имя, inverse, idempotency) — декларативно; org_ip/nsxt_snat становятся генерируемыми; убирает S2/U2/R2 разом; цена — доделать загрузчик оверлея и миграцию.
|
||||
- **Ручной Go-код (статус-кво):** максимум гибкости под нестандартную семантику (replace-массив, `no-needed`); цена — сервис-логика в `resources_core`, обход стража, тройной словарь.
|
||||
- **Иное (гибрид):** генерировать каркас из оверлея, а нетиповые куски (канон vIPConfigure) выносить в маленькие data-хуки; цена — усложнение шаблона ради 1-2 сервисов.
|
||||
|
||||
## M6 — где нужна глубина (прошу доступ)
|
||||
|
||||
Для однозначного вердикта по временному R1 (затирание vs досбор) не хватает **двух** файлов вне §4:
|
||||
1. Реализация `RunInstanceOperationUniversalWithDefaults` (ядро, `core/operation_run*.go` / `operation_cfs.go`) — шлёт ли пустой `ipSpaceName` как `""`.
|
||||
2. Генератор-хелпер `ParamFormat` (`TOOLS/resource-generator/internal/.../funcs.go`) — во что разворачивается null-строка в `params`.
|
||||
|
||||
Разрешить чтение этих двух файлов? Без них пункт помечен «не сверено».
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Ход 10 — Инструкция и пояснение пользователя (дословно)
|
||||
|
||||
```
|
||||
#attachment:Pasted text #1
|
||||
докуменитируй сразу
|
||||
|
||||
пояснияю - модификаторы - модет и слодная, новая, но НЕБОЛЬШАЯ часть всего нашего сервиса
|
||||
не нужно делать на них чрезмерный упор
|
||||
главное - САМ провайдер, его устойчивость и правильность и тд
|
||||
```
|
||||
|
||||
Разбор указания (для дальнейших раундов):
|
||||
|
||||
- **Документировать немедленно** — исполнено этим разделом.
|
||||
- **Приоритет — САМ провайдер**: его устойчивость и корректность. Модификаторы — новая и, возможно,
|
||||
сложная, но **НЕБОЛЬШАЯ** часть сервиса; чрезмерный упор на них делать не нужно.
|
||||
- Следствие: темы `M5`/`M6`/`U2` (глубина по модификаторам) — второстепенны по отношению к вопросам
|
||||
устойчивости/корректности ядра, генерации и всего провайдера в целом.
|
||||
|
||||
---
|
||||
|
||||
## Ход 10a — Запрос пользователя (дословно)
|
||||
|
||||
```
|
||||
твоё мнение ?
|
||||
```
|
||||
|
||||
Мнение агента по раунду 2 приведено ниже, внутри Хода 11 (раздел «Критическое мнение агента
|
||||
о раунде 2»).
|
||||
|
||||
---
|
||||
|
||||
## Ход 11 — Запрос пользователя (дословно)
|
||||
|
||||
```
|
||||
так дополни задание ему, пусть ещё анализирует НО не нало много - токены могути закончиться
|
||||
```
|
||||
|
||||
### Критическое мнение агента о раунде 2 (дословно, отправлено пользователю)
|
||||
|
||||
```
|
||||
**Что стало лучше**
|
||||
- **M1** — Opus честно перепроверил и исправился: `provider.go:182/186/187`. Совпадает с моим чтением → доверие к остальным номерам выросло.
|
||||
- **M2** — догадка устранена: приведена конкретная строка скрипта, область (только `TOOLS/`) и паттерн (`.ServiceID==N`/`.ID==N`). Это уже факт, и факт полезный: дерево `provider/` стражем не покрыто **вообще**.
|
||||
- **M3** — `S4` снят с текстуальной опорой (`ARCHITECTURE.md:134` в разделе про generated-код), `S2` переклассифицирован в пробел спеки с опорой (`:12`, `:110` vs `:13`, `:213`). Честно и корректно.
|
||||
- **U2** — новая ценная находка, которой у меня не было: **третий** словарь жизненного цикла — `delete_strategy` (compile-time в YAML) против runtime-флагов. Настоящий архитектурный дефект.
|
||||
- **M4** — шкала задана, `R5` понижен. Замечание принято по существу.
|
||||
|
||||
**Что остаётся слабым**
|
||||
- **M5 — это чтение кода, а не замер.** Вся защита от вечного diff держится на утверждении «в `Read` смысловое сравнение, и при совпадении state не перезаписывается». Это правдоподобно и проверяемо, но **не проверено прогоном** `terraform apply` ×2 / `plan`. По нашему же правилу «рекомендация без замера — догадка». Opus не предложил верификацию.
|
||||
- **Риск переоценён/недооценён.** Самое опасное (R1: `Update` инстанс-ресурса затирает `ipSpaceName`) осталось **нерешённым** — за M6. При этом Opus сам пишет, что для `vc_nsxt` modify содержит 5 параметров, и запись `params[372]` присутствует **всегда**. Если так — это не «война дрейфов», а **молчаливое затирание SNAT** при любой правке эджа. Это надо было ставить вопросом №1 прошлого раунда, а не откладывать.
|
||||
- **Ничего про сам провайдер.** Раунд целиком про модификаторы — ровно то, что вы просили не переоценивать. Ядро, генератор, устойчивость, корректность — не тронуто.
|
||||
|
||||
**Моё мнение**
|
||||
Раунд 2 — честный и в плюс (правки по номерам, снятие `S4`, находка `U2`). Но по вашему приоритету он **мимо цели**: модификаторы, а не провайдер; и при этом главный вопрос (затирание `ipSpaceName` при `Update` эджа + отсутствие замера) остался открытым. Логичный следующий ход — развернуть раунд на «провайдер целиком», а `M6` **отложить**, а не разворачивать.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Ход 12 — Задание агента (раунд 3) — текст, отправляемый Opus
|
||||
|
||||
Сохранён отдельным файлом: `NOTES/20_prompts/prompt_for_opus_provider_core_round3.md`.
|
||||
Содержимое (дословно):
|
||||
|
||||
```markdown
|
||||
# Промпт для Opus 4.8 — РАУНД 3: сам провайдер (устойчивость и корректность)
|
||||
|
||||
> Продолжение диалога. Раунды 1–2 — `HISTORY/OPUS/2026-09-30_provider_architecture_modifiers_dialog.md`.
|
||||
> Режим и формат ответа — как раньше: сжато, тезисы, `файл:строка`, без догадок.
|
||||
|
||||
---
|
||||
|
||||
## Вводная (смена приоритета)
|
||||
|
||||
Модификаторы (`nubes_vc_org_ip_allocation`, `nubes_vc_nsxt_snat`) — новая, возможно сложная, но
|
||||
**НЕБОЛЬШАЯ** часть сервиса. Чрезмерный упор на них не нужен.
|
||||
|
||||
**Главное — САМ провайдер: его устойчивость и корректность.**
|
||||
|
||||
- `M6` (доступ к двум файлам для доразбора R1) — **снять**; углубление по модификаторам больше не требуется.
|
||||
- Прошлые находки (`S1`, `S2`, `R2`–`R6`, `U1`–`U3`) **не повторять**.
|
||||
|
||||
## Бюджет (жёстко — экономим токены)
|
||||
|
||||
- Прочитать **не более 10 файлов** суммарно. Ранее прочитанные повторно не открывать.
|
||||
- Ответ — **не более 5 находок**, каждая **не более 3 строк**.
|
||||
- Никаких вступлений, повторения прошлых отчётов, «а ещё могу».
|
||||
|
||||
## Что анализировать (провайдер целиком)
|
||||
|
||||
1. **Жизненный цикл инстанс-ресурса:** `create` / adopt / `suspend` / resume / `modify` / redeploy /
|
||||
`delete` и повторный `apply` — где теряется корректность состояния.
|
||||
2. **Досылка и нормализация параметров** (`WithDefaults`, zero-value fallback, дефолты `map-fixed`,
|
||||
регистр UUID): где риск затереть значение или получить ложный diff.
|
||||
3. **`Read` / refresh:** устойчив ли state у генерируемых ресурсов; где возможен вечный diff.
|
||||
4. **Устойчивость ядра:** ретраи и ошибки, `LockInstance`, идемпотентность, поведение при дубликатах
|
||||
и параллельных `apply`.
|
||||
5. **Генератор → код:** какие классы дефектов порождает шаблон.
|
||||
|
||||
## Границы доступа
|
||||
|
||||
Разрешено читать (только это):
|
||||
|
||||
- `provider/internal/core/**`
|
||||
- `provider/internal/resources_core/**`
|
||||
- `provider/internal/provider/provider.go`
|
||||
- `TOOLS/resource-generator/internal/templates/**`
|
||||
- `TOOLS/resource-generator/internal/params/params.go`
|
||||
- `TOOLS/resource-generator/internal/helpers/helpers.go`
|
||||
- `TOOLS/resource-generator/internal/loader/loader.go`
|
||||
- `TOOLS/resource-generator/internal/writers/writers.go`
|
||||
|
||||
Запрещено: `HISTORY/**`, `NOTES/**`, `docs/**`, `HAR/**`, `TMP/**`, `DEV_STAND/**`, `TEST_STAND/**`,
|
||||
`PROD_STAND/**`, git-история. Нужен файл вне списка → вопрос мне, не чтение.
|
||||
|
||||
## Формат ответа
|
||||
|
||||
- **ТОП-5 находок** по убыванию критичности: суть → место (`файл:строка`) → последствие (≤ 3 строк).
|
||||
- Затем **одна строка**: что из этого проверяемо только замером (прогон `apply`/`plan`), а не чтением.
|
||||
- Находок меньше пяти — столько, сколько есть. Водой не добивать.
|
||||
|
||||
## Стоп-правило
|
||||
|
||||
Не хватает файла или данных — один короткий вопрос. Не догадываться.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Ход 12a — Сообщение пользователя и вопрос агента
|
||||
|
||||
Пользователь прислал **только путь к файлу**, без какой-либо инструкции:
|
||||
|
||||
```
|
||||
/home/naeel/TF/tf_provider/NOTES/20_prompts/prompt_for_opus_provider_core_round3.md
|
||||
```
|
||||
|
||||
Агент, не имея однозначной команды, задал уточняющий вопрос с вариантами (проверить пути из
|
||||
списка доступа / поправить текст задания / пересмотреть состав и границы / ничего не делать).
|
||||
**Пользователь вопрос отклонил (cancelled)** — никаких изменений не вносилось, уточнение осталось
|
||||
без ответа.
|
||||
|
||||
---
|
||||
|
||||
## Ход 13 — Ответ Opus на раунд 3 (дословно, как получен от пользователя)
|
||||
|
||||
### Сырой лог сессии
|
||||
|
||||
```
|
||||
Read
|
||||
|
||||
Ограничусь бюджетом. Посмотрю состав `core/` и выберу ключевые непрочитанные файлы.
|
||||
|
||||
Read
|
||||
|
||||
Read
|
||||
|
||||
Read
|
||||
|
||||
Прочитаю ещё 4 ключевых файла ядра: ожидание операций, поиск (дубликаты), нормализацию параметров, cfs.
|
||||
|
||||
Read
|
||||
|
||||
Read
|
||||
|
||||
Прочитаю ещё 2: состояние инстанса (проверка ретрая 401) и нормализацию параметров.
|
||||
|
||||
Read
|
||||
|
||||
Read
|
||||
|
||||
Бюджет: прочитано 6 файлов. ТОП-5 находок по устойчивости/корректности ядра.
|
||||
```
|
||||
|
||||
### Отчёт (дословно)
|
||||
|
||||
```markdown
|
||||
**1. Транзиентный 401 не ретраится нигде; `getInstanceStateWithRetry` — имя-обман (ретрая нет).**
|
||||
`instance_state.go:39` просто зовёт `doRequest` один раз; `http.go:118` `isRetryable` = {429,502,503,504}, **без 401**.
|
||||
Прямо противоречит ARCHITECTURE.md:105-108 («GetInstanceState retry 401», «doRequest treats 401 as retryable»). Кратковременный 401 Gateway роняет любой read → срыв plan/apply/поллинга.
|
||||
|
||||
**2. Ретраится только GET; любой POST падает намертво на транзиентном 502/503/сети.**
|
||||
`http.go:88` — retry только при `method=="GET"`.
|
||||
Сбой на `/instanceOperations`, `/instanceOperationCfsParams` или `run` рвёт операцию; при create остаётся осиротевший инстанс → следующий apply ловит страж дубликатов `instance_find.go:168`.
|
||||
|
||||
**3. Modify обрывается уже ПОСЛЕ создания операции, если чтение live-параметров сбойнуло.**
|
||||
`operation_run.go:150` (и симметрично bycode `operation_run_bycode.go:108`): `instanceLiveParams` err → `return`, но POST `/instanceOperations` уже прошёл.
|
||||
Любой транзиентный сбой чтения live → повисшая pending-операция, блокирующая инстанс до idle.
|
||||
|
||||
**4. Zero-value fallback угадывает тип по ПОДСТРОКЕ имени параметра.**
|
||||
`params.go:47`: при пустом `dataType` тип берётся из `name/code/label`, содержащих `array/list/map/json`.
|
||||
Строковый параметр с кодом вроде `whitelist`/`...json...` получит `"[]"`/`"{}"` вместо `""` → неверно-типизированное значение уходит в API на незаданном поле.
|
||||
|
||||
**5. Эквивалентность значения в `Read` захардкожена под `jsonEnv`/ref_svc — прочие строки пишутся как вернул API.**
|
||||
`state_refresh.go:143` спец-ветка только для `jsonEnv`; остальное — `ParseString(value)` «как есть» `state_refresh.go:168`. Регистровая нормализация есть лишь в ref_svc-пути `refsvc_resolve.go:29`.
|
||||
Для non-ref_svc строкового поля, где API канонизирует формат (регистр UUID и т.п.), plan≠state → вечный diff.
|
||||
|
||||
Проверяемо только замером (не чтением): №1–3 (поведение при флапающем Gateway, орфан-операции) — прогоном `apply`; №5 (вечный diff) — прогоном `plan`.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Ход 14 — Запрос пользователя (дословно)
|
||||
|
||||
```
|
||||
мнение ? более вопросов в том чате задават не получится - он уже большой и жрёт токены
|
||||
```
|
||||
|
||||
### Мнение агента о раунде 3 (дословно)
|
||||
|
||||
```
|
||||
**Сильное**
|
||||
- **#1** — самое ценное: `ARCHITECTURE.md:105-108` буквально обещает ретрай 401 для GET, а код, по его словам, 401 не ретраит. Если верно — это не «мелочь», а срыв `plan`/`apply`.
|
||||
- **#3** — правдоподобно и практично: обрыв modify уже **после** отправленного POST → висящая операция.
|
||||
- **#5** — бьёт в самое больное место Terraform (вечный diff).
|
||||
|
||||
**Где не верю / нужна аккуратность**
|
||||
- **#1 — слово «нигде» настораживает.** Спека говорит, что ретрай 401 живёт внутри `GetInstanceState`/`GetInstanceStateRaw`, а Opus цитирует `instance_state.go:39` — один вызов `doRequest`. Вполне возможно, что ретрай реализован в другой функции, и абсолютный вывод неверен. Проверять надо именно те функции, а не один call-site.
|
||||
- **#2 — подано как дефект, а это может быть защита.** Слепой ретрай POST `/instanceOperations` **опаснее** обрыва: рискует создать дубликат операции. Настоящий дефект — не «нет ретрая», а «нет идемпотентности / безопасного продолжения». В нынешней формулировке вывод вводит в заблуждение.
|
||||
- **#4 — нет оценки частоты.** Срабатывает только при **пустом** `dataType`. Без ответа «как часто он пуст» это теория, а не риск.
|
||||
- **#5 — возможно, уже покрыто.** По §6.5 `ARCHITECTURE_NEW.md` нормализация UUID при **сравнении** есть в `JSONStringsEquivalent` / `JsonNormalize` / `normalizeCompareValue` / `ParamsMatchForResume`. Прежде чем считать это багом, надо убедиться, что расхождение не гасится там.
|
||||
|
||||
**Общее**
|
||||
- Все пять — чтение кода; ни одна не измерена. Это **гипотезы**, и формально Opus это честно признал.
|
||||
- Раз чат исчерпан по токенам — дальнейшая проверка возможна только **локально**, своими силами: `grep` по `isRetryable`/`GetInstanceState`, сверка `state_refresh.go` с `normalizeCompareValue`.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Статус
|
||||
|
||||
- Раунды 1–3: все три отчёта получены. Раунд 3 уложился в бюджет (прочитано 6 файлов, 5 находок).
|
||||
- **Итог раунда 3 (ядро, устойчивость/корректность):**
|
||||
1. 401 не ретраится нигде; `isRetryable` без 401 — противоречие `ARCHITECTURE.md:105-108`.
|
||||
2. Ретрай только для GET; POST обрывается на 502/503/сети → риск осиротевших операций/инстансов.
|
||||
3. Modify может оборваться **после** создания операции (сбой чтения live) → повисшая pending-операция.
|
||||
4. Zero-value fallback определяет тип по подстроке имени → возможна неверная типизация незаданного поля.
|
||||
5. Нормализация значения в `Read` — только `jsonEnv`/ref_svc → вечный diff на прочих канонизируемых строках.
|
||||
- Находки №1–3 и №5 **не подтверждены замером** (по словам самого Opus) — перенесены как гипотезы.
|
||||
- **Чат с Opus исчерпан по токенам** (сообщено пользователем 2026-09-30): новые вопросы в него
|
||||
задавать нельзя; проверка находок возможна только локально.
|
||||
- **Мнения агента записаны по всем раундам:** раунд 1 — Ход 7; раунд 2 — Ход 11 (и Ход 10a);
|
||||
раунд 3 — Ход 14.
|
||||
- Артефакты: `752244f` — промпт раунда 1; `ea75507` — замечания раунда 2;
|
||||
`e46bc35` — задание раунда 3;
|
||||
`NOTES/20_prompts/prompt_for_opus_provider_architecture_modifiers_round2.md`,
|
||||
`NOTES/20_prompts/prompt_for_opus_provider_core_round3.md`.
|
||||
- Следующий шаг (2026-09-30): промпт для **DeepSeek Pro** —
|
||||
`NOTES/20_prompts/prompt_for_deepseek_pro_remediation_plan.md` (план правок кода/документации/архитектуры
|
||||
+ план проверки/тестов; гипотезы групп A/B переданы ему на верификацию; исполнять будет Copilot).
|
||||
- Настоящий документ ведётся по прямому указанию пользователя «документируй ВЕСЬ диалог,
|
||||
без сокращений».
|
||||
@@ -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 + name; у каждого стенда свой список)
|
||||
|
||||
Token options:
|
||||
- `TOKEN_FILE=/home/naeel/terra/HH-MM-SS.token`, or
|
||||
- `NUBES_API_TOKEN` directly
|
||||
|
||||
Example:
|
||||
```bash
|
||||
export TOKEN_FILE=/home/naeel/terra/08-33-41.token
|
||||
./TOOLS/scripts/01_generate_yamls.sh --profile TOOLS/config/dev
|
||||
```
|
||||
|
||||
## Step 2: Generate Go resources and docs
|
||||
|
||||
Script: `TOOLS/scripts/02_generate_resources_and_docs_v2.sh`
|
||||
|
||||
Example:
|
||||
```bash
|
||||
./TOOLS/scripts/02_generate_resources_and_docs_v2.sh --profile TOOLS/config/dev
|
||||
```
|
||||
|
||||
Outputs:
|
||||
- Go files in `generated/<stand>/go`
|
||||
- Docs in `generated/<stand>/docs`
|
||||
|
||||
Important:
|
||||
- The v2 script always rebuilds `resource-generator` and `docs-generator` from source before running.
|
||||
- Do not invoke stale binaries from `TOOLS/resource-generator/bin/` or `TOOLS/docs-generator/bin/` directly.
|
||||
|
||||
## Step 3: Build and upload provider
|
||||
|
||||
Script: `03_build_and_upload_provider.sh`
|
||||
|
||||
Uses `registry-server-build/build-provider.sh` and signs with:
|
||||
- `secrets/private_key.asc` (ignored by git)
|
||||
|
||||
Example:
|
||||
```bash
|
||||
export S3_ENDPOINT=https://s3.msk-1.ngcloud.ru
|
||||
export S3_ACCESS_KEY=...
|
||||
export S3_SECRET_KEY=...
|
||||
./TOOLS/scripts/03_build_and_upload_provider.sh --profile TOOLS/config/dev 2.0.18
|
||||
```
|
||||
|
||||
## Step 4: Build and publish docs
|
||||
|
||||
Script: `04_build_and_publish_docs.sh`
|
||||
|
||||
Example:
|
||||
```bash
|
||||
export S3_ENDPOINT=https://s3.msk-1.ngcloud.ru
|
||||
export S3_ACCESS_KEY=...
|
||||
export S3_SECRET_KEY=...
|
||||
./TOOLS/scripts/04_build_and_publish_docs.sh --profile TOOLS/config/dev 2.0.18
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- The GPG private key must remain stable across releases. Do not regenerate per build.
|
||||
- If the key is regenerated, the registry server must be updated to serve the new public key.
|
||||
- Terraform will fail with `authentication signature from unknown issuer` if the registry public key does not match the signing key.
|
||||
- `TOOLS/config/<стенд>/services_list.txt` — источник правды по тому, какие сервисы генерируются (у каждого стенда свой).
|
||||
- Если меняется версия провайдера — обновить `provider/main.go` (ранее `universal_rebuild/main.go` — устаревший путь).
|
||||
|
||||
## One-time GPG bootstrap (do this once, keep the key stable)
|
||||
|
||||
1) Generate and export keys (no passphrase):
|
||||
```bash
|
||||
GPG_DIR=${ROOT_DIR}/secrets
|
||||
GNUPGHOME=$(mktemp -d)
|
||||
cat > /tmp/gpg_batch <<'EOF'
|
||||
%no-protection
|
||||
Key-Type: RSA
|
||||
Key-Length: 4096
|
||||
Subkey-Type: RSA
|
||||
Subkey-Length: 4096
|
||||
Name-Real: tazet@narod.ru
|
||||
Name-Email: tazet@narod.ru
|
||||
Expire-Date: 0
|
||||
EOF
|
||||
gpg --batch --homedir "$GNUPGHOME" --gen-key /tmp/gpg_batch
|
||||
gpg --batch --homedir "$GNUPGHOME" --armor --export-secret-keys > "$GPG_DIR/private_key.asc"
|
||||
gpg --batch --homedir "$GNUPGHOME" --armor --export > "$GPG_DIR/public_key.asc"
|
||||
rm -rf "$GNUPGHOME" /tmp/gpg_batch
|
||||
```
|
||||
|
||||
2) Update registry server public key (ASCII Armor) in:
|
||||
- `registry-server-build/main.go`
|
||||
- `operator/cmd/registry/main.go`
|
||||
|
||||
3) Rebuild and redeploy the registry server (see `docs/50_history/00_system_mechanics.md`).
|
||||
|
||||
4) Build and upload provider artifacts as usual.
|
||||
|
||||
# check string
|
||||
@@ -4,11 +4,15 @@
|
||||
|
||||
**Первая цифра версии жёстко привязана к стенду. НЕ ПУТАТЬ.**
|
||||
|
||||
| Стенд | Namespace | Первая цифра | Профиль |
|
||||
| Стенд | Namespace | Диапазон | Профиль |
|
||||
|---|---|---|---|
|
||||
| **PROD** | `nubes` | `2.*` | `TOOLS/config/prod` |
|
||||
| **DEV** | `nubes-dev` | `3.*` | `TOOLS/config/dev` |
|
||||
| **TEST** | `nubes-test` | `5.*` | `TOOLS/config/test` |
|
||||
| **PROD** | `nubes` | `1.*` | `TOOLS/config/prod` |
|
||||
| **DEV** | `nubes-dev` | `2.*` | `TOOLS/config/dev` |
|
||||
| **TEST** | `nubes-test` | `3.*` | `TOOLS/config/test` |
|
||||
|
||||
> ⛔ ЛЕГАСИ (не использовать): `prod=2.*`, `dev=3.*`, `test=5.*`, `0.0.1`.
|
||||
> Примеры версий ниже в этом файле могут содержать легаси-номера — подставляйте актуальную
|
||||
> из `../VERSIONS.md`.
|
||||
|
||||
## Архитектура конфигурации
|
||||
|
||||
@@ -23,7 +27,7 @@ TOOLS/config/
|
||||
│ NUBES_API_ENDPOINT = ...dev...
|
||||
│ TOKEN_FILE = secrets/dev.token
|
||||
│ NAMESPACE = nubes-dev
|
||||
│ VERSION = 3.x.x
|
||||
│ VERSION = 2.x.x ← актуальную брать из VERSIONS.md
|
||||
│
|
||||
├── test/profile.env
|
||||
└── prod/profile.env
|
||||
@@ -65,7 +69,7 @@ cd ~/tf_provider
|
||||
### Шаг 3 — Собрать и залить в реестр
|
||||
|
||||
```bash
|
||||
./TOOLS/scripts/03_build_and_upload_provider.sh --profile TOOLS/config/dev 3.1.13
|
||||
./TOOLS/scripts/03_build_and_upload_provider.sh --profile TOOLS/config/dev 2.0.18
|
||||
```
|
||||
|
||||
Компилирует (linux/windows/darwin), подписывает GPG, заливает в S3.
|
||||
@@ -75,7 +79,7 @@ cd ~/tf_provider
|
||||
```bash
|
||||
./TOOLS/scripts/01_generate_yamls.sh --profile TOOLS/config/dev && \
|
||||
./TOOLS/scripts/02_generate_resources_and_docs_v2.sh --profile TOOLS/config/dev && \
|
||||
./TOOLS/scripts/03_build_and_upload_provider.sh --profile TOOLS/config/dev 3.1.13
|
||||
./TOOLS/scripts/03_build_and_upload_provider.sh --profile TOOLS/config/dev 2.0.18
|
||||
```
|
||||
|
||||
## Быстрая заливка (без перегенерации YAML/Go)
|
||||
@@ -83,14 +87,14 @@ cd ~/tf_provider
|
||||
Если YAML'ы и Go-код уже сгенерированы и не менялись — только шаг 3:
|
||||
|
||||
```bash
|
||||
# DEV
|
||||
./TOOLS/scripts/03_build_and_upload_provider.sh --profile TOOLS/config/dev 3.1.13
|
||||
# DEV (2.*)
|
||||
./TOOLS/scripts/03_build_and_upload_provider.sh --profile TOOLS/config/dev 2.0.18
|
||||
|
||||
# TEST
|
||||
./TOOLS/scripts/03_build_and_upload_provider.sh --profile TOOLS/config/test 5.1.17
|
||||
# TEST (3.*)
|
||||
./TOOLS/scripts/03_build_and_upload_provider.sh --profile TOOLS/config/test 3.0.1
|
||||
|
||||
# PROD
|
||||
./TOOLS/scripts/03_build_and_upload_provider.sh --profile TOOLS/config/prod 2.1.23
|
||||
# PROD (1.*)
|
||||
./TOOLS/scripts/03_build_and_upload_provider.sh --profile TOOLS/config/prod 1.0.1
|
||||
```
|
||||
|
||||
Креды S3 подхватываются из `secrets/.s3cfg_registry`. Или через env:
|
||||
@@ -129,7 +133,7 @@ curl -s https://tf-registry.containerk8s.services.ngcloud.ru/v1/providers/nubes/
|
||||
|
||||
## Актуальные версии
|
||||
|
||||
Файл [`VERSIONS.md`](VERSIONS.md) — единственный источник правды. После каждой заливки — обновить.
|
||||
Файл [`../VERSIONS.md`](../VERSIONS.md) — единственный источник правды. После каждой заливки — обновить.
|
||||
|
||||
## Terraform-конфиг пользователя
|
||||
|
||||
@@ -138,7 +142,7 @@ terraform {
|
||||
required_providers {
|
||||
nubes = {
|
||||
source = "tf-registry.containerk8s.services.ngcloud.ru/nubes-dev/nubes"
|
||||
version = "3.1.13"
|
||||
version = "2.0.18"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## 1. Где прописывать
|
||||
|
||||
Единственная точка входа — `devops/config/services_list.txt` (или профильный `profiles/{stand}/services_list.txt`).
|
||||
Единственная точка входа — `TOOLS/config/<стенд>/services_list.txt` (у каждого стенда свой список).
|
||||
|
||||
Формат строки:
|
||||
```
|
||||
@@ -158,21 +158,22 @@ operations:
|
||||
## 8. Быстрый старт: добавляем новый сервис
|
||||
|
||||
```bash
|
||||
# 1. Добавить строку в services_list.txt
|
||||
echo "200 my_new_service # Моя новая услуга" >> devops/config/services_list.txt
|
||||
# 1. Добавить строку в список сервисов нужного стенда
|
||||
# (TOOLS/config/dev|test|prod/services_list.txt)
|
||||
echo "200 my_new_service # Моя новая услуга" >> TOOLS/config/test/services_list.txt
|
||||
|
||||
# 2. Сгенерировать YAML (test-стенд)
|
||||
devops/01_generate_yamls.sh --profile devops/profiles/test
|
||||
./TOOLS/scripts/01_generate_yamls.sh --profile TOOLS/config/test
|
||||
|
||||
# 3. Сгенерировать Go-код + доки
|
||||
devops/02_generate_resources_and_docs_v2.sh --profile devops/profiles/test
|
||||
./TOOLS/scripts/02_generate_resources_and_docs_v2.sh --profile TOOLS/config/test
|
||||
|
||||
# 4. Проверить что появился файл
|
||||
ls devops/profiles/test/generated/resources_yaml/200_my_new_service.yaml
|
||||
ls devops/profiles/test/generated/go/200_my_new_service_resource.go
|
||||
ls generated/test/resources_yaml/200_my_new_service.yaml
|
||||
ls generated/test/go/200_my_new_service_resource.go
|
||||
|
||||
# 5. Собрать и задеплоить провайдер
|
||||
devops/03_build_and_upload_provider.sh --profile devops/profiles/test
|
||||
./TOOLS/scripts/03_build_and_upload_provider.sh --profile TOOLS/config/test
|
||||
|
||||
# 6. Написать тестовый манифест в TEST_STAND/my_new_service/main.tf
|
||||
# 7. terraform init && terraform plan && terraform apply
|
||||
@@ -1,5 +1,12 @@
|
||||
# План миграции в Nubes Managed Kubernetes — Инструкции для агента
|
||||
|
||||
> ⚠️ **Пути `universal_rebuild/*` в этом плане — от ПРЕЖНЕЙ раскладки репозитория.** Актуально:
|
||||
> `universal_rebuild/internal/*` → `provider/internal/*`; `universal_rebuild/resources_yaml` →
|
||||
> `generated/<стенд>/resources_yaml`; `universal_rebuild/tools/gen` → `TOOLS/resource-generator`.
|
||||
> Также план писался ДО смены схемы версий: актуально `prod=1.*`, `dev=2.*`, `test=3.*`.
|
||||
> Часть про миграцию `registry.kube5s.ru` → `registry.nubes.ru` — ИСТОРИЧЕСКАЯ: актуальный реестр
|
||||
> `tf-registry.containerk8s.services.ngcloud.ru` (бакет `nubes-terraform-registry`).
|
||||
|
||||
**Создан:** 2026-03-13 (Opus 4.6)
|
||||
**Исполнитель:** Sonnet 4.6
|
||||
**Статус:** Ожидает исполнения
|
||||
@@ -24,7 +31,7 @@ Nubes (nubes.ru) — российский cloud-провайдер, собств
|
||||
**Обязательно прочитать перед работой:**
|
||||
- `REPO_CONTENTS.md` — карта репозитория
|
||||
- `.github/copilot-instructions.md` — правила работы (IMMUTABILITY POLICY)
|
||||
- `docs/CODEBASE_ANALYSIS_AND_ROADMAP.md` — анализ кодовой базы
|
||||
- `NOTES/30_analysis/CODEBASE_ANALYSIS_AND_ROADMAP.md` — анализ кодовой базы
|
||||
|
||||
---
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
# HOW_TO — все инструкции проекта
|
||||
|
||||
Здесь лежат **общие инструкции**: как собрать/залить провайдер, как добавить сервис, как устроены
|
||||
процессы. Отсюда начинать, если нужно что-то «сделать руками».
|
||||
|
||||
> Публикуемая пользовательская документация — в `../docs/` (mkdocs).
|
||||
> Рабочие материалы (планы, промпты, анализы) — в `../NOTES/`.
|
||||
|
||||
---
|
||||
|
||||
## Индекс: что нужно → какой файл
|
||||
|
||||
| Нужно | Файл | Кому |
|
||||
|---|---|---|
|
||||
| **Собрать и залить провайдер** (YAML → Go → бинарник → S3) | [`HOWTO-UPLOAD.md`](HOWTO-UPLOAD.md) | Релиз-инженеру |
|
||||
| **Полный DevOps-ранбук пайплайна** (4 шага: генерация, ресурсы+доки, сборка, публикация доков) + GPG-bootstrap | [`DEVOPS_BUILD_PIPELINE.md`](DEVOPS_BUILD_PIPELINE.md) | DevOps |
|
||||
| **Добавить новый сервис** в провайдер (полный цикл) | [`HOWTO_ADD_NEW_SERVICE.md`](HOWTO_ADD_NEW_SERVICE.md) | Разработчику провайдера |
|
||||
| **Имплементировать новый managed-сервис** (со стороны облака) | [`HOWTO_IMPLEMENT_NEW_CLOUD_SERVICE.md`](HOWTO_IMPLEMENT_NEW_CLOUD_SERVICE.md) | DevOps облака |
|
||||
| **Понять, как всё устроено на практике** (закрытый developer-guide) | [`howitwasdone.md`](howitwasdone.md) | Разработчику провайдера |
|
||||
| **Генерация документации** (архитектура, пайплайн, правила для LLM) | [`LLM_DOCS_GENERATION.md`](LLM_DOCS_GENERATION.md) | Разработчику доков |
|
||||
| **План миграции + runbook реестра** (обновление, откат, troubleshooting, мониторинг) | [`MIGRATION_PLAN_FOR_AGENT.md`](MIGRATION_PLAN_FOR_AGENT.md) | Агенту/инженеру |
|
||||
|
||||
---
|
||||
|
||||
## Короткий путь: собрать и залить (3 шага)
|
||||
|
||||
```bash
|
||||
cd /home/naeel/TF/tf_provider
|
||||
./TOOLS/scripts/01_generate_yamls.sh --profile TOOLS/config/dev
|
||||
./TOOLS/scripts/02_generate_resources_and_docs_v2.sh --profile TOOLS/config/dev
|
||||
./TOOLS/scripts/03_build_and_upload_provider.sh --profile TOOLS/config/dev 2.0.18
|
||||
```
|
||||
|
||||
Полные детали, требования, проверка после заливки и структура S3 — в [`HOWTO-UPLOAD.md`](HOWTO-UPLOAD.md).
|
||||
|
||||
## Текущие версии (источник правды)
|
||||
|
||||
[`../VERSIONS.md`](../VERSIONS.md). Схема нумерации: **prod = `1.*`, dev = `2.*`, test = `3.*`**
|
||||
(легаси `prod=2.*`, `dev=3.*`, `test=5.*`, `0.0.1` — НЕ использовать). Обоснование схемы:
|
||||
[`../NOTES/10_plans/PLAN_FLASH_reversion_cleanup.md`](../NOTES/10_plans/PLAN_FLASH_reversion_cleanup.md).
|
||||
|
||||
---
|
||||
|
||||
## Где лежит остальное (чтобы не искать вслепую)
|
||||
|
||||
| Тема | Где |
|
||||
|---|---|
|
||||
| Операционные runbook'и (API-токены, стенды, мониторинг, откат, тестирование, реестр) | `../docs/ops/` |
|
||||
| Внутренние справки/разборы по сборке и архитектуре | `../docs/help/` (напр. `BUILD.md`, `build-and-publish.md`) |
|
||||
| Пайплайн публикации документации | `../DOCS_PIPELINE/README.md`, `../DOCS_PIPELINE/publish-docs.sh` |
|
||||
| Правила генерации кода провайдера (ОБЯЗАТЕЛЬНЫ для генератора) | `../TOOLS/ARCHITECTURE.md` |
|
||||
| Скрипты пайплайна | `../TOOLS/scripts/` |
|
||||
| Конфиги стендов и общий реестр | `../TOOLS/config/` (`registry.env`, `<стенд>/profile.env`, `<стенд>/services_list.txt`) |
|
||||
| Секреты (не коммитить) | `../secrets/` |
|
||||
| Текущая задача по IaC/`modify` | `../NOTES/40_chat_summaries/CHAT_RESUME_IAC_2026-09-24.md` |
|
||||
|
||||
---
|
||||
|
||||
## ⛔ Частые грабли (не наступать)
|
||||
|
||||
- **Не вызывать** устаревшие бинарники `TOOLS/*/bin/` — скрипт `02_*` сам пересобирает генераторы.
|
||||
- **Не путать** схемы версий: только `prod=1.*`, `dev=2.*`, `test=3.*`.
|
||||
- **Не использовать** старый API `index.cfm` и хосты `registry.kube5s.ru` / `deck-api.ngcloud.ru` — закрыты.
|
||||
- **GPG-ключ** подписи не перегенерировать: иначе registry и `terraform init` сломаются
|
||||
(`authentication signature from unknown issuer`).
|
||||
- **S3-бакеты разделены**: бинарники — `nubes-terraform-registry`, документация — `terraform-registry`.
|
||||
@@ -2,6 +2,13 @@
|
||||
<!-- Актуальный API: https://lk-api-gateway.ngcloud.ru/api/v1/svc -->
|
||||
# How It Was Done — Developer Guide (закрытая страница)
|
||||
|
||||
> ⚠️ **Пути в этом документе — от ПРЕЖНЕЙ раскладки репозитория (`universal_rebuild/*`).**
|
||||
> Актуальное соответствие: `universal_rebuild/internal/*` → `provider/internal/*`;
|
||||
> `universal_rebuild/resources_yaml` → `generated/<стенд>/resources_yaml`;
|
||||
> `universal_rebuild/tools/gen` → `TOOLS/resource-generator`;
|
||||
> `universal_rebuild/tools/service_params_gen` → `TOOLS/yaml-generator`.
|
||||
> Смысл описанного сохраняется, но пути в тексте сверять по этому соответствию.
|
||||
|
||||
**Filename & Versioning:** howitwasdone.md / 2026‑02‑04 / Draft v1
|
||||
|
||||
Этот документ — единый технический мануал. Он доступен только по прямой ссылке и не включён в публичную навигацию.
|
||||
@@ -0,0 +1,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)
|
||||
|
||||
> Для Flash. Генерацию выполняет Flash по этому плану. Документацию НЕ трогать.
|
||||
@@ -53,16 +57,16 @@ cd /home/naeel/TF/tf_provider
|
||||
|
||||
| Стенд | Namespace | Бинарники в S3 |
|
||||
|---|---|---|
|
||||
| dev | `nubes-dev` | `nubes-terraform-registry/tf-registry.containerk8s.services.ngcloud.ru/nubes-dev/nubes/0.0.1/` |
|
||||
| test | `nubes-test` | `.../nubes-test/nubes/0.0.1/` |
|
||||
| prod | `nubes` | `.../nubes/nubes/0.0.1/` |
|
||||
| dev | `nubes-dev` | `terraform-registry/tf-registry.containerk8s.services.ngcloud.ru/nubes-dev/nubes/0.0.1/` |
|
||||
| test | `nubes-test` | `terraform-registry/tf-registry.containerk8s.services.ngcloud.ru/nubes-test/nubes/0.0.1/` |
|
||||
| prod | `nubes` | `terraform-registry/tf-registry.containerk8s.services.ngcloud.ru/nubes/nubes/0.0.1/` |
|
||||
|
||||
## Предусловия — ПРОВЕРЕНО, всё готово
|
||||
- Go 1.23.1, docker 29.1.3, `mc`, GPG (`secrets/private_key.asc`, `public_key.asc`).
|
||||
- Токены API: `secrets/{dev,test,prod}.token` на месте.
|
||||
- API-эндпоинты доступны (HTTP 403 без токена — ожидаемо, токен передаёт 01).
|
||||
- `TOOLS/config/<стенд>/operation_timeouts.json` на месте.
|
||||
- `registry.env`: `REGISTRY_HOSTNAME=tf-registry.containerk8s.services.ngcloud.ru`, `S3_BUCKET=nubes-terraform-registry`.
|
||||
- `registry.env`: `REGISTRY_HOSTNAME=tf-registry.containerk8s.services.ngcloud.ru`, `S3_BUCKET=terraform-registry`.
|
||||
|
||||
## Чего НЕ делать
|
||||
- НЕ запускать `04_build_and_publish_docs.sh` (документация не нужна сейчас).
|
||||
@@ -0,0 +1,23 @@
|
||||
# 10_plans — планы работ
|
||||
|
||||
Планы, связанные с версионированием, перегенерацией провайдера и модификаторами.
|
||||
|
||||
## Файлы
|
||||
|
||||
| Файл | О чём | Статус |
|
||||
|---|---|---|
|
||||
| `PLAN_FLASH_reversion_cleanup.md` | Чистка реестра (S3) от легаси-версий + новая схема нумерации: **prod=`1.*`, dev=`2.*`, test=`3.*`**; порядок перегенерации стендов | ✅ Актуально (источник схемы нумерации) |
|
||||
| `PLAN_modifier_redesign.md` | Редизайн «ресурсов-модификаторов» (`kind: modifier`) — шаги 1..10, `delete_strategy`, `idempotency`, `inverse` | ⛔ **Отменённый путь** (баннер в файле): логику вшивали в универсальный генератор |
|
||||
| `PLAN_regenerate_providers_0.0.1.md` | Перегенерация всех стендов версией `0.0.1` | ⚠️ **Перекрыт**: `0.0.1` объявлен легаси в `PLAN_FLASH_reversion_cleanup.md` |
|
||||
|
||||
## Как читать
|
||||
|
||||
- Схема нумерации и порядок заливки — только из `PLAN_FLASH_reversion_cleanup.md`.
|
||||
- `PLAN_modifier_redesign.md` читать **только как историю**: он описывает заход, от которого отказались
|
||||
(метки `kind: modifier` в YAML + реестр в `yaml-generator`). Актуальные выводы по модификаторам —
|
||||
в `../40_chat_summaries/CHAT_RESUME_IAC_2026-09-24.md`.
|
||||
|
||||
## Не путать
|
||||
|
||||
Отмена `PLAN_modifier_redesign.md` **не означает**, что модификаторы не нужны. Нужны **отдельные
|
||||
tf-ресурсы под `modify`**, но без доменных меток в универсальном YAML — см. хендовер.
|
||||
@@ -0,0 +1,45 @@
|
||||
# 20_prompts — промпты для LLM
|
||||
|
||||
Промпты, которые отправлялись внешним моделям (Opus / Sol / Sonnet / DeepSeek Flash / GPT‑5.2‑Codex).
|
||||
Ответы моделей лежат отдельно — в `../30_analysis/` и `../40_chat_summaries/`.
|
||||
|
||||
> ⛔ = отменённый («ложный») путь, только история. ✅ = актуально.
|
||||
|
||||
## Статус: актуальное
|
||||
|
||||
| Файл | О чём | Статус |
|
||||
|---|---|---|
|
||||
| `prompt_for_opus_iac_shturval_modify.md` | IaC-развёртывание Штурвала: проблема `modify` и скрытых зависимостей (5 вопросов). **Факты внутри исправлены** (vIPConfigure — replace-семантика, а не накопительная) | ✅ Актуально |
|
||||
| `prompt_for_opus_modifier_global_architecture.md` | Как сделать модификаторы **НЕ инвазивным дополнением**: YAML — чистая выгрузка API, модификаторы — не ветка генератора | ✅ Актуальное направление (не реализовано) |
|
||||
| `prompt_for_opus_modifiable_architecture.md` | Простая логика «изменяемости» параметров (CreateOnly vs Modifiable) | ⚠️ Статус не определён |
|
||||
| `prompt_for_opus_review.md` | Код-ревью + оценка архитектуры, 3 задачи roadmap (в т.ч. `vcOrg modify` — динамическая аллокация IP) | ⚠️ Статус не определён; ответ — `../30_analysis/opus_review_answer.md` |
|
||||
|
||||
## Статус: отменённый заход (модификаторы со метками в YAML)
|
||||
|
||||
| Файл | О чём |
|
||||
|---|---|
|
||||
| `prompt_for_opus_modifier_architecture_full.md` ⛔ | Спроектировать с нуля архитектуру `kind: modifier` |
|
||||
| `prompt_for_opus_modifier_architecture_q3.md` ⛔ | Уточнения к архитектуре модификаторов (расхождения с кодом) |
|
||||
| `prompt_for_opus_modifier_null_bug.md` ⛔ | Баг: modify-модификатор сбрасывает create-поля (`needEnableAVI` true→false) |
|
||||
| `prompt_for_opus_modifier_plan_review.md` ⛔ | Ревью плана редизайна модификаторов |
|
||||
| `prompt_for_opus_modifiers_review.md` ⛔ | Код-ревью модификаторов в универсальном провайдере |
|
||||
| `prompt_for_opus_modifier_review_2.md` ⛔ | Ревью `vc_nsxt` / `vc_org` + досылка modify-params через `paramValue` |
|
||||
| `prompt_for_opus_inverse_architecture.md` ⚠️ | Архитектура inverse-отката модификаторов (`delete_params`, `zero_count`, `off_value`). Модель — от отменённого механизма; факты внутри (count=0, `no-needed`) переиспользуются |
|
||||
|
||||
## Промпты по другим темам (не про модификаторы)
|
||||
|
||||
| Файл | О чём |
|
||||
|---|---|
|
||||
| `prompt_for_opus_bugs.md` | Баг: `FindInstanceByDisplayName` не находит существующий инстанс |
|
||||
| `prompt_for_opus_duplicate.md` | subresource duplicate/exist + `state_out` |
|
||||
| `prompt_for_flash_fix_tainted_replace.md` | ТЗ для DeepSeek Flash: убрать create-time проверку существования из `ModifyPlan` |
|
||||
| `prompt_for_sol_dev_generator_bug.md` | Проверка решения бага Dev-генератора |
|
||||
| `prompt_for_sonnet_docs_ux.md` | BRIEF: анализ и рекомендации по документации провайдера (UX) |
|
||||
| `prompt_deepseek_flash.txt` | Роль: технический редактор документации; правила «не менять параметры/типы/ID» |
|
||||
| `gpt5_5.2_codex_universal_provider_prompt.md` | Задачи по `terra`-провайдеру (Codex) |
|
||||
|
||||
## Ответы на эти промпты
|
||||
|
||||
- `../30_analysis/opus_review_answer.md` — ответ на `prompt_for_opus_review.md`
|
||||
- `../30_analysis/OPUS_ANSWER_IAC_SHTURVAL_MODIFY_2026-09-23.md` — ответ на `prompt_for_opus_iac_shturval_modify.md`
|
||||
- `../HISTORY/OPUS/` и `../HISTORY/SONNET/` — исторические ответы по датам
|
||||
+2
-2
@@ -15,9 +15,9 @@ Constraints:
|
||||
---
|
||||
|
||||
## 2) Файлы для изучения (указать полные пути)
|
||||
- docs/ARCHITECTURE_NEW.md — архитектурный обзор
|
||||
- NOTES/30_analysis/ARCHITECTURE_NEW.md — архитектурный обзор
|
||||
- docs/README.md, docs/index.md — документация / навигация
|
||||
- docs/howitwasdone.md — история решений
|
||||
- NOTES/50_process/howitwasdone.md — история решений
|
||||
- docs/ai_universal_provider_gen.md — процесс генерации провайдера (YAML → Go)
|
||||
- universal_rebuild/tools/gen/main.go — генератор (основная логика)
|
||||
- universal_rebuild/internal/resources_gen/* — примеры сгенерированных ресурсов
|
||||
@@ -0,0 +1,116 @@
|
||||
# Промпт для DeepSeek Pro — план правок кода/документации/архитектуры + стратегия проверки
|
||||
|
||||
**Репо:** `/home/naeel/TF/tf_provider` — `terraform-provider-nubes` (Go, Terraform Plugin Framework).
|
||||
**Дата:** 2026-09-30. **Стенд-источник фактов:** `dev`.
|
||||
|
||||
---
|
||||
|
||||
## Роль и исполнитель
|
||||
|
||||
- Ты — архитектор / ведущий инженер.
|
||||
- Ты **НЕ пишешь код** и **НЕ меняешь файлы**. Только анализ + план.
|
||||
- Результат твоей работы — **план правок** (код, документация, архитектура) и **план проверки/тестов**.
|
||||
- **Исполнять будет другой агент (Copilot)** — строго по твоему плану. Поэтому план должен быть
|
||||
исполнимым: точные пути файлов, функции, что именно менять, чем проверять.
|
||||
|
||||
## Вводная
|
||||
|
||||
- Главное — **сам провайдер**: устойчивость, корректность, отсутствие вечных diff.
|
||||
- Модификаторы (`nubes_vc_org_ip_allocation`, `nubes_vc_nsxt_snat`) — новая, возможно сложная, но
|
||||
**НЕБОЛЬШАЯ** часть сервиса. Не раздувай их.
|
||||
- Ниже — **ГИПОТЕЗЫ** предыдущего анализа. Они **НЕ подтверждены замером**.
|
||||
Твоя первая задача — проверить их по коду и разделить на «подтверждено / опровергнуто / нужен замер».
|
||||
|
||||
## Гипотезы к проверке
|
||||
|
||||
### Группа A — ядро (приоритет)
|
||||
- **A1.** 401 не ретраится: `provider/internal/core/instance_state.go:39` зовёт `doRequest` один раз;
|
||||
`isRetryable` в `provider/internal/core/http.go:118` = {429,502,503,504} без 401.
|
||||
Противоречит `TOOLS/ARCHITECTURE.md:105-108` («GetInstanceState retry 401», «doRequest treats 401 as retryable»).
|
||||
- **A2.** Ретрай только для GET (`http.go:88`); POST (`/instanceOperations`, `/instanceOperationCfsParams`,
|
||||
`run`) обрывается на транзиентном 502/503/сети → возможны осиротевшая операция/инстанс; далее срабатывает
|
||||
страж дубликатов (`instance_find.go:168`).
|
||||
- **A3.** Modify может оборваться **после** создания операции, если чтение live-параметров сбойнуло:
|
||||
`operation_run.go:150` и `operation_run_bycode.go:108` (`instanceLiveParams` err → `return`).
|
||||
- **A4.** Zero-value fallback угадывает тип по **подстроке** имени параметра при пустом `dataType`:
|
||||
`params.go:47` (ищет `array/list/map/json` в `name/code/label`).
|
||||
- **A5.** В `Read` нормализация значения только для `jsonEnv`/ref_svc: `state_refresh.go:143` и `:168`;
|
||||
регистровая нормализация только в ref_svc-пути (`refsvc_resolve.go:29`) → возможен вечный diff.
|
||||
|
||||
### Группа B — модификаторы (второстепенно)
|
||||
- **B1.** `TOOLS/ARCHITECTURE.md:225` («Exception Registry») ссылается на реестр `serviceSpecificModifiers`
|
||||
в `TOOLS/yaml-generator/main.go`, которого **нет** (удалён при рефакторинге; см. комментарий `main.go:80-90`).
|
||||
- **B2.** Спека не описывает **ручные сервисные оверлеи** (`provider/internal/resources_core/org_ip_allocation_resource.go`,
|
||||
`nsxt_snat_resource.go`) — нет категории «ручной сервисный ресурс», неясно, входит ли `resources_core` в «core».
|
||||
- **B3.** Страж `TOOLS/scripts/check_hardcoded_service_ids.sh:15` сканирует **только** `TOOLS/`
|
||||
и ловит **только** `.ServiceID==N`/`.ID==N` → hardcoded `19`/`22`/`"no-needed"` вне охвата.
|
||||
- **B4.** Порядок «edge → аллокация» не гарантируется провайдером — держится на пользовательском `depends_on`
|
||||
(`DEV_STAND/FullPipe/modifiers.tf`).
|
||||
- **B5.** У `nubes_vc_org_ip_allocation` нет способа снять аллокацию через атрибут (пустой массив запрещён) — только `destroy`.
|
||||
- **B6.** `modify` выполняется всегда (нет pre-check идемпотентности), хотя idempotent-путь в ядре есть.
|
||||
- **B7.** `ImportState` модификаторов не заполняет Required-атрибуты.
|
||||
- **B8.** `modifiers.yaml` (оверлей) **не существует**, при этом генерируемый слой модификаторов готов
|
||||
(`TOOLS/resource-generator/internal/loader/loader.go:57-86`, `internal/templates/modifier.go`) и не используется.
|
||||
- **B9.** Три несогласованных словаря жизненного цикла: у генерируемых ресурсов `suspend_on_destroy`/`keep_on_destroy`,
|
||||
у генерируемых модификаторов `delete_strategy` (compile-time), у ручных модификаторов снова `keep_on_destroy`.
|
||||
|
||||
## Что нужно на выходе (строго в этом порядке)
|
||||
|
||||
**A. Верификация гипотез**
|
||||
Таблица: `№ | подтверждено / опровергнуто / нужен замер | опора (файл:строка) | примечание`.
|
||||
Опровергнутые — обосновать, почему вывод неверен.
|
||||
|
||||
**B. План правок кода**
|
||||
Таблица: `№ | файл | функция/место | что изменить | зачем | риск (низк/сред/высок) | ломает ли совместимость`.
|
||||
Только правки, вытекающие из подтверждённых пунктов. Никаких «заодно улучшим».
|
||||
|
||||
**C. План правок документации и архитектуры**
|
||||
Что именно и в каком файле (`TOOLS/ARCHITECTURE.md`, `README.md`, `VERSIONS.md`, `HOW_TO/**`, `docs/**`).
|
||||
Отдельно: какие **архитектурные решения** надо зафиксировать (напр. единый словарь жизненного цикла).
|
||||
|
||||
**D. План проверки и тестирования**
|
||||
Для каждой правки — три уровня:
|
||||
1. **Юнит/пакетный тест** — какой пакет, что проверяет, где лежит (есть примеры: `provider/internal/resources_core/*_test.go`,
|
||||
`TOOLS/resource-generator/internal/loader/loader_modifier_test.go`).
|
||||
2. **Интеграционная проверка** — `terraform plan` / `apply` на `DEV_STAND/FullPipe`: что запустить, что ожидать
|
||||
в выводе, на что смотреть (с учётом: `apply`/`destroy` выполняет **владелец**, не агент).
|
||||
3. **Регрессия** — что ещё может сломаться и как это поймать.
|
||||
Плюс статические стражи: `TOOLS/scripts/check_generated_drift.sh`, `check_hardcoded_service_ids.sh`,
|
||||
сборка `03_build_and_upload_provider.sh`, `dev-materialize.sh`.
|
||||
|
||||
**E. Порядок работ**
|
||||
Шаги, сгруппированные в **отдельные коммиты**, от безопасных к рискованным. Для каждого шага — 1 строка:
|
||||
что делаем и как проверяем, после чего фиксируем коммитом.
|
||||
|
||||
**F. Открытые вопросы**
|
||||
Только то, что нельзя выяснить из кода (значения, решения владельца).
|
||||
|
||||
## Ограничения
|
||||
|
||||
- **Ничего не менять**: не править файлы, не коммитить, не запускать `terraform`/`go`.
|
||||
- `terraform apply` и `destroy` — **только владелец**.
|
||||
- Каждое утверждение — с `файл:строка`. **Факт и предположение разделяй явно.**
|
||||
- Бюджет: читать **не более ~20 файлов**; ответ — компактный, таблицами, **без воды и без «а ещё могу»**.
|
||||
- Не предлагать переписывание с нуля без доказанной необходимости.
|
||||
|
||||
## Границы доступа
|
||||
|
||||
Разрешено читать:
|
||||
- `TOOLS/**` (архитектура, генераторы, скрипты, конфиги)
|
||||
- `provider/**` (кроме `artifacts/`, `bin/`, `generated/`)
|
||||
- `generated/dev/resources_yaml/**`
|
||||
- `docs/**`, `HOW_TO/**`, `README.md`, `VERSIONS.md`
|
||||
- `DEV_STAND/FullPipe/**` (пример использования)
|
||||
|
||||
Запрещено: `HISTORY/**`, `NOTES/**`, `TMP/**`, `HAR/**`, `secrets/**`, `! /`, `.git` (история коммитов).
|
||||
Нужен файл вне списка — задай вопрос, не читай.
|
||||
|
||||
## Формат ответа
|
||||
|
||||
- Сжато, тезисами, таблицами. Каждое утверждение проверяемо (`файл:строка`).
|
||||
- Никаких вступлений, повторения вводной, «лирики».
|
||||
|
||||
## Стоп-правило
|
||||
|
||||
Задание неоднозначно или данных не хватает — **остановиться и задать один короткий вопрос**.
|
||||
Не достраивать смысл и не действовать по догадке.
|
||||
@@ -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,43 @@
|
||||
# Opus — код-ревью правок (раунд 5, 2026-09-30)
|
||||
|
||||
**Репо:** `/home/naeel/TF/tf_provider` (Go, Terraform Plugin Framework). Правки уже влиты в `master`,
|
||||
`go build ./...` и `go test ./internal/... -short` — зелёные. Нужно только ревью.
|
||||
|
||||
## Формат ответа (ЖЁСТКО)
|
||||
|
||||
- **Только дефекты.** По 1 строке: `файл:строка` → что не так → чем грозит.
|
||||
- **Весь ответ ≤ 10 строк.** Дефектов нет — ответ «ок».
|
||||
- Без похвал, пересказа, «а ещё можно», без предложений рефакторинга.
|
||||
|
||||
## Что ревьюить (ровно эти коммиты)
|
||||
|
||||
```
|
||||
git --no-pager show 383f8ea c5a4499 ea75cac 8519ba0 9da9766
|
||||
# или сразу:
|
||||
git --no-pager diff 047d53a^..9da9766 -- provider/ TOOLS/
|
||||
```
|
||||
|
||||
Файлы:
|
||||
- `provider/internal/core/http.go` — 401 в `isRetryable`
|
||||
- `provider/internal/core/modifier_compare.go` — `modifierDesiredEqualsLive`, `modifierValuesEqual`, `modifierCodeMap`
|
||||
- `provider/internal/core/operation_run_bycode.go` — idempotency pre-check по live
|
||||
- `provider/internal/core/instance_create.go` — `instanceUid` возвращается вместе с ошибкой
|
||||
- `provider/internal/resources_core/nsxt_snat_resource.go`, `org_ip_allocation_resource.go` — `ImportState`, `ByIdempotent`
|
||||
- `TOOLS/resource-generator/internal/templates/instance.go` — partial state в `Create`
|
||||
- `TOOLS/scripts/check_hardcoded_service_ids.sh` — расширение области/паттернов
|
||||
|
||||
## Вопросы (ответ — по 1 строке на каждый, дефект или «ок»)
|
||||
|
||||
1. **Q5 partial state.** `Create`: при `err != nil && id != ""` вызывается `resp.State.Set` с планом,
|
||||
затем `AddError`. Не нарушает ли это контракт framework (допустим ли partial state при ошибке)?
|
||||
2. **Live pre-check.** `modifierDesiredEqualsLive` ищет live-значение через `lookupLiveParam`
|
||||
(ключи `Code`/`SvcOperationCfsParam`/`Name`/`Label`). Достаточно ли этого, чтобы не пропустить
|
||||
нужный `modify`?
|
||||
3. **`isRetryable` + 401.** Не создаёт ли ретрай 401 ложных повторов там, где это опасно (GET-пути)?
|
||||
4. **`ImportState`.** Запись Required-атрибутов в `ImportState` не конфликтует ли с последующим `Read`?
|
||||
5. Что-то ещё критичное в этих диффах — 1 строка.
|
||||
|
||||
## Границы доступа
|
||||
|
||||
Разрешено: `provider/internal/**`, `TOOLS/**`, `git show`/`git diff` по перечисленным коммитам.
|
||||
Запрещено: `HISTORY/**`, `NOTES/**`, `docs/**`, `HAR/**`, стенды. Нужен файл вне списка → вопрос.
|
||||
@@ -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,106 @@
|
||||
# Промпт для Opus 4.8 — анализ архитектуры провайдера + ресурсов-модификаторов
|
||||
|
||||
**Репо:** `/home/naeel/TF/tf_provider` — `terraform-provider-nubes` (Go, Terraform Plugin Framework).
|
||||
**Дата:** 2026-09-30. **Стенд-источник фактов:** `dev`.
|
||||
|
||||
---
|
||||
|
||||
## Роль и режим работы
|
||||
|
||||
- Ты — архитектор/ревьюер универсального Terraform-провайдера.
|
||||
- Работаем **в диалоге**: я (агент) передаю твои вопросы пользователю и возвращаю его ответы.
|
||||
- Вопрос задавай ТОЛЬКО если без него ответить нельзя. Максимум 1–2 вопроса за раз, предельно коротко.
|
||||
- Не догадываться. Нет данных — вопрос, а не допущение.
|
||||
- Область не расширять: отвечать ровно на поставленную задачу.
|
||||
|
||||
## Задача
|
||||
|
||||
Проанализировать архитектуру универсального провайдера Nubes и встроенный в неё слой
|
||||
**ресурсов-модификаторов** — отдельных ресурсов, которые вызывают операцию `modify`
|
||||
у родительского инстанса (когда нужного параметра нет в операции `create`).
|
||||
|
||||
Оценить:
|
||||
|
||||
1. Как устроена архитектура по слоям и как течёт поток данных (API → YAML → код → API).
|
||||
2. Соответствие заявленной спеки (`TOOLS/ARCHITECTURE.md`) фактической реализации — все
|
||||
расхождения, с указанием `файл:строка`.
|
||||
3. Корректность жизненного цикла модификаторов: `Create` / `Read` / `Update` / `Delete`,
|
||||
идемпотентность, дрейф (drift), поведение при `replace` / повторном `apply`, импорт.
|
||||
4. Место модификаторов в универсальном ядре: где и как нарушается принцип
|
||||
«ядро универсально, доменные знания — только данные». Насколько оправдано текущее
|
||||
решение (ручные Go-ресурсы, зарегистрированные поверх генерируемых).
|
||||
5. Границы ответственности: что модификатор делает сам, что отдаёт платформе; как
|
||||
выражается обратная операция (откат при `destroy`, значение «выключено»).
|
||||
6. Риски и топ-проблемы — по убыванию критичности, каждое с `файл:строка`.
|
||||
|
||||
## Границы доступа (ЖЁСТКО)
|
||||
|
||||
Читать РАЗРЕШЕНО **только** файлы из списка ниже. Всё остальное — ЗАПРЕЩЕНО, в частности:
|
||||
|
||||
- `HISTORY/**`, `NOTES/**`, `TMP/**`, `HAR/**`, `docs/**`, `site/**`, `site_test/**`,
|
||||
`apps/**`, `charts/**`, `FIYR_MGU/**`, `gateway/**`, `scripts/**`, `secrets/**`,
|
||||
`tfflaskcrud/**`, `tfluceecrud/**`, `tfnodejscrud/**`, `DEV_STAND/**` (кроме одного файла
|
||||
из списка), `TEST_STAND/**`, `PROD_STAND/**`, `provider/artifacts/**`, `provider/bin/**`;
|
||||
- история git (`git log`, `git show`, `git diff` с коммитами), коммиты, теги, ветки;
|
||||
- любой файл репозитория, которого нет в списке ниже.
|
||||
|
||||
Нужен файл вне списка → НЕ читать, а задать мне вопрос.
|
||||
|
||||
## Файлы к изучению (исчерпывающий список)
|
||||
|
||||
### Группа 1. Архитектура (спека)
|
||||
- `TOOLS/ARCHITECTURE.md`
|
||||
|
||||
### Группа 2. Ресурсы-модификаторы и их регистрация
|
||||
- `provider/internal/provider/provider.go`
|
||||
- `provider/internal/resources_core/org_ip_allocation_resource.go`
|
||||
- `provider/internal/resources_core/nsxt_snat_resource.go`
|
||||
- `provider/internal/resources_core/org_ip_allocation_test.go`
|
||||
|
||||
### Группа 3. Рантайм-зависимости модификаторов (ядро)
|
||||
- `provider/internal/core/client.go`
|
||||
- `provider/internal/core/operation_run_bycode.go`
|
||||
- `provider/internal/core/instance_params.go`
|
||||
- `provider/internal/core/refsvc_resolve.go`
|
||||
- `provider/internal/resources_core/resource_diagnostics.go`
|
||||
- `provider/internal/resources_core/crud.go`
|
||||
|
||||
### Группа 4. Генератор (как рождается «универсальная» часть)
|
||||
- `TOOLS/yaml-generator/main.go`
|
||||
- `TOOLS/resource-generator/internal/loader/loader.go`
|
||||
|
||||
### Группа 5. Факты API (спеки операций/параметров)
|
||||
- `generated/dev/resources_yaml/19_vc_org.yaml`
|
||||
- `generated/dev/resources_yaml/22_vc_nsxt.yaml`
|
||||
|
||||
### Группа 6. Применение модификаторов (композиция цепочки)
|
||||
- `DEV_STAND/FullPipe/modifiers.tf`
|
||||
|
||||
### Группа 7. Только если без них нельзя ответить (иначе не открывать)
|
||||
- `provider/internal/resources_core/state_refresh.go`
|
||||
- `provider/internal/resources_core/params_compare.go`
|
||||
- `provider/internal/resources_core/helpers.go`
|
||||
- `TOOLS/resource-generator/main.go`
|
||||
- `TOOLS/resource-generator/internal/templates/instance.go`
|
||||
|
||||
## Что нужно на выходе
|
||||
|
||||
Структурированный отчёт, разделы строго в этом порядке:
|
||||
|
||||
1. **Устройство архитектуры** — слои и поток данных, 5–10 строк.
|
||||
2. **Спека ↔ код** — список расхождений `ARCHITECTURE.md` с реализацией (`файл:строка`).
|
||||
3. **Дефекты и риски модификаторов** — по убыванию критичности. По каждому:
|
||||
суть → место (`файл:строка`) → последствие → предлагаемое направление (одна строка).
|
||||
4. **Открытые вопросы** — списком, если есть.
|
||||
|
||||
## Формат ответа
|
||||
|
||||
- Максимально сжато: тезисы, без вступлений, воды и «лирики».
|
||||
- Каждое утверждение проверяемо: ссылка `файл:строка`.
|
||||
- Код — только короткие фрагменты, и лишь если без них тезис не понятен.
|
||||
- Никаких «а ещё могу», никаких предложений расширить работу.
|
||||
|
||||
## Правило «стоп»
|
||||
|
||||
Если задание неоднозначно или данных не хватает — **остановиться и задать один короткий
|
||||
вопрос**. Не достраивать смысл и не действовать по догадке.
|
||||
@@ -0,0 +1,91 @@
|
||||
# Промпт для Opus 4.8 — РАУНД 2: замечания к отчёту (2026-09-30)
|
||||
|
||||
> Продолжение диалога. Раунд 1 — `prompt_for_opus_provider_architecture_modifiers.md`.
|
||||
> Формат ответа тот же: сжато, тезисы, `файл:строка`, без догадок. Границы доступа — как в раунде 1
|
||||
> (плюс список из §4 ниже). `HISTORY/**`, `NOTES/**`, `docs/**`, `HAR/**`, `TMP/**`, git-история — по-прежнему ЗАПРЕЩЕНЫ.
|
||||
|
||||
---
|
||||
|
||||
## 1. Зачтено (переделывать НЕ надо)
|
||||
|
||||
`S1`, `S5`, `R1`, `R4` — приняты. Не повторяй их в ответе.
|
||||
|
||||
## 2. Замечания — обязательны к отработке
|
||||
|
||||
**M1. Номера строк не сходятся.**
|
||||
Ты дал `provider.go:199-204` для регистрации модификаторов. По моему чтению файла (начиная со
|
||||
строки 180) функция `Resources()` находится примерно на строке 182, и регистрация идёт сразу за ней —
|
||||
твои 199–204 не сходятся. Требование задания — «каждое утверждение проверяемо».
|
||||
Действие: перепроверь **каждую** ссылку `файл:строка` в отчёте и дай точные номера; где не сверял —
|
||||
пометь «не сверено». Без этого отчёт не принимается.
|
||||
|
||||
**M2. `R2` — нарушено правило «без догадок».**
|
||||
Ты утверждаешь, что `check_hardcoded_service_ids.sh` «это не ловит», но этот скрипт **не читал**
|
||||
(его не было в разрешённом списке). Это догадка, а не факт.
|
||||
Действие: скрипт теперь разрешён (см. §4). Либо приведи факт из его кода, либо переформулируй в вопрос.
|
||||
|
||||
**M3. `S2`/`S4` — проверь основание, иначе они натянуты.**
|
||||
Правила спеки («No manual edits to **generated** Go code», «Service-specific Go code is fully
|
||||
**generated** from YAML») писаны про генерируемый код. Ресурсы в `resources_core` — ручные, не
|
||||
генерируемые. Плюс `S4` («modify всегда через WithDefaults») относится к `Update` инстанс-ресурсов,
|
||||
а не к отдельному ресурсу-модификатору.
|
||||
Действие: для каждого из S2/S4 дай **текстуальную опору из спеки** (`TOOLS/ARCHITECTURE.md:строка`)
|
||||
и переформулируй: это **пробел спеки** (нет категории для ручных оверлеев) или **нарушение**? Если
|
||||
опоры нет — пункт снять.
|
||||
|
||||
**M4. `R5` — обоснуй приоритет или понизь.**
|
||||
Ты сам пишешь «корректности не нарушает», но ставишь R5 выше R6.
|
||||
Действие: назови шкалу ранжирования (например: вероятность × последствие × обнаружимость) и
|
||||
пересчитай порядок; либо понизь R5.
|
||||
|
||||
**M5. Главный пробел: устойчивость `Read` и вечный diff.**
|
||||
Для Terraform это опаснее всего, а разобрано тоньше всего (только R6/импорт).
|
||||
Действие: разбери построчно, как `Read` модификатора формирует `vip_configure` / `ip_space_name`
|
||||
из live-состояния и совпадёт ли это с тем, что вернёт `jsonencode` из конфига:
|
||||
- порядок ключей JSON;
|
||||
- тип `count` (строка vs число);
|
||||
- снятие `null` и пустых значений;
|
||||
- что видит `plan` после `Read` для `keep_on_destroy`.
|
||||
Отдельно: может ли `Update` инстанс-ресурса (`nubes_vc_org` / `nubes_vc_nsxt`) вклиниться между
|
||||
apply-ами модификатора и вернуть поле назад (пересечение — это R1, но нужен сценарий во времени).
|
||||
Если считаешь, что глубокий разбор здесь не нужен — обоснуй одной строкой, почему.
|
||||
|
||||
## 3. Что упущено — доработать
|
||||
|
||||
**U1. Файла `modifiers.yaml` в репозитории НЕТ.**
|
||||
Есть только упоминания-комментарии (в `TOOLS/yaml-generator/main.go` и док-заметках). Это не
|
||||
«параллельный слой», а **задокументированное, но не созданное** наложение.
|
||||
Действие: переформулируй свой открытый вопрос №1 с учётом этого факта.
|
||||
|
||||
**U2. Рассинхронизация словаря жизненного цикла.**
|
||||
У генерируемых ресурсов — `suspend_on_destroy`, у модификаторов — `keep_on_destroy`. Два имени
|
||||
для одного смысла, живут в разных ветках кода.
|
||||
Действие: оцени как архитектурный дефект (единый словарь/контракт жизненного цикла), с `файл:строка`.
|
||||
|
||||
**U3. Корневая причина «ручных» модификаторов.**
|
||||
Генератор умеет `kind: modifier`, но доменное знание (какая операция, имя, inverse) некуда положить —
|
||||
оверлей не создан.
|
||||
Действие: сформулируй вывод по вариантам (оверлей-данные vs ручной код vs иное) — одной строкой на вариант.
|
||||
|
||||
## 4. Дополнительно РАЗРЕШЕНО читать (только это, сверх раунда 1)
|
||||
|
||||
- `TOOLS/scripts/check_hardcoded_service_ids.sh`
|
||||
- `TOOLS/resource-generator/main.go`
|
||||
- `TOOLS/resource-generator/internal/templates/instance.go`
|
||||
- `TOOLS/resource-generator/internal/templates/modifier.go`
|
||||
- `provider/internal/resources_core/state_refresh.go`
|
||||
- `provider/internal/resources_core/helpers.go`
|
||||
- `provider/internal/resources_core/params_compare.go`
|
||||
|
||||
Всё остальное — по-прежнему нельзя. Нужен файл вне списка → вопрос мне, не чтение.
|
||||
|
||||
## 5. Формат ответа
|
||||
|
||||
- **Только дельта**: что исправляешь, что снимаешь, чем обосновываешь; новые находки по M5/U2.
|
||||
- Полный отчёт заново не переписывать.
|
||||
- Как в раунде 1: тезисы, `файл:строка`, без «лирики» и без «а ещё могу».
|
||||
|
||||
## 6. Разрешение копать глубже
|
||||
|
||||
Если для честного ответа нужно копнуть глубже (в т.ч. по файлам вне §4) — **скажи прямо**, каких
|
||||
файлов не хватает, и задай вопрос. Если глубина не нужна — так и напиши по конкретному пункту.
|
||||
@@ -0,0 +1,59 @@
|
||||
# Промпт для Opus 4.8 — РАУНД 3: сам провайдер (устойчивость и корректность)
|
||||
|
||||
> Продолжение диалога. Раунды 1–2 — `HISTORY/OPUS/2026-09-30_provider_architecture_modifiers_dialog.md`.
|
||||
> Режим и формат ответа — как раньше: сжато, тезисы, `файл:строка`, без догадок.
|
||||
|
||||
---
|
||||
|
||||
## Вводная (смена приоритета)
|
||||
|
||||
Модификаторы (`nubes_vc_org_ip_allocation`, `nubes_vc_nsxt_snat`) — новая, возможно сложная, но
|
||||
**НЕБОЛЬШАЯ** часть сервиса. Чрезмерный упор на них не нужен.
|
||||
|
||||
**Главное — САМ провайдер: его устойчивость и корректность.**
|
||||
|
||||
- `M6` (доступ к двум файлам для доразбора R1) — **снять**; углубление по модификаторам больше не требуется.
|
||||
- Прошлые находки (`S1`, `S2`, `R2`–`R6`, `U1`–`U3`) **не повторять**.
|
||||
|
||||
## Бюджет (жёстко — экономим токены)
|
||||
|
||||
- Прочитать **не более 10 файлов** суммарно. Ранее прочитанные повторно не открывать.
|
||||
- Ответ — **не более 5 находок**, каждая **не более 3 строк**.
|
||||
- Никаких вступлений, повторения прошлых отчётов, «а ещё могу».
|
||||
|
||||
## Что анализировать (провайдер целиком)
|
||||
|
||||
1. **Жизненный цикл инстанс-ресурса:** `create` / adopt / `suspend` / resume / `modify` / redeploy /
|
||||
`delete` и повторный `apply` — где теряется корректность состояния.
|
||||
2. **Досылка и нормализация параметров** (`WithDefaults`, zero-value fallback, дефолты `map-fixed`,
|
||||
регистр UUID): где риск затереть значение или получить ложный diff.
|
||||
3. **`Read` / refresh:** устойчив ли state у генерируемых ресурсов; где возможен вечный diff.
|
||||
4. **Устойчивость ядра:** ретраи и ошибки, `LockInstance`, идемпотентность, поведение при дубликатах
|
||||
и параллельных `apply`.
|
||||
5. **Генератор → код:** какие классы дефектов порождает шаблон.
|
||||
|
||||
## Границы доступа
|
||||
|
||||
Разрешено читать (только это):
|
||||
|
||||
- `provider/internal/core/**`
|
||||
- `provider/internal/resources_core/**`
|
||||
- `provider/internal/provider/provider.go`
|
||||
- `TOOLS/resource-generator/internal/templates/**`
|
||||
- `TOOLS/resource-generator/internal/params/params.go`
|
||||
- `TOOLS/resource-generator/internal/helpers/helpers.go`
|
||||
- `TOOLS/resource-generator/internal/loader/loader.go`
|
||||
- `TOOLS/resource-generator/internal/writers/writers.go`
|
||||
|
||||
Запрещено: `HISTORY/**`, `NOTES/**`, `docs/**`, `HAR/**`, `TMP/**`, `DEV_STAND/**`, `TEST_STAND/**`,
|
||||
`PROD_STAND/**`, git-история. Нужен файл вне списка → вопрос мне, не чтение.
|
||||
|
||||
## Формат ответа
|
||||
|
||||
- **ТОП-5 находок** по убыванию критичности: суть → место (`файл:строка`) → последствие (≤ 3 строк).
|
||||
- Затем **одна строка**: что из этого проверяемо только замером (прогон `apply`/`plan`), а не чтением.
|
||||
- Находок меньше пяти — столько, сколько есть. Водой не добивать.
|
||||
|
||||
## Стоп-правило
|
||||
|
||||
Не хватает файла или данных — один короткий вопрос. Не догадываться.
|
||||
@@ -0,0 +1,43 @@
|
||||
# Opus — раунд 4: короткие решения (2026-09-30)
|
||||
|
||||
**Репо:** `/home/naeel/TF/tf_provider` — `terraform-provider-nubes` (Go, Terraform Plugin Framework).
|
||||
|
||||
## Формат ответа (ЖЁСТКО)
|
||||
|
||||
- На каждый вопрос — **1–3 строки**, с `файл:строка`.
|
||||
- **Весь ответ ≤ 25 строк.** Без вступлений, пересказа, «а ещё могу», без повторения вопросов.
|
||||
- Не хватает данных — одна короткая строка-вопрос, не догадка.
|
||||
|
||||
## Уже сделано — не обсуждать
|
||||
|
||||
401 в `isRetryable`; страж хардкодов покрыл `provider/`; idempotency pre-check переведён на live
|
||||
(`core/modifier_compare.go`); `ImportState` модификаторов заполняет Required.
|
||||
|
||||
## Вопросы
|
||||
|
||||
**Q1. Retry POST.** `core/http.go:87-108` ретраит только GET. Слепой retry создающего
|
||||
`POST /instanceOperations` рискует дубликатом операции. Как правильно: (а) не ретраить +
|
||||
задокументировать; (б) ретраить только безопасный POST (`validate-cfs`); (в) иное?
|
||||
|
||||
**Q2. Осиротевшая операция.** `core/operation_run.go:150`, `core/operation_run_bycode.go:108`: при
|
||||
сбое `instanceLiveParams` уже созданная операция остаётся невыполненной. Продолжение с fallback
|
||||
вернёт reset-баг (защита поставлена сознательно). Есть ли безопасный третий путь (cancel/delete
|
||||
операции через API) или оставить как есть? Нужен API-метод — назови его.
|
||||
|
||||
**Q3. Zero-value по подстроке имени.** `core/params.go:47-59`: при пустом `dataType` тип угадывается
|
||||
по `name/code/label`. Пути применения: `instance_create.go:105-113` (required без значения), досылка
|
||||
modify. Менять (убрать угадывание) или оставить?
|
||||
|
||||
**Q4. Словарь жизненного цикла.** Три несогласованных контракта: `suspend_on_destroy`/`keep_on_destroy`
|
||||
(генерируемые ресурсы), `delete_strategy` (генерируемые модификаторы), `keep_on_destroy` (ручные).
|
||||
Какой единый контракт зафиксировать в `TOOLS/ARCHITECTURE.md`?
|
||||
|
||||
**Q5. Что упущено.** Назови **один** самый критичный для устойчивости/корректности **ядра** дефект,
|
||||
не упомянутый выше: 1 строка — суть → `файл:строка`.
|
||||
|
||||
## Границы доступа
|
||||
|
||||
Читать: `provider/internal/**`, `TOOLS/ARCHITECTURE.md`,
|
||||
`TOOLS/resource-generator/internal/{templates,loader,params,helpers}/**`.
|
||||
|
||||
Не читать: `HISTORY/**`, `NOTES/**`, `docs/**`, `HAR/**`, стенды, git-историю. Нужен файл вне списка → вопрос.
|
||||
@@ -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 строк, ключевой файл)
|
||||
3. **TOOLS/docs-generator/main.go** — CLI, флаги, оркестрация
|
||||
4. **TOOLS/scripts/05_generate_docs_llm.py** — LLM-обогащение, SYSTEM_PROMPT
|
||||
5. **docs/LLM_DOCS_GENERATION.md** — архитектурная документация
|
||||
5. **NOTES/50_process/LLM_DOCS_GENERATION.md** — архитектурная документация
|
||||
6. **docs/30_registry/assets/extra.css** — CSS-стили
|
||||
7. **docs/30_registry/javascripts/fix-slash.js** — JS (trailing slash fix)
|
||||
|
||||
@@ -43,6 +43,25 @@
|
||||
- `universal_rebuild/tools/gen/main.go`
|
||||
- Читает YAML и генерирует ресурсы + `registry.go`.
|
||||
|
||||
### 1.6 Отдельные modifier-ресурсы
|
||||
Для parent-level операций `modify`, которые должны выполняться отдельным шагом Terraform-цепочки, используется `kind: modifier`.
|
||||
|
||||
Пример:
|
||||
|
||||
```yaml
|
||||
- name: modify
|
||||
kind: modifier
|
||||
action: modify
|
||||
modifier: ip_space
|
||||
params: []
|
||||
```
|
||||
|
||||
Такой блок не попадает в обычный instance CRUD. Go-генератор создаёт отдельный ресурс с именем `nubes_<service>_<modifier>`. Ресурс принимает ID родительского инстанса и параметры операции, выполняет parent `modify` при Create/Update и читает актуальные значения из `state_params` при Read.
|
||||
|
||||
Для `vcOrg` используется modifier `ip_space` с параметром `vIPConfigure`; для `vcNsxt` используется modifier `network` с параметрами операции сетевой настройки. Nested API-параметры modifier-ресурсов передаются как JSON-строки, поэтому их Terraform-значения должны быть валидным JSON.
|
||||
|
||||
Удаление modifier пока является no-op: подтверждённого обратного payload для отмены выделенных IP или SNAT нет. Операции удаления родительского сервиса не являются rollback и намеренно не вызываются.
|
||||
|
||||
---
|
||||
|
||||
## 2) Как получить параметры сервиса (без instanceUid)
|
||||
@@ -126,6 +145,27 @@ go build -o terraform-provider-nubes
|
||||
- Генератор может создавать «разбитые» snake_case для CamelCase (например `resource_c_p_u`).
|
||||
- Это ожидаемо, но если критично — нужен отдельный маппинг (по согласованию).
|
||||
|
||||
### 6.5 Регистр UUID в JSON-параметрах (map-fixed) — ОБЯЗАТЕЛЬНО ЗНАТЬ
|
||||
|
||||
- Платформа хранит UUID в lowercase, но **сравнивает регистр при create**. Ресурс
|
||||
`nubes_vc_nsxt` отдаёт `id` в UPPERCASE → `startupConfiguration.vdcUid/nsxtUid`
|
||||
в верхнем регистре → ошибка «Edge не развёрнут в указанном vDC».
|
||||
- **Нормализация нужна в ДВУХ разных местах, и они не взаимозаменяемы:**
|
||||
1. **Сравнение** (план vs state, adopt/suspend/resume, modifier-compare, диагностика) —
|
||||
`jsonutil.LowercaseUUIDsInText` внутри `JSONStringsEquivalent`, `JsonNormalize()`,
|
||||
`ParamsMatchForResume`, `normalizeCompareValue`.
|
||||
2. **Отправка в API** — единственная точка: `resources_core.BuildJSON`
|
||||
(`provider/internal/resources_core/helpers.go`), её вызывает генератор
|
||||
(`NestedJSONExpr` в `templates/instance.go`: Create / Modify / Redeploy).
|
||||
С 30.09 результат оборачивается в `LowercaseUUIDsInText(...)`.
|
||||
- ⛔ **Не «лечить» это в HCL** (`lower(...)` в конфиге стенда) — это костыль, который
|
||||
существовал только потому, что путь отправки не нормализовал UUID. Он ломался при
|
||||
работе из-под Windows на провайдере `2.0.23`.
|
||||
- Детали, аудит всех мест и границы применимости: `docs/60_strategy/terraform_case_sensitivity_fix.md`
|
||||
(§10 — регистр при сравнении, §11 — регистр при отправке).
|
||||
- Не покрыто: скалярные UUID в `normalizeUniversalValueV6` (дефолты create / досылка modify)
|
||||
и валидация ref-параметров внутри JSON при adopt — см. §11 и HISTORY/2026-09-30.
|
||||
|
||||
---
|
||||
|
||||
## 7) Проверенная цепочка (dummy)
|
||||
@@ -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` не делался.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user