231 Commits
Author SHA1 Message Date
Repinoid 09e38928d0 chore: сохранить текущие изменения стендов и заметок 2026-09-29 11:16:57 +03:00
Repinoid a52170cf1e docs(history): vpn-transit-213 — итог оптимизации: автоматизация, провал mux и DNAT, разбор ошибок 2026-09-28 16:45:04 +03:00
Repinoid 25f339bc8a 1 2026-09-28 09:42:25 +03:00
Repinoid 5742457cc0 docs(rules): приведена стилистика copilot-instructions.md — разделы и списки вместо капслока, все правила сохранены 2026-09-28 09:41:27 +03:00
Repinoid 6b6252c42b docs(pipeline): fix vdc CPU example for VM 2026-09-28 09:40:12 +03:00
Repinoid 70b937ea2f docs(pipeline): встроена схема зависимостей сервисов 2026-09-28 09:20:25 +03:00
Repinoid bc1af681aa build(docs): скрипт 04 копирует картинки docs/diagrams (*.svg|png) в публикуемый docs_dir; README диаграмм — раздел о публикации 2026-09-28 09:20:25 +03:00
Repinoid 318b847ede docs(modifiers): квота IP — учёт внешнего адреса ВМ (count = 4), актуальная ссылка на страницу пайплайна 2026-09-28 09:19:14 +03:00
Repinoid 7d762387e7 docs(provider-behavior): vApp/ВМ в таблице ресурсов, в заморозке (suspend/adopt) и в §6-пайплайне (шаги 6-8, лимит vCPU vDC, 14 дней для vApp) 2026-09-28 09:19:00 +03:00
Repinoid 0d7b8c5310 docs(nav): пункт меню пайплайна — добавлены vApp и ВМ 2026-09-28 09:19:00 +03:00
Repinoid 2675495eef docs(pipeline): цепочка расширена vApp -> ВМ — требования (квота CPU vDC, 4-й IP, SSH-ключ), параметры ВМ и образы, внешний доступ, раздел 6 с чек-листом, destroy для vApp/ВМ 2026-09-28 09:18:36 +03:00
Repinoid 034096d16d chore: бэкап FPipeGmail перед добавлением ВМ (без tfvars/tfstate — они в .gitignore) + HISTORY по vpn-transit-213 2026-09-28 08:04:52 +03:00
Repinoid 15cd369ec3 feat(fpipeline): ВМ в пайплайн FPipeGmail — самодостаточный vm.tf (vApp 26 + ВМ 28), внешний IP через общий SNAT, suspend_on_destroy 2026-09-28 07:54:50 +03:00
Repinoid 2fd9ef8aba docs(resume): правки по фактам — ответы по vApp/ВМ (обязательные параметры, suspend, образы, сеть), реальное состояние аккаунтов, исправлено ложное наблюдение про список инстансов (ключ results) 2026-09-27 19:20:22 +03:00
Repinoid ef22e78d8a docs(resume): резюме для нового чата — добавление ВМ (vApp 26 + ВМ 28) в пайплайн; что уже сделано, состояние стендов/облака, точки входа, развилки 2026-09-27 19:17:08 +03:00
Repinoid a366f47eff docs: move diagram artifacts to docs/diagrams, add vApp-IP precondition link and creation guide 2026-09-26 19:30:41 +03:00
Repinoid 6791e8fb5f docs: add styled infrastructure dependency diagram (generator + png + svg) 2026-09-26 19:13:57 +03:00
Repinoid ff38352bfc docs: remove duplicated SNAT label from edge node in flow diagram 2026-09-26 19:06:01 +03:00
Repinoid dc837e8e1a docs: verify cloud service dependency chains against YAML, add VM/Shurval checklists, redraw diagram 2026-09-26 18:49:17 +03:00
Repinoid 223b0ccdca docs: fix compute pool link to originate from vDC 2026-09-26 18:33:01 +03:00
Repinoid ea725bd8b9 docs: add infrastructure services flow diagram (mmd, svg, png) 2026-09-26 08:22:03 +03:00
Repinoid 48009583a6 Add September 25 Terraform backups 2026-09-26 07:20:59 +03:00
Repinoid e72eb75d10 stand(FPipeGmail): свои имена Штурвала — shturval-dev1 / shturval-dev-01 (занятое имя кластера не подошло) 2026-09-25 13:31:33 +03:00
Repinoid 8d25de9e79 docs: страница «Как работает провайдер (отличия от Terraform)» в навигации + ссылка на страницу пайплайна vDC → Edge → IP → SNAT → Штурвал 2026-09-25 09:59:56 +03:00
Repinoid f8d64948c8 docs(curated): страница «vDC → Edge → IP → SNAT → Штурвал» (требования, заморозка, проверка результата) + nav + HISTORY 2026-09-25 09:47:33 +03:00
Repinoid e20c22e0cb stand(FPipeGmail): организация kontra (аккаунт tazetdinovn@gmail.com) вместо устаревшей kontora 2026-09-25 08:53:33 +03:00
Nail dbaadccc50 docs(resume): подробное резюме состояния Штурвал/freeze для нового чата + стенд DEV_STAND/FPipeGmail 2026-09-25 08:05:12 +03:00
Nail c808a3b345 docs: повышение читаемости provider-behavior.md (упрощены §1 списком, §2 модификаторы, §3 id/нормализация, §5 приоритет флагов, §6 ALB-константы) 2026-09-24 20:53:17 +03:00
Nail aaf87d966b docs: страница «Как работает провайдер: отличия от канонического Terraform» (freeze/destroy, пайплайн vDC→Edge→IP→SNAT→Штурвал, FAQ) + кейс UUID внутри JSON в case-sensitivity документе 2026-09-24 20:40:18 +03:00
Repinoid 10520670a5 1 2026-09-24 20:27:54 +03:00
Nail 208d97e2ce docs+release(dev): 2.0.23 — аудит регистра UUID (8 мест), фикс внутри JSON, залито в реестр 2026-09-24 20:22:39 +03:00
Nail 03fff05117 docs(gitignore/embed): operation_timeouts.json — исходник, а не артефакт; профильные значения подменяются при релизе 2026-09-24 20:22:11 +03:00
Nail 621280a530 fix(uuid-case): нормализация UUID внутри JSON — adopt suspended-инстанса больше не падает на регистре (jsonutil + JsonNormalize), тесты 2026-09-24 20:17:32 +03:00
Nail c9d73450b0 docs: проверен цикл destroy=заморозка на живом стенде (5 destroyed, кластер/vDC suspend, эдж/SNAT/квота не тронуты) 2026-09-24 19:44:26 +03:00
Nail 5fd64b68d0 gitignore: TMP/devbin и terraform-provider-nubes — локально собранные бинарники не в git 2026-09-24 19:24:49 +03:00
Nail 4bdf03a531 tmp: бэкапы файлов перед правками заморозки + terraformrc для dev_overrides 2026-09-24 19:23:39 +03:00
Nail eaff056d9c stand(FullPipe): провайдер переведён на 2.0.22 2026-09-24 19:23:39 +03:00
Nail 77de8cece6 docs: стенд FullPipe переведён на 2.0.22, plan без изменений, флаги заморозки зафиксированы в state 2026-09-24 19:22:37 +03:00
Nail cab606b90e docs: релиз dev-провайдера 2.0.22 зафиксирован (залит в реестр, VERSIONS.md обновлён) 2026-09-24 19:14:53 +03:00
Nail c29df2173f release(dev): 2.0.22 — keep_on_destroy (state_only) для всех instance-ресурсов + предупреждения в Delete 2026-09-24 19:14:39 +03:00
Nail ba3887fa69 docs: фиксирую реализацию freeze-on-destroy (генератор 22c6c83, конфиг стенда 40aef87) и порядок проверки через dev_overrides 2026-09-24 19:07:06 +03:00
Nail 40aef879e4 stand(FullPipe): режим «заморозки» на destroy — keep_on_destroy=true (эдж/SNAT/квота IP), adopt для эджа, явный suspend_on_destroy для кластера 2026-09-24 19:06:46 +03:00
Nail 22c6c83a0f generator: третий режим destroy keep_on_destroy (state_only) для всех instance-ресурсов + предупреждения «заморожен/оставлен как есть» 2026-09-24 19:06:40 +03:00
Nail 0de72e09f0 docs(history): запись за 2026-09-24 — adopt для кластера Штурвал, диагностика dev-00 и направление freeze-on-destroy 2026-09-24 19:00:12 +03:00
Nail 3df93ad07f docs(notes): диагностика Штурвал dev-00 (44/48 подов, мусор init-job) + разбор ошибки destroy по квоте IP и дизайн freeze-on-destroy через генератор 2026-09-24 18:59:53 +03:00
Nail 57abb7bfa4 stand(FullPipe): adopt_existing_on_create=true для кластера Штурвал — apply усыновляет существующий инстанс shturval-dev вместо ошибки «РЕСУРС С ТАКИМ ИМЕНЕМ УЖЕ СУЩЕСТВУЕТ (SUSPEND)» 2026-09-24 18:18:57 +03:00
Nail 72d5771ffc stand(FullPipe): worker_configuration в camelCase (groupName/sizingPolicy/sizingDisk/labelDeck) — платформа падала на split() on null 2026-09-24 17:15:56 +03:00
Nail d98f6036d1 stand(FullPipe): добавлен Kubernetes кластер Штурвал — сервис 150 (nubes_k8s_sthutrval_cluster, vdc_uid+nsxt_uid), operation_timeout 60m 2026-09-24 16:51:24 +03:00
Nail 1a90c7737d stand(FullPipe): удалён Штурвал из конфига (был добавлен неверный сервис 148 вместо 150) 2026-09-24 16:45:18 +03:00
Nail b8adeb6582 stand(FullPipe): всё про Штурвал собрано в shturval.tf (переменные + ресурс); в variables.tf и terraform.tfvars ничего про Штурвал не осталось 2026-09-24 16:35:57 +03:00
Nail 5a2a5e7487 stand(FullPipe): добавлен Штурвал (nubes_vc_mgmt_sthutrval_cluster, 148) в конец цепочки vDC -> Edge -> IP -> SNAT 2026-09-24 16:27:57 +03:00
Nail 1611f7afa8 docs(curated): страницы примеров приведены к реальным файлам (versions/provider/variables/outputs), организация по имени, снята пометка «не проверено» 2026-09-24 16:15:15 +03:00
Repinoid 418b5645e5 docs: DEV 2.0.21 залит — ресурс аллокации принимает имя организации; стенд переведён на 2.0.21 2026-09-24 15:35:18 +03:00
Repinoid 5bd197f031 feat(provider): ресурс аллокации принимает имя организации (резолв в UUID через ResolveRefSvcParamValue, как в nubes_vc_vdc); конфиги и доки без org_uid 2026-09-24 15:28:13 +03:00
Repinoid bd5de0cead docs(pipeline): страница переписана как инструкция для пользователя (шаги, значения из ЛК, команды) 2026-09-24 15:21:41 +03:00
Repinoid c3b82cf074 docs(pipeline): ссылка на репозиторий примеров tf_examples и порядок клонирования 2026-09-24 15:00:07 +03:00
Repinoid 9590005914 docs: страница пайплайна vDC → Edge → внешние IP → SNAT (Ресурсы-модификаторы (IP организации, SNAT)) 2026-09-24 14:56:00 +03:00
Repinoid caa55d9ff8 chore(stand/FullPipe): провайдер 2.0.20 (проверено plan из реестра) 2026-09-24 14:16:05 +03:00
Repinoid d76418303a docs: DEV 2.0.20 залит (nubes-dev) — исправлен plan-modifier в nubes_vc_org_ip_allocation 2026-09-24 14:11:33 +03:00
Repinoid 6196a0119a docs(rules): добавить правило — при неясной команде переспросить и подтвердить, не гадать 2026-09-24 14:05:06 +03:00
Repinoid e25ef02a1a docs: убрать устаревшее «канонизация в plan-modifier» (совет Opus был неверен); план живого прогона FullPipe 2026-09-24 14:04:08 +03:00
Repinoid 807dfde287 fix(provider): убран plan-modifier, менявший пользовательское значение (Terraform: planned value must match config); сравнение аллокаций — смысловое в Read 2026-09-24 13:57:56 +03:00
Repinoid 721c3fcfab feat(stand/FullPipe): орг organ (org_uid) + ресурсы-модификаторы — аллокация IP после эджа, затем SNAT 2026-09-24 13:48:53 +03:00
Repinoid e6675be906 docs: DEV 2.0.19 залит (nubes-dev) — правки по ревью ресурсов-модификаторов 2026-09-24 10:57:10 +03:00
Repinoid ed4493c0ee docs: правки по ревью (канонизация, destroy-семантика) + статус выполнения 2026-09-24 10:52:51 +03:00
Repinoid 1236c59e18 test(provider): тесты канонизации vIPConfigure (jsonencode-форма, пробелы, [{}], невалидный JSON) 2026-09-24 10:52:36 +03:00
Repinoid 4b497e61db fix(provider): nsxt_snat — не писать null в Required-атрибут, ошибки API в Delete → error, валидация пустого ip_space_name 2026-09-24 10:52:36 +03:00
Repinoid ba6c4f5122 fix(provider): канонизирующий plan-modifier для vip_configure (jsonencode сортирует ключи → вечный diff); не писать null в Required; Delete: ошибки API → error 2026-09-24 10:52:36 +03:00
Repinoid 3374bf4e08 docs(prompts): ответ Opus на ревью кода ресурсов-модификаторов (блокеры: порядок ключей, Required+null) + список правок 2026-09-24 10:51:10 +03:00
Repinoid 648db99628 docs(prompts): промпт на ревью Opus — полный код двух ресурсов-модификаторов, известный баг и вопросы 2026-09-24 10:49:15 +03:00
Repinoid 9412106e3f docs: DEV 2.0.18 залит (nubes-dev) — ресурсы-модификаторы vc_org_ip_allocation и vc_nsxt_snat 2026-09-24 10:34:02 +03:00
Repinoid 6e6d223c22 docs(plans): §13 — статус работ (сделано/ждёт команды) 2026-09-24 10:26:36 +03:00
Repinoid 62abcd64f5 docs(providers): страница ресурсов-модификаторов (nubes_vc_org_ip_allocation, nubes_vc_nsxt_snat) + nav 2026-09-24 10:26:21 +03:00
Repinoid 3973f912fc chore(docs): удалить docs/TODO/what_not_in_terraform.md + убрать ссылки на него из комментариев 2026-09-24 10:24:13 +03:00
Repinoid 73a7459a38 feat(provider): регистрация ресурсов-модификаторов vc_org_ip_allocation и vc_nsxt_snat 2026-09-24 10:22:17 +03:00
Repinoid 80d82a145a feat(provider): ресурс nubes_vc_nsxt_snat (modify ipSpaceName, inverse no-needed) 2026-09-24 10:22:17 +03:00
Repinoid 22cf2595ee feat(provider): ресурс nubes_vc_org_ip_allocation (modify vIPConfigure, uid орги) + тесты нормализации 2026-09-24 10:22:17 +03:00
Repinoid 574e300476 docs(plans): §12 — орга делается руками в ЛК, в tf только uid; правка генератора не блокер, нужны только 2 ресурса 2026-09-24 10:10:55 +03:00
Repinoid cb8389c17f docs(plans): §11 — ответы Opus раунд 3 (критерий отбора = явный список в конфиге генератора, релиз A только (б), Deprecated вместо падения) 2026-09-24 10:04:25 +03:00
Repinoid 664f04eb49 docs(plans): §10 — вопрос Опусу про безопасность универсальной правки графа генератора (5 сервисов с modify-only) 2026-09-24 10:00:57 +03:00
Repinoid 602b27ee1a docs(plans): ревью Opus по плану — §9 (ответы на 5 вопросов, count строкой, обязательный follow-up по генератору) 2026-09-24 09:55:02 +03:00
Repinoid 97d5ca818e docs(plans): план двух ресурсов-модификаторов (nubes_vc_org_ip_allocation, nubes_vc_nsxt_snat) + вопросы на ревью 2026-09-24 09:32:55 +03:00
Repinoid bccf8f7320 docs(notes): раунд 2 Q&A с Opus (владелец параметра, массив vs элемент, keep_on_destroy, deprecated-переход, тип атрибута) 2026-09-24 09:31:10 +03:00
Repinoid 129dab97a0 docs(notes): Q&A с Opus по дизайну ресурсов-модификаторов + замечания к ответам 2026-09-24 09:29:22 +03:00
Repinoid 75700a92da docs(notes): исправлен ложный факт «схема только из create» в CHAT_RESUME_IAC (loader.go:96 мержит create+modify) 2026-09-24 08:48:54 +03:00
Repinoid 51ff9b3751 docs(notes): разбор fresh-create HAR — state после create (vIPConfigure=[{}], ipSpaceName только в modify) 2026-09-24 08:48:54 +03:00
Repinoid f7fffb9ed7 refactor: разложить рабочие материалы по NOTES/ и HOW_TO/, корневой README — карта проекта
- NOTES/: 10_plans, 20_prompts, 30_analysis, 40_chat_summaries, 60_reference + README в каждой папке
- HOW_TO/: все общие инструкции (сборка/заливка, DevOps-ранбук, добавление сервиса, миграция, генерация доков) + индекс «что нужно -> какой файл»
- новый README.md: карта проекта, пайплайн, стенды, реестр, запреты/грабли
- HOWTO-UPLOAD.md: исправлена легаси-схема версий (prod=1.*, dev=2.*, test=3.*)
- DEVOPS_BUILD_PIPELINE.md: пути скриптов -> TOOLS/scripts, universal_rebuild/main.go -> provider/main.go
- howitwasdone.md / MIGRATION_PLAN_FOR_AGENT.md: пометки о соответствии старых путей
- внутри перенесённых файлов обновлены ссылки на новые пути
2026-09-24 07:51:38 +03:00
Repinoid 2d8e435dd4 docs: пометить отменённый заход модификаторов как LEGACY + исправить ложные факты
- баннеры «ЛОЖНЫЙ ПУТЬ — ОТМЕНЕНО» на 4 файла HISTORY/OPUS/2026-09-22_modifier_* и docs/60_strategy/modifier_resources_ideology_and_specification.md
- vIPConfigure: replace-семантика, НЕ накопительная (по тесту docs/ORG_IP_MODIFIER_TEST_2026-09-22.md)
- обновлены ссылки на перенесённые материалы (docs/... -> NOTES/..., HOW_TO/...)
2026-09-24 07:51:25 +03:00
Repinoid d93ff66482 chore: save current changes 2026-09-24 07:25:27 +03:00
Repinoid a96e38fb1e chore: save current changes 2026-09-23 19:23:31 +03:00
Repinoid d78573de45 refactor(fullpipe): убрать зависимость от модификаторов (org_ips.tf, edge_network.tf) — чистые ресурсы vdc+nsxt 2026-09-23 08:55:26 +03:00
Repinoid eaa4593622 chore(fullpipe): bump провайдера 2.0.16 -> 2.0.17 (чистая генерация без модификаторов) 2026-09-23 08:51:01 +03:00
Repinoid a5a5f83730 chore(dev): bump провайдера 2.0.16 -> 2.0.17 (чистая генерация без модификаторов) 2026-09-23 08:46:32 +03:00
Repinoid 20ea79a489 docs(modifier): анализ inverse-архитектуры + промпты Опусу (глобальная архитектура оверлея) 2026-09-23 08:43:56 +03:00
Repinoid d5dc1212ba refactor(yaml-gen): убрать вплетение модификаторов — YAML = чистая полная выгрузка из API 2026-09-23 08:42:01 +03:00
Repinoid 016b7d246a chore(fullpipe): bump провайдера 2.0.15 -> 2.0.16 2026-09-23 07:11:12 +03:00
Repinoid 200bf457ce fix(gen): Update без modify перечитывает read-back поля через RefreshResourceState (устраняет устаревший state_params/ложный дрейф) 2026-09-23 07:04:02 +03:00
Repinoid 60bf218532 chore(fullpipe): bump провайдера 2.0.14 -> 2.0.15 2026-09-22 22:40:16 +03:00
Repinoid b9fa18164c fix(core,gen): UseStateForUnknown для Optional+Computed + live-dосылка без тихого fallback (R1+R3+A/B) 2026-09-22 22:35:45 +03:00
Repinoid 0473f80f69 chore(fullpipe): bump провайдера 2.0.13 -> 2.0.14 2026-09-22 22:10:28 +03:00
Repinoid f62a094043 fix(fullpipe): ALB/VS/qos задаются в модификаторе vc_nsxt_network (resource vc_nsxt Update — no-op) 2026-09-22 22:07:14 +03:00
Repinoid a011358660 fix(core): досылать незаданные modify-params из live state.params (а не paramValue формы) — устраняет сброс needEnableAVI/ALB 2026-09-22 22:04:09 +03:00
Repinoid d33673a567 chore(fullpipe): bump провайдера 2.0.12 -> 2.0.13 2026-09-22 21:53:36 +03:00
Repinoid e0ad8bb472 chore(dev): bump провайдера 2.0.12 -> 2.0.13 2026-09-22 21:44:59 +03:00
Repinoid 94c4c44ba4 fix(core): ShouldRemoveFromState читает deleted/404 через GetInstanceStateRaw без падения 2026-09-22 21:44:16 +03:00
Repinoid 4220f4f483 chore(fullpipe): bump провайдера 2.0.11 -> 2.0.12 2026-09-22 21:32:36 +03:00
Repinoid c634a1c51b docs: move root prompts 2026-09-22 21:26:26 +03:00
Repinoid 73357d0e0f fix(gen): bt как функция в шаблоне modifier + обновить описание 2.0.12 2026-09-22 21:22:56 +03:00
Repinoid bd46d11155 test(core,gen): unit-тесты modifierDesiredEqualsCurrent и ValidateSpec modifier 2026-09-22 21:20:43 +03:00
Repinoid 6a722e49cf feat(yaml): реестр исключений модификаторов (delete_strategy/idempotency/delete_params) 2026-09-22 21:17:09 +03:00
Repinoid 6b0378cc0d refactor(gen): шаблон modifier — reconcile(override), Delete стратегия, idempotency-вызов 2026-09-22 21:17:09 +03:00
Repinoid 25e988886d feat(core): modifierDesiredEqualsCurrent + RunInstanceOperationUniversalByIdempotent + RunOperationByCodeIdempotent 2026-09-22 21:13:56 +03:00
Repinoid 17a803836a refactor(core): вынести JSON-эквивалентность в core/jsonutil (+реэкспорт в resources_core) 2026-09-22 21:10:13 +03:00
Repinoid 8e24f8107e feat(gen): GenModifier расширение (DeleteStrategy/Idempotency/DeleteParams) + LoadSpecs + ValidateSpec 2026-09-22 21:07:15 +03:00
Repinoid 6e297cc649 feat(lib): delete_strategy/idempotency/delete_params в OperationSpec (+DeleteParam) 2026-09-22 21:03:11 +03:00
Repinoid 2933c57a4b docs(plan): внести правки ревью Опуса в план (порядок, ID-миграция, override, array-map-fixed) 2026-09-22 20:57:54 +03:00
Repinoid e06a2c0b11 docs(plan): финализировать план + запрос на ревью Опуса 2026-09-22 20:52:15 +03:00
Repinoid fba4cbda37 docs(plan): детальный план редизайна модификаторов (10 шагов + коммиты) 2026-09-22 20:50:37 +03:00
Repinoid c9c69a3d11 docs(opus): ответы №3 (граница lib/GenModifier, pre-check в core, частичный inverse) 2026-09-22 20:44:39 +03:00
Repinoid e55ca14d5e docs(opus): вопросы по расхождениям архитектуры модификаторов с кодом 2026-09-22 20:26:54 +03:00
Repinoid c3683cbe65 docs(opus): дописать ответы №2 (inverse/schema/current/idempotency/skip) 2026-09-22 20:19:04 +03:00
Repinoid 3b3cfc85c0 docs(opus): задокументировать архитектуру модификаторов + открытые вопросы 2026-09-22 20:15:59 +03:00
Repinoid bea39508f5 docs(opus): исчерпывающий запрос — спроектировать архитектуру модификаторов с нуля (все кейсы) 2026-09-22 20:11:14 +03:00
Repinoid 3d722fb7ee refactor(core): разбить client.go (1678 строк) на 15 мелких модулей по зонам ответственности 2026-09-22 20:06:49 +03:00
Repinoid c2deff1a58 chore(dev): bump провайдера 2.0.11 -> 2.0.12; закрепить 2.0.11 в FullPipe 2026-09-22 19:49:41 +03:00
Repinoid 3236203366 fix(core): не досылать modify-params без live-значения/дефолта — избежать синтетического 0 (integer > 0) 2026-09-22 19:49:04 +03:00
Repinoid 3a2a93b32e chore(dev): bump версии провайдера 2.0.10 -> 2.0.11 2026-09-22 19:26:17 +03:00
Repinoid 261809ba99 fix(generator): is_modifiable=true → параметр НЕ create-only (канон: меняется в UI → меняется в Terraform) 2026-09-22 19:21:42 +03:00
Repinoid d6520138f6 docs(opus): prompt — спроектировать простую модель изменяемости параметров (CreateOnly vs modifier) 2026-09-22 19:14:59 +03:00
Repinoid 3d0fc2be93 fix(fullpipe): убрать дубликат переменной nsxt_qos_profile 2026-09-22 19:10:36 +03:00
Repinoid 6cd9a3184b fix(fullpipe): убрать костыль — ALB/VS на create (true/3), модификатор только SNAT (ядро 2.0.10 досылает live) 2026-09-22 19:07:28 +03:00
Repinoid 8b0228cfd1 fix(fullpipe): Edge create ALB=false (неизменяемо), ALB/VS включаются только модификатором 2026-09-22 19:05:55 +03:00
Repinoid 307ef13a4e chore(fullpipe): закрепить провайдер nubes-dev 2.0.10 2026-09-22 19:03:02 +03:00
Repinoid 7e2ad415b4 chore(dev): bump версии провайдера 2.0.9 -> 2.0.10 2026-09-22 18:56:45 +03:00
Repinoid 57bf79d85b docs(todo): план разбиения client.go (1678 строк) на модули 2026-09-22 18:56:11 +03:00
Repinoid aca15e8f69 docs(opus): задокументировать разбор бага сброса create-полей modify 2026-09-22 18:55:27 +03:00
Repinoid c420ea0e70 fix(core): дозаполнять ВСЕ незаданные params modify их live-значением (по коду), иначе модификатор сбрасывает create-поля в дефолт 2026-09-22 18:54:51 +03:00
Repinoid 4f7edc1200 docs(opus): prompt по багу — modify-модификатор сбрасывает create-поля в дефолт 2026-09-22 18:51:00 +03:00
Repinoid 815ace7d2c fix(fullpipe): модификатор edge_net шлёт ALB/VS/qos целиком — иначе modify с null сбрасывает needEnableAVI в false 2026-09-22 18:48:46 +03:00
Repinoid 5129d2726d fix(fullpipe): ALB=true и VS=3 при создании Edge, модификатор только SNAT, раскомментировать всё 2026-09-22 18:37:21 +03:00
Repinoid 34c8e72e46 test(fullpipe): закомментировать Edge/edge_net/org_ips — оставить только VDC для destroy-проверки 2026-09-22 18:30:28 +03:00
Repinoid 7a6e3bd1e5 fix(fullpipe): ALB/VS меняются только через модификатор (edge create неизменяем) 2026-09-22 18:23:07 +03:00
Repinoid ca928b1e4b fix(fullpipe): параметры под Штурвал — ALB=true, VS=3, внешних IP=3 2026-09-22 18:20:54 +03:00
Repinoid 779f74b68c fix(fullpipe): org_ip_count default 1 (нужен свободный IP для SNAT) 2026-09-22 18:11:00 +03:00
Repinoid e0604fd9f9 feat(fullpipe): добавить модификатор vc_nsxt network (SNAT с внешним IP из vc_org) 2026-09-22 18:10:12 +03:00
Repinoid 099494eb63 test(fullpipe): org_ip_count default 0 после проверки modify 1→0 2026-09-22 17:46:23 +03:00
Repinoid 5ec3936235 chore(fullpipe): закрепить провайдер nubes-dev 2.0.9 2026-09-22 17:46:23 +03:00
Repinoid 788073f1ea docs(fullpipe): задокументировать проверку модификатора vc_org ip_space (modify 1↔2↔0, идемпотентность) 2026-09-22 17:46:23 +03:00
Repinoid 9107ba00fa chore(dev): bump версии провайдера 2.0.8 -> 2.0.9 2026-09-22 16:49:55 +03:00
Repinoid b1cea8a930 fix(core): fallback на /instanceOperations/default/{opId} при 500 getResourceRealmConfig в modify 2026-09-22 16:49:36 +03:00
Repinoid 947887a6ea docs(opus): задокументировать код-ревью модификаторов 2026-09-22 16:42:58 +03:00
Repinoid 423de314dd docs(opus): prompt код-ревью модификаторов 2026-09-22 16:40:20 +03:00
Repinoid 4bfbce4f44 feat(fullpipe): добавить модификатор vc_org ip_space (выделение внешних IP) 2026-09-22 16:40:20 +03:00
Repinoid eb85a3dc72 docs(har): пометить ошибки сломанного Edge как неподтверждённые наблюдения, не правила 2026-09-22 15:01:23 +03:00
Repinoid bcbc49dc52 docs(har): Insufficient rule blocks = нельзя снять ALB при живых VS (needEnableAVI forward-only) 2026-09-22 14:59:57 +03:00
Repinoid 19ad60b485 docs(har): ошибки операций EDGE — ipSpace '' невалиден, Insufficient rule blocks, delete FORBIDDEN при VS 2026-09-22 14:52:57 +03:00
Repinoid c7c80fd1bd fix(yaml-generator): сортировать subParams по ID — детерминированный порядок вложенных параметров 2026-09-22 14:36:04 +03:00
Repinoid 21b4da12a1 docs(har): имя ipSpace — выбор из списка (динамический); де-аллокация пока заблокирована 2026-09-22 14:27:58 +03:00
Repinoid ca4a246c8c docs(har): org2.har — де-аллокация = меньший count в vIPConfigure; операция pending (блок детей) 2026-09-22 14:04:31 +03:00
Repinoid d218bffad7 docs(har): ограничение де-аллокации IP в vcOrg (нельзя при дочерних инстансах) + порядок destroy 2026-09-22 14:01:00 +03:00
Repinoid 08108ba62b docs(har): no-needed — каноническое значение выключенного SNAT (подтверждено UI) 2026-09-22 14:00:08 +03:00
Repinoid a1b6ac8f23 docs(har): разбор SNAT/ipSpace модификаций — payload-и, no-needed, reverse SNAT 2026-09-22 13:54:00 +03:00
Repinoid 78f9dfbcfb refactor(build): эфемерные generated-копии — dev-materialize по стенду вместо постоянного дубля 2026-09-22 13:48:36 +03:00
Repinoid c14f7de7bc docs: раздел «Реестр исключений» + диалог код-ревью opus/astra 2026-09-22 08:31:22 +03:00
Repinoid dc321b5e3a refactor(generator): реестры исключений (данные) вместо хардкодов svc.ID==N / ServiceID==N 2026-09-22 08:31:22 +03:00
Repinoid 05f56c5e00 fix(build): гейт дрейфа сгенерированного кода + запрет прямой сборки из provider/ 2026-09-22 08:31:22 +03:00
Repinoid 7eab45ed71 docs(opus): промпт на код-ревью roadmap (vcOrg modify IP, vcNsxt SNAT, k8sShturval) 2026-09-22 07:21:16 +03:00
Repinoid a424e4e319 docs: резюме для старта новой сессии (состояние 2.0.8, правила, файлы, открытые вопросы) 2026-09-22 07:09:23 +03:00
Repinoid 13beb9c142 release(dev): 2.0.8 2026-09-21 21:56:42 +03:00
Repinoid 4b34cc7e63 fix(core): гарантия known для read-back полей (unknown -> null), чтобы Computed без Default не ломал apply 2026-09-21 21:48:54 +03:00
Repinoid 3c0157a1af fix(generator): read-back параметры без Default -> Optional+Computed (универсальное правило ShouldBeOptionalComputed) 2026-09-21 21:48:54 +03:00
Repinoid 25080b7379 release(dev): 2.0.7 2026-09-21 21:18:03 +03:00
Repinoid 724f5f7efb fix(generator): убрать create-time проверку существования из ModifyPlan (ломал tainted-replace и terraform destroy) 2026-09-21 20:54:57 +03:00
Repinoid 14ada09335 docs(flash): ТЗ на фикс tainted-replace - убрать create-time проверку из ModifyPlan 2026-09-21 20:52:51 +03:00
Repinoid e8da976a7b docs(instructions): copilot-instructions.md1 -> copilot-instructions.md 2026-09-21 20:41:58 +03:00
Repinoid 67c4d2f190 tool(scripts): validate_docs_examples.sh — terraform validate по примерам из сгенерированных доков 2026-09-21 20:41:13 +03:00
Repinoid 69808bdf09 release(dev): 2.0.6 2026-09-21 20:30:59 +03:00
Repinoid c6715e81be fix(generator): передавать supportsSuspend в diagnostics; не генерировать suspend_on_destroy для сервисов без suspend 2026-09-21 20:26:41 +03:00
Repinoid 8d405ba695 fix(core): подсказки при конфликте имени учитывают отсутствие suspend/resume (no adopt) + supportsSuspend в сигнатурах 2026-09-21 20:26:41 +03:00
Repinoid 3ca0752df8 fix(docs-generator): строковые дефолты в кавычках (default 81.22.46.22 ломал HCL-парсер: Invalid number literal) 2026-09-21 20:26:41 +03:00
Repinoid 7c2cc673cc fix(docs-generator): array-map-fixed = jsonencode([...]) (StringAttribute, не объект); классификация по data_type, а не is_json 2026-09-21 20:22:27 +03:00
Repinoid af2e10b1d5 fix(docs-generator): вложенные map-fixed как аргумент '= {' (а не блок), array-map-fixed через jsonencode 2026-09-21 20:19:42 +03:00
Repinoid 54b0baa718 release(dev): 2.0.5 2026-09-21 20:08:55 +03:00
Repinoid 61c7e207ba docs(HISTORY): сессия 2026-09-21 - фиксы генератора, refSvc, FullPipe (vDC+Edge) 2026-09-21 20:08:40 +03:00
Repinoid bffe3d9950 fix(generator): refSvc-поля без Computed (unset = null, а не unknown) 2026-09-21 20:05:02 +03:00
Repinoid 92e04daea5 docs(TODO): баг docs-generator - вложенный map-fixed как блок вместо = {} 2026-09-21 19:57:48 +03:00
Repinoid d608fba338 stand(FullPipe): vc_nsxt (edge.tf), storage_config fast->SATA, provider 2.0.4 2026-09-21 19:57:48 +03:00
Repinoid 140100378f release(dev): 2.0.4 2026-09-21 19:57:48 +03:00
Repinoid 2286d34499 fix(generator): destroy-guard в ModifyPlan + универсальный refSvc (имя или UUID) 2026-09-21 19:57:48 +03:00
Repinoid 7ecd2aaf44 Fix generator rebuild and release pipeline 2026-09-21 18:08:26 +03:00
Repinoid 1ec6a0fedc Fix VDC flow and FullPipe example 2026-09-21 13:02:25 +03:00
Repinoid 7d446977a5 Fix FullPipe VDC example placeholders 2026-09-21 12:07:06 +03:00
Repinoid 308f92bc37 fix(core): graceful fallback for cfsParams 500 error in createInstanceWithContext
- In provider/internal/core/client.go:
  - If GET /instanceOperations/{opUid}?fields=cfsParams fails with 500 or JSON unmarshal error,
    check whether provided parameters contain unresolved names via hasUnresolvedParams.
  - If all parameters are already resolved (UUIDs/numbers/booleans/JSON), proceed to send
    parameters via POST /instanceOperationCfsParams without hard-failing.
  - If unresolved names remain, fail immediately with original error.
- Bumped DEV provider version to 2.0.2 in TOOLS/config/dev/profile.env, VERSIONS.md, and DEV_STAND/FullPipe/versions.tf.
- Built, signed, and published provider 2.0.2 to DEV registry bucket nubes-terraform-registry.
2026-09-21 09:59:15 +03:00
Repinoid 7ff98f8edc feat(stand): add FullPipe DEV stand for vc_vdc and document 500 error fallback plan
- Added DEV_STAND/FullPipe with modular configuration for vc_vdc resource creation:
  - versions.tf: Terraform >= 1.5.0, provider nubes 2.0.1
  - provider.tf: nubes provider config with DEV gateway endpoint
  - variables.tf: variables for vdc (cpu=8, mem=32, guaranteed=0, fast storage=200GB)
  - vdc.tf: nubes_vc_vdc resource definition supporting org name or UUID
  - outputs.tf: vdc_id, vdc_name, vdc_state_params
  - terraform.tfvars.example: example values without secrets
- Added docs/DEBUG_REPORT_VC_VDC_500.md documenting API 500 issue in Lucee backend:
  - Error: getResourceRealmConfig fails casting Struct to string on GET /instanceOperations/{opUid}?fields=cfsParams
  - Planned changes in provider/internal/core/client.go:
    Implement graceful fallback in createInstanceWithContext to skip hard-failing
    when GET ?fields=cfsParams returns 500, since vc_vdc ref parameters (organization_uid)
    are already resolved to UUID and remaining parameters are literals/numbers.
2026-09-21 09:46:43 +03:00
Repinoid 4b218fbe33 fix: publish providers to registry bucket 2026-09-20 21:42:07 +03:00
Repinoid f32159b3af fix: publish generated providers to working registry 2026-09-20 20:00:34 +03:00
Repinoid a44894d877 1 2026-09-20 18:15:17 +03:00
Repinoid 8f552ecacc fix: harden modifier resource lifecycle
Validate required modifier parameters, refresh modifier state from parent state_params, preserve operation timeout and log level during update, and normalize nested modifier payloads as JSON. Document the modifier contract, vcOrg/vcNsxt usage, and the intentionally unsupported rollback semantics.
2026-09-20 18:11:17 +03:00
Repinoid a48b658d78 feat: add generated parent modify resources
Introduce the modifier YAML kind for delayed parent-level modify operations and generate dedicated Terraform resources with typed parameters. Keep modifier parameters out of the ordinary instance CRUD resource, register modifiers separately, and leave delete as a no-op until an inverse API payload is confirmed. Mark vcOrg and vcNsxt modify operations during YAML generation so the contract survives regeneration.
2026-09-20 18:05:24 +03:00
Repinoid 33672e05bf docs: add modifier resources ideology and architecture specification 2026-09-19 08:32:28 +03:00
Repinoid 0464a30642 docs(strategy): оркестрация взаимозависимых ресурсов и паттерн привязок
- complex_provisioning_workflow.md — цепочка vcOrg -> vcVdc -> vcNsxt -> Штурвал,
  включая modify-шаги и пошаговые модификации
- shturval_dev_provisioning_spec.md — точные параметры и ID операций DEV-стенда
  для цепочки развёртывания k8s_sthutrval_cluster
- terraform_association_pattern_and_pipeline.md — Resource Association Pattern
  (разделение сущности и ресурсов-привязок/модификаций)
2026-09-18 18:44:36 +03:00
Repinoid 106ddbe092 stand: switch every stand to actual provider versions (prod=1.0.0, dev=2.0.0, test=3.0.0)
Легаси-версии (5.0.x/5.1.x/3.1.x/2.1.x) в реестре отсутствуют (404), стенды не могли
пройти terraform init. Приведены к новой схеме нумерации от 2026-09-03.

- DEV  (nubes-dev):  CRUD, POSTGRES, SHTURVAL_MGMT -> 2.0.0
- TEST (nubes-test): CRUD, PG, POSTGRES, MARIA_DB, buck0, kuber, IOT_RMQ_DEMO -> 3.0.0
                     (DEV_STAND/IOT_KAFKA_DEMO тоже nubes-test -> 3.0.0)
- PROD (nubes):      PG1, POSTGRES, RABBIT -> 1.0.0
- README стендов TEST_STAND/buck0, TEST_STAND/PG: версии приведены к 3.0.0
- getting-started: убрана versioned-ссылка на доки (2.1.7) -> актуальный домен без версии
- .gitignore: откатано правило TMP/ (на remote TMP/init-test-*/main.tf трекаются)
2026-09-16 16:34:40 +03:00
Repinoid dbaeea5c89 stand(PGwNewRegistry): pin provider to 3.0.0 (new TEST scheme) 2026-09-16 16:33:27 +03:00
Repinoid 7eb8eb8859 chore(git): ignore TMP/ (local terraform init scratch) 2026-09-16 16:33:27 +03:00
“Naeel” 7a25ad8fc9 docs: show current environment on index 2026-09-03 17:58:13 +03:00
“Naeel” a222a0dace fix(docs): publish cross-stand index links 2026-09-03 17:39:24 +03:00
“Naeel” bba6b47dc2 docs: link documentation environments 2026-09-03 17:26:20 +03:00
“Naeel” ccb458a167 docs: record test and prod publication 2026-09-03 16:56:51 +03:00
“Naeel” aa0f7f6402 fix(docs): remove stand-specific hardcodes 2026-09-03 16:14:28 +03:00
“Naeel” 6bf514e03a docs: describe stand-agnostic docs pipeline 2026-09-03 12:05:21 +03:00
“Naeel” 8d5bbd368d docs: link verified documentation upload guide 2026-09-03 12:03:58 +03:00
“Naeel” 423c74d3f1 docs: record verified documentation upload pipeline 2026-09-03 12:03:12 +03:00
“Naeel” 085a310720 test: add terraform init configs for all stands 2026-09-03 11:52:47 +03:00
“Naeel” b76e0d1086 fix(generator): inherit nested schema across operation params 2026-09-03 10:54:59 +03:00
“Naeel” 9ebe5b19d6 docs: remove S3 credentials from release plans 2026-09-03 10:53:36 +03:00
“Naeel” 62d8d7b45d docs: record universal dev generator fix plan and Sol review prompt 2026-09-03 10:36:45 +03:00
“Naeel” 02b7d7b701 fix(docs): per-stand getting-started injection (source namespace, version, api_endpoint) after copy into docs_dir; placeholders {{NAMESPACE}} 2026-09-03 09:19:56 +03:00
“Naeel” 9090488731 reversion: prod=1.*, dev=2.*, test=3.*; cleanup registry; test 3.0.0 + prod 1.0.0 uploaded (dev skipped: jsonEnv nested bug) 2026-09-03 09:07:05 +03:00
“Naeel” 9e02b696ba fix(docs-pipeline): default DOCS_GEN_DIR -> generated/<stand>/docs; document env setup + S3 via VM 2026-09-03 08:06:59 +03:00
“Naeel” 9fd7334a60 feat(docs): version badge v0.1 in header right corner (manual bump on changes) 2026-09-03 07:32:59 +03:00
“Naeel” 72a8a491c6 docs: актуализация DOCS_PIPELINE/README (без версий, новый хост); старый -> legacy 2026-09-03 07:27:58 +03:00
“Naeel” dc469c6dce feat: publish docs without version (mc mirror overwrite) + site_url per stand 2026-09-02 18:36:55 +03:00
“Naeel” 211143980c fix: restore scripts/publish-docs.sh (docs upload to S3 terraform-registry) 2026-09-02 14:40:53 +03:00
“Naeel” 2414647337 docs: record full docs pipeline analysis 2026-09-02 11:42:37 +03:00
“Naeel” 97fd77c2e9 docs: update provider version to 5.0.5 in getting-started guide 2026-09-02 08:09:29 +03:00
“Naeel” b3342bc0c5 docs: add DOCS_PIPELINE — инструкция по генерации и заливке MkDocs-документации 2026-09-01 08:43:55 +03:00
“Naeel” ed568a867a Record stand configuration and remove exposed secret 2026-08-31 20:20:18 +03:00
“Naeel” 86871498a2 Fix provider review findings 2026-08-31 20:19:31 +03:00
“Naeel” 75c868e0b5 chore: bump test docs version to 5.0.7 2026-08-31 17:04:08 +03:00
“Naeel” d61c8d5cb7 docs: fix registry URL in getting started guide 2026-08-31 16:49:10 +03:00
“Naeel” 05694a3446 stand(CRUD): refactor resource names, drop git_revision, add adopt_existing, provider 5.0.5 2026-08-13 12:49:46 +04:00
279 changed files with 19808 additions and 2396 deletions
+87
View File
@@ -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]]
}
Executable
+67
View File
@@ -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
}
+54
View File
@@ -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
}
}
}
+23 -55
View File
@@ -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 и НЕ использовать как источник правил для новой логики.
⚠️ ЗАПРЕТ НА ПРЕДПОЛОЖЕНИЯ
Не знаешь значение переменной? СПРОСИ.
Не уверен в конфигурации среды? СПРОСИ.
Запрещено действовать на основе догадок.
🔑 РАБОТА С ТОКЕНАМИ
## Общий принцип
- Соблюдать инструкции, давать подтверждения и не делать самостоятельных изменений.
- Не полагаться на память — всегда проверять актуальность инструкций.
+9
View File
@@ -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,6 +33,9 @@ provider/generated/
*.exe
*.test
*.out
# Локально собранный провайдер под dev_overrides (см. TMP/terraformrc.dev)
TMP/devbin/
terraform-provider-nubes
# === Build artifacts (generated by devops scripts) ===
devops/profiles/*/generated/
@@ -68,6 +74,8 @@ HAR/*.har
*.token
secrets/private_key.asc
secrets/.s3cfg_registry
secrets/.s3cfg_provider
secrets/.s3cfg*
secrets/pearlharbor_registry.txt
secrets/id_ed25519.txt
@@ -112,5 +120,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/
+1 -1
View File
@@ -2,7 +2,7 @@ terraform {
required_providers {
nubes = {
source = "tf-registry.containerk8s.services.ngcloud.ru/nubes-dev/nubes"
version = "3.1.13"
version = "2.0.0"
}
}
}
+28
View File
@@ -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
}
+60
View File
@@ -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
}
+29
View File
@@ -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
}
+4
View File
@@ -0,0 +1,4 @@
provider "nubes" {
api_token = var.api_token
api_endpoint = var.api_endpoint
}
+161
View File
@@ -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\"}]"
+116
View File
@@ -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"
}
+20
View File
@@ -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
}
+10
View File
@@ -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"
}
}
}
+189
View File
@@ -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
}
+28
View File
@@ -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
}
+60
View File
@@ -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
}
+29
View File
@@ -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
}
+4
View File
@@ -0,0 +1,4 @@
provider "nubes" {
api_token = var.api_token
api_endpoint = var.api_endpoint
}
+161
View File
@@ -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\"}]"
+116
View File
@@ -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"
}
+20
View File
@@ -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
}
+10
View File
@@ -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"
}
}
}
+1 -1
View File
@@ -2,7 +2,7 @@ terraform {
required_providers {
nubes = {
source = "tf-registry.containerk8s.services.ngcloud.ru/nubes-test/nubes"
version = "5.1.16"
version = "3.0.0"
}
}
}
+1 -1
View File
@@ -2,7 +2,7 @@ terraform {
required_providers {
nubes = {
source = "tf-registry.containerk8s.services.ngcloud.ru/nubes-dev/nubes"
version = "3.1.1"
version = "2.0.0"
}
}
}
+1 -1
View File
@@ -2,7 +2,7 @@ terraform {
required_providers {
nubes = {
source = "tf-registry.containerk8s.services.ngcloud.ru/nubes-dev/nubes"
version = "3.1.13"
version = "2.0.0"
}
}
}
+134
View File
@@ -0,0 +1,134 @@
# Документация MkDocs: генерация и заливка в реестр
> ⛔⛔⛔ **НЕ ЛОМАТЬ РАБОТАЮЩИЙ КОД** ⛔⛔⛔
>
> Эта папка — **справочная**. Скрипты пайплайна в `TOOLS/scripts/` и `scripts/`
> работают и должны оставаться **нетронутыми**.
> Любая правка в них — только после явного «делай» и с проверкой, что ничего не сломалось.
---
## Что здесь
Всё про **генерацию документации** провайдера Nubes, **сборку** MkDocs-сайта
и **заливку** статики в S3-реестр.
## Два независимых потока
### A. Генерация Markdown-доков по ресурсам (API → YAML → .md)
| Шаг | Скрипт | Что делает |
|---|---|---|
| 1 | `TOOLS/scripts/01_generate_yamls.sh --profile TOOLS/config/<стенд>` | Тянет спецификации из API стенда → `generated/<стенд>/resources_yaml/` |
| 2 | `TOOLS/scripts/02_generate_resources_and_docs_v2.sh --profile TOOLS/config/<стенд>` | YAML → Go-код (`TOOLS/bin/resource-generator`) + Markdown-доки (`TOOLS/bin/docs-generator`) в `generated/<стенд>/docs/` |
| 3 (опц.) | `TOOLS/scripts/05_generate_docs_llm.py` | Прогоняет .md через LLM (улучшение описаний) |
| 4 | `TOOLS/scripts/03_build_and_upload_provider.sh --profile ... <ver>` | Сборка провайдера + GPG-подпись + заливка бинарников в S3 |
### B. Сборка MkDocs-сайта + заливка доков в S3
| Шаг | Скрипт | Что делает |
|---|---|---|
| 1 | `TOOLS/scripts/04_build_and_publish_docs.sh --profile ... <ver>` | Генерирует `.mkdocs.tmp.yml` (версия/`docs_dir`/nav), собирает сайт (docker → venv → system mkdocs) в `site/` |
| 2 | `scripts/publish-docs.sh` | Заливает `site/` в S3 (`mc cp --recursive` + `mc policy set public`) |
| 3 (опц.) | `scripts/publish-doc-page.sh` | Заливка **одной** страницы |
| CI | `.github/workflows/publish-docs.yml` | Авто-публикация по git-тегу `v*.*.*` |
---
## Команды (полный цикл, стенд = dev/test/prod)
```bash
# DEV (пример)
./TOOLS/scripts/01_generate_yamls.sh --profile TOOLS/config/dev
./TOOLS/scripts/02_generate_resources_and_docs_v2.sh --profile TOOLS/config/dev
./TOOLS/scripts/03_build_and_upload_provider.sh --profile TOOLS/config/dev 3.1.13
./TOOLS/scripts/04_build_and_publish_docs.sh --profile TOOLS/config/dev 3.1.13
```
**Быстрая заливка** (YAML/Go уже сгенерированы, не менялись) — только шаг 3/4:
```bash
./TOOLS/scripts/03_build_and_upload_provider.sh --profile TOOLS/config/test 5.1.17
./TOOLS/scripts/04_build_and_publish_docs.sh --profile TOOLS/config/test 5.1.17
```
### Ручная заливка доков (рабочий способ)
```bash
# S3-креды из secrets/.s3cfg_registry (или env S3_ENDPOINT/S3_ACCESS_KEY/S3_SECRET_KEY)
/home/naeel/terra/scripts/publish-docs.sh \
site \
tf-registry.containerk8s.services.ngcloud.ru \
nubes nubes 2.0.2
```
---
## Список файлов
### Скрипты (пайплайн)
- `TOOLS/scripts/01_generate_yamls.sh`
- `TOOLS/scripts/02_generate_resources_and_docs_v2.sh`
- `TOOLS/scripts/03_build_and_upload_provider.sh`
- `TOOLS/scripts/04_build_and_publish_docs.sh`
- `TOOLS/scripts/05_generate_docs_llm.py`
- `TOOLS/scripts/build-provider.sh`
- `scripts/publish-doc-page.sh`
- `scripts/publish-docs.sh` ← ⚠️ см. «Известная проблема» ниже
### Генераторы (Go-бинарники)
- `TOOLS/bin/resource-generator`
- `TOOLS/bin/docs-generator`
- `TOOLS/bin/yaml-generator`
### Конфиг
- `mkdocs.yml` — конфиг MkDocs (site_url, nav, тема material)
- `TOOLS/config/registry.env` — реестр (`REGISTRY_HOSTNAME`, `S3_ENDPOINT`, `S3_BUCKET`)
- `TOOLS/config/{dev,test,prod}/profile.env` — стенд (`NUBES_API_ENDPOINT`, `NAMESPACE`, `VERSION`)
- `TOOLS/config/{dev,test,prod}/services_list.txt`
- `TOOLS/config/{dev,test,prod}/operation_timeouts.json`
### Секреты
- `secrets/{dev,test,prod}.token`
- `secrets/private_key.asc` — GPG-подпись
- `secrets/.s3cfg_registry` — S3-креды
### Контент / ассеты
- `docs/` — ручные источники (`index.md`, `curated/`, `help/`, `30_registry/` и др.)
- `docs/30_registry/` — `guides/`, `resources/`, `assets/`, `javascripts/fix-slash.js`
- `generated/<стенд>/docs/` — сгенерированные доки (включая `_nav_fragment.yml`)
- `site/`, `site_test/` — результат сборки
---
## S3 / бакеты
| Что | Бакет | Путь |
|---|---|---|
| **Документация** | `terraform-registry` | `docs/<namespace>/<name>/<version>/` |
| **Бинарники провайдера** | `nubes-terraform-registry` | `<host>/<namespace>/<name>/<version>/` |
- Эндпоинт S3: `https://s3.msk-1.ngcloud.ru`
- Хост реестра: `tf-registry.containerk8s.services.ngcloud.ru`
- Клиент: `mc` (MinIO), алиасы `prod-s3`/`reg`/`registry`/`tfreg`
---
## ⚠️ Известная проблема: `scripts/publish-docs.sh` отсутствует в этом репозитории
1. Скрипт `scripts/publish-docs.sh` **удалён** из `/home/naeel/tf_provider`
коммитом `c2438f5` (2026-07-05, «superseded by devops/»).
2. Но `TOOLS/scripts/04_build_and_publish_docs.sh` (строка ~280) и
`.github/workflows/publish-docs.yml` (строка ~54) **до сих пор вызывают**
`./scripts/publish-docs.sh`.
3. **Следствие:** запуск `04` из этого репозитория соберёт сайт, но упадёт
на шаге заливки (`No such file or directory`). CI по тегу — аналогично.
**Рабочая копия скрипта живёт в старом репозитории** (отдельный git, не клон):
- `/home/naeel/terra/scripts/publish-docs.sh`
- архив: `/home/naeel/terraform__OFF/scripts/publish-docs.sh`
Копия этого скрипта сохранена рядом: [`publish-docs.sh`](./publish-docs.sh)
### Варианты устранения (только после «делай»)
1. Восстановить `scripts/publish-docs.sh` в это репозиторий (из копии рядом или из git `c2438f5^`).
2. Инлайнить заливку прямо в `04_build_and_publish_docs.sh` (как уже сделано в `publish-doc-page.sh`).
+176
View File
@@ -0,0 +1,176 @@
# Документация провайдера Nubes: генерация и публикация
> Актуально на 2026-09-03. Историческая версия — [`README.legacy.md`](./README.legacy.md).
## Общая схема
```
API стенда ──▶ generated/<стенд>/resources_yaml/ ──▶ generated/<стенд>/docs/ (.md)
│ (docs_dir для MkDocs)
▼
MkDocs build ──▶ site/ (HTML)
│
▼
S3 terraform-registry/docs/<namespace>/<name>/ (без версии, public)
│
▼
ВМ 5.172.178.213 nginx (зеркало /var/www/tf-docs/) ◀─ под tf_docs (proxy)
│
▼
https://tf-docs.nodejsk8s.dev.nubes.ru/<namespace>/
```
Ключевые принципы:
- **Без версий в URL**: docs публикуются в `docs/<namespace>/<name>/` перезаписью (`mc mirror --overwrite --remove`).
- **Вечный бесплатный домен**: `tf-docs.nodejsk8s.dev.nubes.ru/<namespace>/` (managed-кластер → под-прокси → ВМ nginx).
- Имя провайдера (`<name>`) во всех стендах — `nubes`; в URL сайта не фигурирует (только `<namespace>`), в S3-ключе — есть.
## Стенды
| Стенд | profile.env | Namespace (S3/URL) | API-эндпоинт | Токен |
|---|---|---|---|---|
| dev | `TOOLS/config/dev/profile.env` | `nubes-dev` | `https://lk-api-gateway-dev.ngcloud.ru/api/v1/svc` | `secrets/dev.token` |
| test | `TOOLS/config/test/profile.env` | `nubes-test` | `https://lk-api-gateway-test.ngcloud.ru/api/v1/svc` | `secrets/test.token` |
| prod | `TOOLS/config/prod/profile.env` | `nubes` | `https://lk-api-gateway.ngcloud.ru/api/v1/svc` | `secrets/prod.token` |
В `profile.env` также: `PROVIDER_NAME=nubes`, пути GPG-ключей, актуальная `VERSION` стенда.
## Нумерация версий провайдера по стендам
> ⛔ **ЕДИНСТВЕННАЯ схема (с 2026-09-03).** Старые диапазоны (`prod=2.*`, `dev=3.*`,
> `test=5.*`, а также `0.0.x`) — ЛЕГАСИ, **НЕ ИСПОЛЬЗОВАТЬ**. Полная чистка реестра
> выполнена 2026-09-03 — старые версии удалены из S3.
| Стенд | Диапазон версий | Первая |
|---|---|---|
| **prod** (`nubes`) | `1.*.*` | `1.0.0` |
| **dev** (`nubes-dev`) | `2.*.*` | `2.0.0` |
| **test** (`nubes-test`) | `3.*.*` | `3.0.0` |
Версия передаётся аргументом в `03_build_and_upload_provider.sh <ver>` и хранится в
`VERSION` в `profile.env`. Источник правды — [`VERSIONS.md`](../../VERSIONS.md).
## Поток A — генерация Markdown (API → YAML → .md)
| Шаг | Скрипт | Результат |
|---|---|---|
| 1 | `TOOLS/scripts/01_generate_yamls.sh --profile TOOLS/config/<стенд>` | спецификации ресурсов из API → `generated/<стенд>/resources_yaml/` |
| 2 | `TOOLS/scripts/02_generate_resources_and_docs_v2.sh --profile TOOLS/config/<стенд>` | YAML → Go-код провайдера + Markdown-доки → `generated/<стенд>/docs/` (в т.ч. `_nav_fragment.yml`) |
| 3 (опц.) | `TOOLS/scripts/05_generate_docs_llm.py` | LLM-улучшение описаний `.md` |
| 4 | `TOOLS/scripts/03_build_and_upload_provider.sh --profile ... <ver>` | сборка провайдера + GPG-подпись + бинарники в S3 (не docs) |
## Поток B — сборка MkDocs-сайта и публикация
| Шаг | Скрипт | Что делает |
|---|---|---|
| 1 | `TOOLS/scripts/04_build_and_publish_docs.sh --profile TOOLS/config/<стенд> [ver]` | собирает сайт и публикует (см. ниже) |
| 2 | `scripts/publish-docs.sh <site> <host> <ns> <name>` | заливка `site/` в S3 (см. ниже) |
| 3 (опц.) | `scripts/publish-doc-page.sh` | заливка одной страницы |
| CI | `.github/workflows/publish-docs.yml` | авто-публикация по git-тегу `v*.*.*` |
### Детали шага 04
1. Читает `profile.env` стенда (`--profile`): `NAMESPACE`, `VERSION`, `NUBES_API_ENDPOINT`, `REGISTRY_HOST` (default `tf-docs.nodejsk8s.dev.nubes.ru`).
2. `MKDOCS_DOCS_DIR` = `generated/<стенд>/docs` — **никогда не сливается с ручным `docs/`**.
3. Копирует ручные ассеты в сгенерированный каталог: `docs/30_registry/` и `docs/curated/` → `generated/<стенд>/docs/`.
4. Подставляет в `generated/<стенд>/docs/guides/getting-started.md` актуальные `version` и `api_endpoint`.
5. Генерирует `.mkdocs.tmp.yml` из `mkdocs.yml`:
- `site_url: https://<REGISTRY_HOST>/<NAMESPACE>/`;
- `docs_dir` — относительный на `generated/<стенд>/docs`;
- в `nav` секция «Ресурсы» заменяется на `resources_nav` из `_nav_fragment.yml`.
6. Сборка в `site/` (по убыванию приоритета): docker `squidfunk/mkdocs-material` → `.venv` python mkdocs → системный `mkdocs`. Пинованные версии: `mkdocs==1.6.1`, `mkdocs-material==9.7.3`.
7. Заливка: `./scripts/publish-docs.sh site "$REGISTRY_HOST" "$NAMESPACE" "$PROVIDER_NAME" "$VERSION"`.
- ⚠️ `publish-docs.sh` принимает 4 аргумента (`site host ns name`); 5-й (`VERSION`) игнорируется — публикация всегда без версии.
### Детали publish-docs.sh (актуальный)
- Берёт S3-креды из `S3_ENDPOINT/S3_ACCESS_KEY/S3_SECRET_KEY` (или legacy `MINIO_*`), при вызове из `04` — подгружаются из `secrets/.s3cfg_registry`.
- `mc alias set registry <endpoint> <ak> <sk> --api S3v4`.
- `mc mirror --overwrite --remove "$SITE_DIR/" → registry/terraform-registry/docs/<namespace>/<name>/`.
- `mc policy set public` на target.
- Публикация «на месте»: старые файлы удаляются, версий нет.
## Промежуточные файлы и папки
| Папка/файл | Назначение |
|---|---|
| `generated/<стенд>/resources_yaml/` | сырые YAML-спеки из API (шаг A1) |
| `generated/<стенд>/docs/` | сгенерированные Markdown + `_nav_fragment.yml` (docs_dir для MkDocs) |
| `site/` | результат сборки MkDocs (HTML), заливается в S3 |
| `site_test/` | тестовая сборка по `.mkdocs.docs_test.yml` |
| `docs/` | ручные источники (`index.md`, `curated/`, `help/`, `30_registry/`); внутренние разделы (`00_overview`, `20_discovery`, `40_analysis`, `50_history`, `60_strategy`, `70_api`, `help/*`, `README.md`, `ai_universal_provider_gen.md`) исключаются через `exclude_docs` |
| `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/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) |
| `.mkdocs.tmp.yml` | генерируется в 04, удаляется по trap |
| `.mkdocs.docs_test.yml` | конфиг тестовой сборки (site_test) |
| `DOCS_PIPELINE/publish-docs.sh` | ⚠️ легаси-копия старого скрипта (с версией, `mc cp`); **не использовать** |
## S3 и хостинг
| Что | Бакет | Ключ |
|---|---|---|
| Документация | `terraform-registry` (public) | `docs/<namespace>/<name>/` — без версии |
| Бинарники провайдера | `nubes-terraform-registry` | `<host>/<namespace>/<name>/<version>/` |
- S3-эндпоинт: `https://s3.msk-1.ngcloud.ru` (Ceph RGW). Клиент `mc` (алиасы `prod-s3`/`reg`/`registry`/`tfreg`).
- Доставка до браузера: S3 → ВМ-зеркало (`/var/www/tf-docs/`) → nginx ВМ отдаёт `/<namespace>/` → под `tf_docs` (reverse-proxy в кластере) → `https://tf-docs.nodejsk8s.dev.nubes.ru/<namespace>/`.
- ВМ отдаёт также по прямому IP `http://5.172.178.213/<namespace>/`.
## Требования к окружению (настроено 2026-09-03)
Чтобы пайплайн работал **штатно и не ломался**, на машине сборки должно быть:
| Компонент | Как проверить | Что ставить |
|---|---|---|
| `python3-venv` (Debian/Ubuntu) | `python3 -m venv /tmp/v && ls /tmp/v/bin/pip` | `sudo apt install -y python3.12-venv` — без него venv создаётся БЕЗ pip |
| `.venv` проекта с mkdocs | `.venv/bin/python -m mkdocs --version` | пересоздать: `rm -rf .venv && python3 -m venv .venv && .venv/bin/pip install mkdocs==1.6.1 mkdocs-material==9.7.3` |
| Системный mkdocs (запасной) | `python3 -m mkdocs --version` | `pip3 install --user mkdocs==1.6.1 mkdocs-material==9.7.3` |
| `mc` (MinIO client) | `mc --version` | см. docs min.io |
| docker + образ `squidfunk/mkdocs-material` (запасной) | `docker images` | `docker pull squidfunk/mkdocs-material` |
> **Почему так.** `04` при `--profile` собирает через `.venv` проекта. Если `.venv` пустой/сломан (нет pip/mkdocs) — сборка падает. Корень: без системного пакета `python3.12-venv` виртуальное окружение создаётся без `pip`/`ensurepip`. Это чинится один раз (apt + пересоздание `.venv`), дальше не ломается.
> Версии зафиксированы: `mkdocs==1.6.1`, `mkdocs-material==9.7.3` (совпадают и в системном python3, и в `.venv`).
## Публикация: где запускать `mc mirror`
S3 (`s3.msk-1.ngcloud.ru`) из локальной сети **рвёт большие ответы** (рекурсивный листинг >нескольких сотен объектов зависает: `mc: Unable to list ... unexpected EOF`; малые `mc ls`/`mc cp` работают). Поэтому **заливку на S3 делать с ВМ `5.172.178.213`** — у неё быстрый канал до S3 (~10 МБ/с).
Полный цикл публикации стенда (сборка локально → S3 с ВМ → зеркало на ВМ):
```bash
# 1. Сборка (локально, штатно)
./TOOLS/scripts/04_build_and_publish_docs.sh --profile TOOLS/config/test 5.0.8
# (если site/ собирался docker-ом от root — mkdocs не сможет его перезаписать:
# sudo rm -rf site или docker run --rm -v $PWD:/docs --entrypoint rm squidfunk/mkdocs-material -rf /docs/site)
# 2. Передать собранный site/ на ВМ
tar -C site -cf - . | ssh naeel@5.172.178.213 'rm -rf ~/tmp-docs-site && mkdir -p ~/tmp-docs-site && tar -C ~/tmp-docs-site -xf -'
# 3. Залить на S3 с ВМ (быстрый канал)
ssh naeel@5.172.178.213 'mc mirror --overwrite --remove ~/tmp-docs-site/ registry/terraform-registry/docs/nubes-test/nubes/'
# 4. Обновить зеркало /var/www/tf-docs (откуда nginx отдаёт сайт)
ssh naeel@5.172.178.213 'mc mirror --overwrite --remove "registry/terraform-registry/docs/nubes-test/nubes/" /var/www/tf-docs/nubes-test/'
```
> ⚠️ Если `mc mirror`/`mc ls -r` локально зависает — это не баг скрипта, а сеть до S3; заливать с ВМ.
## Быстрые команды
```bash
# Полный цикл для стенда dev
./TOOLS/scripts/01_generate_yamls.sh --profile TOOLS/config/dev
./TOOLS/scripts/02_generate_resources_and_docs_v2.sh --profile TOOLS/config/dev
./TOOLS/scripts/04_build_and_publish_docs.sh --profile TOOLS/config/dev
# Только пересборка и публикация (YAML/Go не менялись)
./TOOLS/scripts/04_build_and_publish_docs.sh --profile TOOLS/config/test
# Ручная заливка уже собранного site/
scripts/publish-docs.sh site tf-docs.nodejsk8s.dev.nubes.ru nubes-test nubes
```
+34
View File
@@ -0,0 +1,34 @@
#!/usr/bin/env bash
set -euo pipefail
# Заливка собранного MkDocs-сайта (site/) в S3-реестр.
# Копия рабочего скрипта из старого репозитория /home/naeel/terra/scripts/publish-docs.sh.
# ⚠️ НЕ ЛОМАТЬ РАБОТАЮЩИЙ КОД: этот файл — справочная копия, не подменяет пайплайн.
# Usage: publish-docs.sh <site-dir> <registry-host> <namespace> <name> <version>
SITE_DIR=${1:-site}
REGISTRY_HOST=${2:-tf-registry.containerk8s.services.ngcloud.ru}
NAMESPACE=${3:-nubes}
NAME=${4:-nubes}
VERSION=${5:-dev}
# Support both S3_* (New Standard) and MINIO_* (Legacy) variables
ENDPOINT=${S3_ENDPOINT:-${MINIO_ENDPOINT:-}}
ACCESS_KEY=${S3_ACCESS_KEY:-${MINIO_ACCESS_KEY:-}}
SECRET_KEY=${S3_SECRET_KEY:-${MINIO_SECRET_KEY:-}}
if [ -z "$ENDPOINT" ] || [ -z "$ACCESS_KEY" ] || [ -z "$SECRET_KEY" ]; then
echo "Error: S3_ENDPOINT/S3_ACCESS_KEY/S3_SECRET_KEY must be set"
exit 2
fi
MC_ALIAS=registry
mc alias set $MC_ALIAS "$ENDPOINT" "$ACCESS_KEY" "$SECRET_KEY" --api S3v4
TARGET="${MC_ALIAS}/terraform-registry/docs/${NAMESPACE}/${NAME}/${VERSION}/"
# mc создаёт промежуточные каталоги неявно при копировании
mc cp --recursive "$SITE_DIR/" "$TARGET"
# Публичная политика на бакет
mc policy set public "$TARGET" || true
echo "Published docs to: https://${REGISTRY_HOST}/docs/${NAMESPACE}/${NAME}/${VERSION}/"
+173
View File
@@ -0,0 +1,173 @@
# Code Review провайдера — Opus — 2026-08-31
**Источник:** анализ и код-ревью через VS Code Copilot Chat
**Статус:** анализ завершён; часть исправлений внесена 2026-08-31
## Область анализа
Проверены:
- рукописное ядро провайдера в `provider/internal/core` и `provider/internal/resources_core`;
- CRUD, state management и валидация;
- HTTP-слой и `client.go`;
- регистрация провайдера и TLS-настройки;
- генераторы Go-ресурсов, YAML и build-пайплайн;
- Python- и shell-скрипты;
- gateway.
## Критичные находки
### 1. Отладочный лог с данными инстансов пишется в `/tmp` безусловно
В `provider/internal/core/client.go:629-637` замыкание `debug()` в `FindInstanceByDisplayName` всегда пишет в `/tmp/nubes_find_debug.log` с правами `0644`. В лог попадают `instanceUid`, `displayName` и `serviceId`.
Файл не защищён условием `NUBES_DEBUG_HTTP`, не ротируется и не очищается. Это создаёт риск раскрытия данных и неконтролируемого роста файла.
**Рекомендация:** убрать постоянную запись либо включать её только через явный debug-флаг; использовать безопасный путь и контролируемую ротацию.
### 2. Bearer-токен попадает в stderr при HTTP-отладке
В `provider/internal/core/client.go:1100-1101` вызов `httputil.DumpRequestOut(req, ...)` выводит полный исходящий запрос вместе с заголовком `Authorization: Bearer <token>` при `NUBES_DEBUG_HTTP=1`.
Токен может попасть в логи CI/CD или окружения выполнения.
**Рекомендация:** перед дампом удалять или маскировать `Authorization`; не выводить секреты ни в одном режиме.
### 3. В Python-скрипте сетевые вызовы выполняются без таймаутов
В `scripts/check_cloud_instances.py:87-88` вызовы `self.session.get(...)` не передают `timeout=`. При зависании API процесс может ожидать ответ бесконечно.
**Рекомендация:** добавить явные таймауты ко всем HTTP-вызовам и определить единое значение или конфигурационный параметр.
## Существенные находки
### 4. Retry сетевых ошибок применяется к POST-запросам
В `provider/internal/core/client.go:1113-1120` при сетевой ошибке повторяется любой HTTP-метод, включая POST к `/instances` и `/instanceOperations`.
Если сервер принял запрос, но ответ потерян, повтор может создать дубликат инстанса или операции. Идемпотентность POST не гарантирована.
**Рекомендация:** ограничить retry идемпотентными методами либо использовать идемпотency key и явную серверную поддержку повторов.
### 5. Ответ `401 Unauthorized` включён в retryable
В `provider/internal/core/client.go:1150-1156` статус `401` считается повторяемым. Протухший или неверный токен приводит к трём попыткам с задержкой, маскируя исходную ошибку авторизации и увеличивая время отказа.
**Рекомендация:** исключить `401` из retryable; возвращать ошибку авторизации сразу.
### 6. Gateway раскрывает внутренние upstream-адреса
В `gateway/server.js:60-71` корневой endpoint `/` и обработчик 404 возвращают наружу адреса `upstream` для маршрутов.
Публичный ответ раскрывает внутреннюю топологию сервисов.
**Рекомендация:** убрать `upstream` из публичных ответов; внутренние адреса оставлять только в серверных логах с необходимой санацией.
### 7. Некорректное определение неуспешной операции в Python
В `scripts/check_cloud_instances.py:187-189` используется сравнение `last_op.get("isSuccessful") == False`. При отсутствии поля возвращается `None`, поэтому состояние `OPERATION_FAILED` не определяется.
**Рекомендация:** использовать проверку `is False` либо явно обрабатывать отсутствие ключа согласно контракту API.
## Умеренные находки
### 8. Retry-логика дублируется в трёх местах
В `provider/internal/core/client.go:777-905` похожие циклы retry присутствуют в `doRequest`, `GetInstanceState` и `GetInstanceStateRaw`.
Дублирование увеличивает риск расхождения поведения и повторного появления ошибок безопасности.
**Рекомендация:** вынести общую retry-логику в единый внутренний helper с параметрами метода, таймаутов и политики повторов.
### 9. Пагинация имеет тихий предел 10 000 инстансов
В fallback-ветке `FindInstanceByDisplayName` (`provider/internal/core/client.go:747-749`) поиск прекращается после `page > 100` при размере страницы `100`.
При большем количестве инстансов совпадение может не быть найдено без предупреждения.
**Рекомендация:** убрать произвольный предел либо возвращать диагностируемую ошибку/предупреждение при достижении лимита.
### 10. Ошибка `gofmt` не останавливает генерацию
`FormatSourceOrWarn` в `TOOLS/resource-generator/writers.go:61` при ошибке форматирования только выводит предупреждение и записывает исходник.
В результате pipeline может сохранить неформатированный или потенциально некомпилируемый Go-код.
**Рекомендация:** считать ошибку форматирования фатальной для генерации либо выполнять последующую обязательную компиляционную проверку.
### 11. Секрет передаётся в командной строке shell-скрипта
В `TOOLS/s3_notification_example.sh:74` значение `SECRET_KEY` передаётся аргументом в `mc alias set`.
Секрет может быть виден через `ps` или аналогичный список процессов.
**Рекомендация:** использовать механизм передачи секрета через stdin, переменную окружения, конфигурационный файл с безопасными правами или другой поддерживаемый секретный канал.
## Дополнительные замечания
- В `provider/internal/core/client.go` ссылка на `tools/gen_v2/generate_resources_v2.go` обновлена на актуальный путь `TOOLS/resource-generator/internal/templates/instance.go`.
- В исходниках генератора (`TOOLS/resource-generator/internal/templates/*`, `TOOLS/resource-generator/internal/writers/writers.go`) метка `Code generated by tools/gen_v2` обновлена на `Code generated by TOOLS/resource-generator`.
- Текущий `provider/internal/resources_gen/registry.go` обновлён на новую метку генератора.
- `TOOLS/resource-generator/main.go` переведён на `run()` с корректным `exit code=1` и агрегированным отчётом по ошибкам записи ресурсов (instance/subresource/action).
- Пути debug-логов в `provider/internal/core/client.go` переведены на `os.TempDir()` с override через `NUBES_DEBUG_DIR` (без хардкода `/tmp`).
## Что выглядит хорошо
- Сериализация операций на инстансе через `instanceMutexes` в `client.go` защищает от параллельных операций API.
- TLS настроен с `MinVersion: TLS 1.2`; `InsecureSkipVerify` по умолчанию равен `false`.
- `api_token` отмечен как `Sensitive: true` в схеме провайдера.
- Канонизация JSON для сравнения state устраняет ложные различия из-за порядка ключей.
## Итоговый статус
| Находка | Статус |
|---|---|
| Безусловная запись данных инстансов в `/tmp` | Исправлено: debug gated + права `0600` |
| Bearer-токен в HTTP debug dump | Исправлено: `Authorization` маскируется |
| Python HTTP-вызовы без таймаутов | Исправлено: добавлен `REQUEST_TIMEOUT` |
| Retry POST-запросов | Исправлено: retry сетевых ошибок только для GET |
| `401` в retryable | Исправлено: исключён из retryable |
| Раскрытие upstream в gateway | Исправлено: `upstream` удалён из root-ответа |
| Ошибка определения `OPERATION_FAILED` | Исправлено: сравнение через `is False` |
| Дублирование retry-логики | Исправлено: общий helper для чтения состояния |
| Тихий предел пагинации | Частично исправлено: добавлена явная ошибка при достижении лимита |
| Некритичная ошибка `gofmt` в генераторе | Исправлено: fail-fast при ошибке форматирования |
| Секрет в аргументах shell-команды | Исправлено: исключена передача в argv |
## Выполненные изменения (2026-08-31)
- `provider/internal/core/client.go`:
- debug-лог `FindInstanceByDisplayName` теперь пишется только при `NUBES_DEBUG_HTTP=1`;
- права debug-логов снижены до `0600`;
- в stderr-дампе HTTP-запроса маскируется заголовок `Authorization`;
- retry сетевых ошибок ограничен методом `GET`;
- `401 Unauthorized` удалён из `isRetryable`;
- при достижении лимита fallback-пагинации возвращается явная ошибка.
- `GetInstanceState` и `GetInstanceStateRaw` переведены на общий helper `getInstanceStateWithRetry` с единым retry/HTTP-поведением.
- `scripts/check_cloud_instances.py`:
- добавлен `REQUEST_TIMEOUT = 30` и применён ко всем `session.get(...)`;
- проверка failed-операции изменена на `is False`.
- `gateway/server.js`:
- удалено поле `upstream` из публичного ответа `GET /`.
- `TOOLS/resource-generator/internal/helpers/helpers.go`:
- `FormatSourceOrWarn` переведён на fail-fast: возвращает ошибку при сбое `gofmt`.
- `TOOLS/resource-generator/internal/writers/writers.go`:
- все вызовы форматирования обрабатывают ошибку и прерывают генерацию.
- `TOOLS/resource-generator/main.go`:
- убраны `panic` на первом сбое записи ресурса;
- добавлена агрегация ошибок генерации с отчётом по каждому ресурсу;
- завершение с `exit code=1` и человекочитаемым сообщением в stderr.
- `TOOLS/resource-generator/internal/templates/instance.go`:
- обновлён marker генерации на `Code generated by TOOLS/resource-generator`.
- `TOOLS/resource-generator/internal/templates/subresource.go`:
- обновлён marker генерации на `Code generated by TOOLS/resource-generator`.
- `TOOLS/resource-generator/internal/templates/action.go`:
- обновлён marker генерации на `Code generated by TOOLS/resource-generator`.
- `provider/internal/resources_gen/registry.go`:
- обновлён marker генерации на `Code generated by TOOLS/resource-generator`.
- `scripts/s3_notification_example.sh`:
- убрана передача секрета в аргументах процесса;
- для `mc` используется временный `--config-dir` и переменная `MC_HOST_<alias>`.
- `provider/internal/core/client.go`:
- debug log path переведён на `os.TempDir()`;
- добавлен override директории через `NUBES_DEBUG_DIR`.
@@ -0,0 +1,14 @@
# Registry Getting Started URL Fix — 2026-08-31
## Проблема
В примере `required_providers` на странице `30_registry/guides/getting-started` значение `source` содержало старый адрес `registry.kube5s.ru` и вложенные HTML-комментарии `LEGACY`. Из-за этого пример Terraform был синтаксически и семантически неверным.
В этом же файле старый адрес с HTML-комментарием присутствовал в ссылке на пример Postgres.
## Решение
- `source` заменён на `tf-registry.containerk8s.services.ngcloud.ru/nubes/nubes`.
- Ссылка на пример Postgres переведена на `tf-registry.containerk8s.services.ngcloud.ru`.
- Все вставки `LEGACY` и упоминания `registry.kube5s.ru` удалены из страницы.
- Версии профилей повышены: DEV `3.0.7`, TEST `5.0.6`, PROD `2.0.7`.
@@ -0,0 +1,73 @@
# Настройка Terraform для разных стендов
Дата: 2026-08-31
## Матрица стендов
| Стенд | Рабочие каталоги | Provider source | API endpoint | Версия в найденных Terraform-файлах |
|---|---|---|---|---|
| DEV | `DEV_STAND/CRUD`, `DEV_STAND/POSTGRES`, `DEV_STAND/IOT_KAFKA_DEMO`, `DEV_STAND/SHTURVAL_MGMT` | `tf-registry.containerk8s.services.ngcloud.ru/nubes-dev/nubes` | `https://lk-api-gateway-dev.ngcloud.ru/api/v1/svc` | обычно `3.x` |
| TEST | `TEST_STAND/CRUD`, `TEST_STAND/PG`, `TEST_STAND/POSTGRES`, `TEST_STAND/MARIA_DB`, `TEST_STAND/IOT_RMQ_DEMO`, `TEST_STAND/buck0`, `TEST_STAND/kuber` | `tf-registry.containerk8s.services.ngcloud.ru/nubes-test/nubes` | `https://lk-api-gateway-test.ngcloud.ru/api/v1/svc` | обычно `5.x` |
| PROD | `PROD_STAND/PG1`, `PROD_STAND/POSTGRES`, `PROD_STAND/RABBIT` | `tf-registry.containerk8s.services.ngcloud.ru/nubes/nubes` | `https://lk-api-gateway.ngcloud.ru/api/v1/svc` | обычно `2.x` |
Provider выбирается в `terraform { required_providers { nubes { ... } } }` конкретного рабочего каталога. API endpoint задаётся в блоке `provider "nubes"`.
## Что настраивать
1. Перейти в конкретный каталог конфигурации, например `TEST_STAND/PG`.
2. Создать локальный файл `terraform.tfvars` по шаблону `terraform.tfvars.example`, если он есть.
3. Заполнить только переменные, объявленные в `main.tf`/`variables.tf`:
- `api_token` — токен того же стенда;
- `realm` — Kubernetes-платформа/кластер;
- `s3_uid` или `s3_user_uid` — UUID S3 для backup или ресурса bucket;
- `s3_name` — имя S3, если это предусмотрено конфигурацией;
- дополнительные `org_uid`, `vdc_uid`, `edge_uid`, `sizing_policy` — только для соответствующих ресурсов.
4. Проверить имена ресурсов и параметры в остальных `.tf`-файлах: `resource_name`, домены, `git_revision`, CPU, memory, replicas, disk, PostgreSQL version, backup schedule и `adopt_existing_on_create`.
5. Выполнить Terraform из этого же каталога:
```bash
terraform init
terraform plan
terraform apply
```
Для CRUD-конфигураций с PostgreSQL сначала требуется первый `terraform apply` для базы, пользователя и БД, затем второй `terraform apply` для приложений. Это прямо указано в `TEST_STAND/CRUD/README.md`.
## Передача токена
Токен не следует хранить в репозитории. Допустимые варианты:
```bash
export TF_VAR_api_token="..."
terraform plan
```
или локальный `terraform.tfvars`, исключённый из публикации. Не использовать PROD-токен в DEV/TEST и не использовать TEST-токен в PROD.
## State и backend
В проверенных стендах нет блока `backend` и отдельных backend-конфигураций. Если backend не добавлен локально, Terraform использует локальный state в рабочем каталоге (`terraform.tfstate`). Нельзя запускать два разных стенда с одним state; для общего или удалённого state нужен отдельный backend с уникальным bucket/key для каждого стенда.
## Профили сборки provider
`TOOLS/config/{dev,test,prod}/profile.env` используется скриптами сборки и публикации provider, а не Terraform-манифестами стендов:
| Профиль | API | Token file | Namespace | Версия профиля |
|---|---|---|---|---|
| `dev` | dev Gateway | `secrets/dev.token` | `nubes-dev` | `3.0.7` |
| `test` | test Gateway | `secrets/test.token` | `nubes-test` | `5.0.6` |
| `prod` | production Gateway | `secrets/prod.token` | `nubes` | `2.0.7` |
Для сборки использовать профильный pipeline из `HOWTO-UPLOAD.md`, а не смешивать профиль одного стенда с Terraform-конфигурацией другого.
## Найденные расхождения и риски
- `docs/ops/STANDS.md` содержит устаревшие `deck-api-*`, старые пути `devops/profiles` и версии, не совпадающие с `TOOLS/config/*/profile.env` и частью Terraform-файлов.
- Версии provider неоднородны даже внутри одного стенда: перед запуском нужно сверять `required_providers` конкретного каталога с опубликованной версией.
- В `PROD_STAND/PG1/terraform.tfvars` обнаружен токен в открытом виде. Его нужно отозвать/заменить в Nubes и удалить из локального файла перед публикацией или передачей репозитория.
- В отдельных PROD-файлах встречаются захардкоженные пароли и адреса внешних сервисов; их следует перенести в переменные/секретное хранилище перед использованием в общем доступе.
- `TEST_STAND/PG/README.md` указывает версии и структуры параметров, которые могут отличаться от текущего `main.tf`; источником истины для запуска считать сам каталог Terraform и lock-файл после `terraform init`.
## Синхронизация на VM
`DEV_STAND/sync.sh` и `TEST_STAND/sync.sh` синхронизируют конфигурацию на VM и исключают `.terraform`, state и lock-файл. Перед синхронизацией проверить целевой стенд и не переносить state между стендами.
@@ -0,0 +1,29 @@
# Анализ полного pipeline документации и публикации
Дата: 2026-09-02
Проверен полный маршрут `tf_provider`:
```text
Nubes API
-> TOOLS/scripts/01_generate_yamls.sh
-> generated/<stand>/resources_yaml/*.yaml
-> TOOLS/scripts/02_generate_resources_and_docs_v2.sh
-> generated/<stand>/go/*.go
-> generated/<stand>/docs/*.md + _nav_fragment.yml
-> TOOLS/scripts/05_generate_docs_llm.py (опционально)
-> TOOLS/scripts/04_build_and_publish_docs.sh
-> .mkdocs.tmp.yml
-> site/
-> S3 terraform-registry/docs/<namespace>/<name>/<version>/
```
Параллельно релиз провайдера идёт через `03_build_and_upload_provider.sh` и `build-provider.sh`: временная копия provider собирается под linux/windows/darwin, подписывается GPG и загружается в `nubes-terraform-registry/<host>/<namespace>/<name>/<version>/`.
Ключевые реализации: `TOOLS/yaml-generator/main.go`, `TOOLS/resource-generator/main.go`, `TOOLS/docs-generator/main.go`, их `internal/**`, `mkdocs.yml`, профильные конфиги `TOOLS/config/<stand>/*`, `.github/workflows/publish-docs.yml` и серверные файлы `/home/naeel/TF/tf_registry/server/{main.go,handlers.go,router_versions.go,proxy.go}`.
Обнаружен фактический разрыв: `TOOLS/scripts/04_build_and_publish_docs.sh` и CI вызывают `./scripts/publish-docs.sh`, но такого файла в `tf_provider/scripts/` нет. Справочная рабочая копия находится в `DOCS_PIPELINE/publish-docs.sh`. Поэтому генерация `site/` возможна, а штатная финальная загрузка из текущего репозитория завершается ошибкой отсутствующего файла.
Подробный пользовательский отчёт сохранён в:
`/home/naeel/TF/TMP/tf_provider_full_docs_pipeline_2026-09-02.md`
@@ -0,0 +1,17 @@
# Fix cross-stand links publication
## Cause
The source change was present in `TOOLS/docs-generator/internal/writers/writers.go`, but `TOOLS/bin/docs-generator` was an older compiled binary. TEST generation therefore continued to produce an index without the links. The build validator also incorrectly treated intentional links to other documentation roots as contamination.
## Fix and verification
- Rebuilt `TOOLS/bin/docs-generator` from the current Go source.
- Updated the validator to allow links to the DEV, TEST, and PROD documentation roots while still rejecting foreign API, dashboard, and provider values.
- Regenerated and built DEV, TEST, and PROD sequentially.
- Published one `index.html` to each active VM mirror and verified the `Другие стенды` block remotely:
- `/var/www/tf-docs/nubes-dev/index.html`
- `/var/www/tf-docs/nubes-test/index.html`
- `/var/www/tf-docs/nubes/index.html`
The S3 mirror still reports `unexpected EOF`; direct VM transfer was used for the verified publication.
@@ -0,0 +1,15 @@
# Cross-stand links on documentation index pages
## Change
The generated resource index now includes a short "Other environments" section with links to the DEV, TEST, and PROD documentation home pages. The links are added in `TOOLS/docs-generator/internal/writers/writers.go`, the actual source of `generated/<stand>/docs/index.md`.
## Publication
All three profiles were regenerated and built sequentially. Only the resulting `index.html` was transferred to the corresponding active VM mirror:
- `/var/www/tf-docs/nubes-dev/index.html`
- `/var/www/tf-docs/nubes-test/index.html`
- `/var/www/tf-docs/nubes/index.html`
Each remote file was checked for the three cross-stand links. The regular S3 mirror continued to report `unexpected EOF`, so direct VM transfer was used again.
@@ -0,0 +1,11 @@
# Current stand in documentation index
The generated resource index now shows the current environment explicitly:
- `Текущий стенд: DEV`
- `Текущий стенд: TEST`
- `Текущий стенд: PROD`
Each index lists only the two other environments with short usage comments. The namespace is passed explicitly to `docs-generator`, so the label is generated from the selected profile rather than inferred in the HTML build.
DEV, TEST, and PROD were regenerated and their individual `index.html` files were published and verified on the VM. The S3 mirror still reports `unexpected EOF`; direct VM transfer was used.
@@ -0,0 +1,85 @@
# Баг Dev-генератора: рассинхрон nested-параметра
**Дата:** 2026-09-03
**Статус:** план решения, изменения не выполнены
## Симптом
Сборка Dev-провайдера падает на сгенерированном `95_nodejs_resource.go`:
```text
plan.JsonEnv.IsNull undefined
plan.JsonEnv.IsUnknown undefined
plan.JsonEnv.ValueString undefined
```
## Причина
В Dev API один и тот же параметр `jsonEnv` описан по-разному:
- в `create` — `map` с `sub_params` (`DB_PASS`), то есть nested-параметр;
- в `modify` — `map` без `sub_params`, то есть параметр выглядит плоским.
Генератор объединяет параметры через `params.Merge`. Поэтому в канонической
`SchemaParams` `jsonEnv` становится nested и модель содержит
`*NodejsJsonEnvModel`.
Однако `params.AlignParamTypes` переносит вложенные параметры только когда у
параметра операции уже установлен `HasSubParams`. У `modify.jsonEnv` этот флаг
ложный, поэтому `ModifyParams` сохраняет scalar-представление.
Шаблон `Update` видит `modify.jsonEnv` как scalar и генерирует вызовы
`IsNull()`, `IsUnknown()` и `ValueString()`. В сгенерированной модели это
указатель на nested-структуру, поэтому Go-код не компилируется.
## Универсальное решение
Генератор не должен содержать условий для Dev, Test, Prod или конкретного
сервиса. Нужна единая нормализация всех operation params относительно общей
канонической схемы:
```text
schemaParams = Merge(createParams, modifyParams, deleteParams)
createParams = NormalizeAgainstSchema(createParams, schemaParams)
modifyParams = NormalizeAgainstSchema(modifyParams, schemaParams)
deleteParams = NormalizeAgainstSchema(deleteParams, schemaParams)
```
Нормализация должна рекурсивно переносить из канонической схемы структурные
свойства:
- `Type`;
- `HasSubParams`;
- `SubParams` и их типы.
Собственные свойства конкретной операции должны сохраняться: `ID`,
`Required`, `Default`, описания и остальные operation-specific поля.
После нормализации `SchemaParams.jsonEnv` и `ModifyParams.jsonEnv` будут иметь
одинаковую nested-структуру, а шаблон сгенерирует nested-обработку вместо
scalar-методов.
## Граница ответственности
Расхождение Dev API остаётся дефектом входной схемы, но не должно ломать
универсальный генератор. Исправление только YAML Dev или специальная проверка
`jsonEnv` были бы стендовыми обходами и не решают общий класс проблем.
## Обязательная проверка
Добавить генераторный тест на общий случай:
```text
create: map-fixed/map с sub_params
modify: тот же code без sub_params
ожидание: modify после нормализации — nested
```
Проверка результата: сгенерированный Go-код должен компилироваться, а nested
параметр не должен получать scalar-вызовы в `Update`.
## Текущий статус стендов
- Test `3.0.0` опубликован.
- Prod `1.0.0` опубликован.
- Dev `2.0.0` не опубликован: сборка остановилась на компиляции generated Go.
@@ -0,0 +1,67 @@
# 2026-09-03 — Устранение хардкодов документации и публикация DEV
## Найденная причина
Общие материалы `docs/30_registry/` и `docs/curated/` копировались в каждый `generated/<stand>/docs/`, но подстановка выполнялась только для части `getting-started.md`. Поэтому в DEV попадали TEST-значения:
- TEST provider source;
- `5.0.5`;
- TEST API endpoint;
- `deck-test.ngcloud.ru`.
Дополнительно `02_generate_resources_and_docs_v2.sh` не очищал старые generated-файлы. Ресурс, отсутствующий в текущем `services_list.txt`, мог остаться от предыдущей генерации.
## Изменения
- Общие документы используют placeholders:
- `{{NAMESPACE}}`;
- `{{VERSION}}`;
- `{{PROVIDER_SOURCE}}`;
- `{{NUBES_API_ENDPOINT}}`;
- `{{DASHBOARD_URL}}`.
- `04_build_and_publish_docs.sh` подставляет значения рекурсивно во все скопированные Markdown-файлы.
- Добавлена проверка чужих namespace, API/dashboard host и старого `registry.kube5s.ru` до сборки.
- Профиль стал обязательным; обязательные значения не берутся из PROD fallback.
- `02_generate_resources_and_docs_v2.sh` очищает только собственный `generated/<stand>/docs` перед генерацией.
- `docs-generator` больше не содержит DEV default для API/provider source.
- Базовый `mkdocs.yml` больше не содержит versioned URL.
## Проверки
- `bash -n` для обоих docs scripts — PASS.
- `go test ./...` и `go build ./...` в `TOOLS/docs-generator` — PASS.
- DEV regeneration — PASS.
- DEV MkDocs build — PASS; contamination check — PASS.
- В DEV отсутствуют `5.0.5`, TEST API, `deck-test.ngcloud.ru` и `registry.kube5s.ru`.
- Legacy generated `vc_vm_v2` удалён чистой генерацией, так как отсутствует в актуальном `services_list.txt`.
## Публикация
Локальный рекурсивный S3 mirror завершался `unexpected EOF`, поэтому exit code штатного скрипта нельзя считать достаточным подтверждением загрузки. Проверенный артефакт `site/` был передан на ВМ `5.172.178.213` по SSH и атомарно установлен в:
```text
/var/www/tf-docs/nubes-dev/
```
На ВМ проверены страницы getting-started и curated PostgreSQL:
- namespace `nubes-dev`;
- provider version `2.0.0`;
- DEV API endpoint;
- DEV dashboard URL;
- отсутствие TEST-значений.
Legacy versioned каталоги TEST ранее удалены и после публикации отсутствуют:
```text
/var/www/tf-docs/nubes-test/5.0.5
/var/www/tf-docs/nubes-test/5.0.57
```
Публичный путь документации:
```text
https://tf-docs.nodejsk8s.dev.nubes.ru/nubes-dev/
```
Публичный `curl` завершался timeout на большом HTML; содержимое активного зеркала ВМ проверено напрямую.
@@ -0,0 +1,170 @@
# 2026-09-03 — Проверенный pipeline публикации документации
## Цель
Зафиксировать фактический pipeline публикации заново сгенерированной документации провайдера, чтобы не восстанавливать его заново по догадкам.
## Источник документации
Для стенда `<stand>` используются только сгенерированные страницы:
```text
generated/<stand>/docs/
```
Ручной каталог `docs/` не используется как основной `docs_dir`. Скрипт `04_build_and_publish_docs.sh` перед сборкой копирует в сгенерированный каталог только общие материалы:
```text
docs/30_registry/
docs/curated/
```
После копирования в `30_registry/guides/getting-started.md` подставляются параметры конкретного стенда:
- namespace;
- версия провайдера;
- API endpoint.
## Актуальные скрипты
Генерация Markdown выполняется так:
```text
TOOLS/scripts/01_generate_yamls.sh
-> generated/<stand>/resources_yaml/
TOOLS/scripts/02_generate_resources_and_docs_v2.sh
-> generated/<stand>/docs/
```
Сборка сайта выполняется скриптом:
```text
TOOLS/scripts/04_build_and_publish_docs.sh --profile TOOLS/config/<stand>
```
Он создаёт временный `.mkdocs.tmp.yml`, задаёт `site_url` с namespace стенда, запускает MkDocs и создаёт:
```text
site/
```
В конце этот скрипт вызывает актуальный:
```text
./scripts/publish-docs.sh site "$REGISTRY_HOST" "$NAMESPACE" "$PROVIDER_NAME" "$VERSION"
```
## Фактическое хранилище документации
Документация хранится не в bucket бинарников провайдера. Используется отдельный bucket:
```text
terraform-registry
```
Публикация выполняется без версии. Для любого стенда целевой S3 prefix:
```text
terraform-registry/docs/<namespace>/nubes/
```
Актуальный `scripts/publish-docs.sh` использует:
```text
mc mirror --overwrite --remove site/ registry/terraform-registry/docs/<namespace>/nubes/
```
Следствие: в URL документации нет версии `2.0.0`, `3.0.0` или `1.0.0`.
## Где выполнять S3 upload
История commit `9e02b69` зафиксировала, что из локальной сети большие рекурсивные операции S3 нестабильны. Поэтому `mc mirror` для документации выполняется на ВМ:
```text
5.172.178.213
```
Проверенный порядок:
```text
1. Собрать site/ локально.
2. Передать site/ на ВМ в ~/tmp-docs-site/.
3. На ВМ выполнить:
mc mirror --overwrite --remove \
~/tmp-docs-site/ \
registry/terraform-registry/docs/<namespace>/nubes/
4. На ВМ обновить локальное зеркало:
mc mirror --overwrite --remove \
registry/terraform-registry/docs/<namespace>/nubes/ \
/var/www/tf-docs/<namespace>/
```
S3 upload и обновление зеркала — два отдельных действия. Одной загрузки в S3 недостаточно, если публичный proxy читает локальное зеркало ВМ.
## Публичная доставка
На ВМ nginx использует корень:
```text
/var/www/tf-docs/
```
Сервис `tf_docs` проксирует публичный домен на ВМ. Для любого стенда итоговый путь:
```text
/var/www/tf-docs/<namespace>/
```
Итоговый URL любого стенда:
```text
https://tf-docs.nodejsk8s.dev.nubes.ru/<namespace>/
```
Например, для DEV `<namespace>` равен `nubes-dev`, но это только значение профиля, а не отдельная логика pipeline.
Путь с версией не используется для любого стенда:
```text
https://tf-docs.nodejsk8s.dev.nubes.ru/<namespace>/<version>/
```
не является корректным URL документации.
## Важное различие с публикацией бинарников
Бинарники Terraform-провайдера публикуются в другом bucket и с версионным prefix:
```text
nubes-terraform-registry/
tf-registry.containerk8s.services.ngcloud.ru/
<namespace>/nubes/<version>/
```
Документация публикуется отдельно:
```text
terraform-registry/docs/<namespace>/nubes/
```
Не смешивать эти два pipeline.
## Legacy, который не использовать
```text
DOCS_PIPELINE/publish-docs.sh
```
Это справочная legacy-копия старого скрипта. Она использует старую схему `mc cp`, старую структуру и версионный путь. Для текущей публикации использовать:
```text
scripts/publish-docs.sh
```
## История изменений, подтверждающая схему
- `dc469c6` — публикация docs без версии, `mc mirror`, `site_url` по стенду.
- `72a8a49` — актуализация README и новый docs host; старый скрипт помечен legacy.
- `9e02b69` — зафиксирована загрузка S3 с ВМ и обновление зеркала `/var/www/tf-docs/`.
- `02b7d7b` — подстановка namespace, версии и API endpoint выполняется после копирования `30_registry` в стендовый generated docs каталог.
+61
View File
@@ -0,0 +1,61 @@
# 2026-09-03 — Чистка реестра + новая нумерация версий + баг dev
## Новая схема нумерации версий (с 2026-09-03)
| Стенд | Namespace | Диапазон | Первая |
|---|---|---|---|
| prod | `nubes` | `1.*.*` | `1.0.0` |
| dev | `nubes-dev` | `2.*.*` | `2.0.0` |
| test | `nubes-test` | `3.*.*` | `3.0.0` |
⛔ Старые схемы (`prod=2.*`, `dev=3.*`, `test=5.*`, `0.0.x`) — ЛЕГАСИ, не использовать.
Обновлено: `VERSIONS.md`, `TOOLS/config/*/profile.env`, `DOCS_PIPELINE/README.md`,
`docs/30_registry/guides/getting-started.md`.
## Чистка реестра
Из S3 (`nubes-terraform-registry`, креды super `1112_terraform`) удалены ВСЕ старые версии:
- `nubes-dev`: 3.0.2–3.0.6
- `nubes`: 2.0.2, 2.0.3, 2.0.5, 2.0.6
- `nubes-test`: 0.0.1, 5.0.1–5.0.5, 5.1.17
После чистки в каждом namespace — 0 объектов. Легаси (5.1.17 и т.д.) нигде не осталось.
## Статус перегенерации (2026-09-03)
- ✅ **test** `3.0.0` — сгенерирован и загружен (`Done. Version 3.0.0 uploaded`).
- ❌ **dev** `2.0.0` — НЕ собирается (пропущен по решению пользователя), см. баг ниже.
- ⏳ **prod** `1.0.0` — в работе.
## Баг dev: nodejs jsonEnv (create vs modify)
Симптом: `03` dev падает на компиляции сгенерированного кода:
```
internal/resources_gen/95_nodejs_resource.go:350-353:
plan.JsonEnv.IsNull / IsUnknown / ValueString undefined
(type *NodejsJsonEnvModel has no field or method ...)
```
Причина: **API dev** для nodejs `jsonEnv`:
- в `create` (op id=58) — `map` **с `sub_params`** (типизированные ключи, напр. DB_PASS) → генератор создаёт вложенную модель `NodejsJsonEnvModel`;
- в `modify` (op id=59) — `map` **без `sub_params`** → генератор для diff генерирует строковое сравнение (`IsNull/ValueString`).
У test/prod `jsonEnv` без sub_params в обоих операциях → строка → собирается.
Корень: `TOOLS/resource-generator/internal/params/params.go`, `AlignParamTypes` —
подмешивает `SubParams` из schema в modify только если `HasSubParams` уже true:
```go
if !p.HasSubParams { continue } // modify-jsonEnv (без sub) пропускается
```
Возможный фикс: наследовать `HasSubParams`/`SubParams` из schema для параметров с тем же
code. ⚠️ Нюанс: diff-шаблон исключает nested-поля из `hasServiceParamChanges` — изменение
nested jsonEnv не будет триггерить modify (нужно продумать отдельно).
**Вывод:** сервисы/структуры API стендов отличаются (dev jsonEnv — nested в create).
Каждый стенд рассматривать независимо. dev отложен до решения по генератору/API.
## Прочее (инфраструктура, этот же день)
- Токены API `secrets/*.token` были отозваны на стороне IAM (401 IAM error при валидном exp) — обновлены 2026-09-03.
- S3-креды `.s3cfg_registry` (docs) не имеют прав на бакет бинарников `nubes-terraform-registry`;
заливка бинарников — subuser `super` аккаунта `1112_terraform` (см. `tf_registry/HISTORY/HOWTO-UPLOAD.md`).
@@ -0,0 +1,20 @@
# TEST and PROD documentation publication
## Result
- TEST documentation was regenerated from `TOOLS/config/test` with version `3.0.0`.
- PROD documentation was regenerated from `TOOLS/config/prod` with version `1.0.0`.
- TEST and PROD builds were executed sequentially because both use the shared local `site/` directory.
- TEST active mirror was replaced on the VM at `/var/www/tf-docs/nubes-test/`.
- PROD active mirror was replaced on the VM at `/var/www/tf-docs/nubes/`.
## Verification
- TEST active mirror contains `714` files and its `index.html` is present.
- PROD active mirror contains `344` files and its `index.html` is present.
- TEST HTML contains the TEST dashboard/API/provider values.
- PROD HTML contains the PROD dashboard/API/provider values.
## Infrastructure note
The S3 mirror command reported `unexpected EOF` while listing the registry. Its exit status was not treated as proof of publication. Each generated site was transferred directly to the VM, validated there, and atomically installed into its corresponding active mirror.
@@ -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` не печатать, сравнивать по хешу/маскировать.
+166
View File
@@ -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,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 не привязан к реальной операции.
- Захардкоженные коды ломаются в рантайме, а не на сборке.
+142
View File
@@ -0,0 +1,142 @@
# DevOps Runbook: Provider Build Pipeline
> Перенесено из корневого `README.md` 2026-09-24 (в корне теперь — карта проекта).
> Пути и версии в тексте приведены к текущему состоянию репозитория.
Пайплайн сборки провайдера. Скрипты живут в `TOOLS/scripts/` (НЕ в корне репозитория).
## Overview
1) Generate YAML specs from API
2) Generate Go resources + documentation files from YAML
3) Build and upload provider binaries for 3 OS targets
4) Build and publish documentation site
## Documentation publishing instructions
The verified documentation generation and publishing pipeline is documented in
[`../HISTORY/2026-09-03_docs_upload_pipeline_verified.md`](../HISTORY/2026-09-03_docs_upload_pipeline_verified.md).
It covers the generated docs source, MkDocs build, the separate documentation
S3 bucket, VM upload and mirror steps, stand-specific URLs, and the legacy
script that must not be used.
## Prerequisites
- Go 1.22+
- `python3`
- `gpg`
- `mc` (MinIO/S3 client)
- Docker (for mkdocs build)
## Shared settings
S3 environment:
- `S3_ENDPOINT` (example: `https://s3.msk-1.ngcloud.ru`)
- `S3_ACCESS_KEY`
- `S3_SECRET_KEY`
Provider naming defaults:
- `REGISTRY_HOSTNAME`: `tf-registry.containerk8s.services.ngcloud.ru`
- `NAMESPACE`: `nubes`
- `NAME`: `nubes`
## Step 1: Generate YAMLs from API
Script: `TOOLS/scripts/01_generate_yamls.sh --profile TOOLS/config/<стенд>`
Input list of services:
- `TOOLS/config/services_list.txt` (service_id only)
Token options:
- `TOKEN_FILE=/home/naeel/terra/HH-MM-SS.token`, or
- `NUBES_API_TOKEN` directly
Example:
```bash
export TOKEN_FILE=/home/naeel/terra/08-33-41.token
./TOOLS/scripts/01_generate_yamls.sh --profile TOOLS/config/dev
```
## Step 2: Generate Go resources and docs
Script: `TOOLS/scripts/02_generate_resources_and_docs_v2.sh`
Example:
```bash
./TOOLS/scripts/02_generate_resources_and_docs_v2.sh --profile TOOLS/config/dev
```
Outputs:
- Go files in `generated/<stand>/go`
- Docs in `generated/<stand>/docs`
Important:
- The v2 script always rebuilds `resource-generator` and `docs-generator` from source before running.
- Do not invoke stale binaries from `TOOLS/resource-generator/bin/` or `TOOLS/docs-generator/bin/` directly.
## Step 3: Build and upload provider
Script: `03_build_and_upload_provider.sh`
Uses `registry-server-build/build-provider.sh` and signs with:
- `secrets/private_key.asc` (ignored by git)
Example:
```bash
export S3_ENDPOINT=https://s3.msk-1.ngcloud.ru
export S3_ACCESS_KEY=...
export S3_SECRET_KEY=...
./TOOLS/scripts/03_build_and_upload_provider.sh --profile TOOLS/config/dev 2.0.18
```
## Step 4: Build and publish docs
Script: `04_build_and_publish_docs.sh`
Example:
```bash
export S3_ENDPOINT=https://s3.msk-1.ngcloud.ru
export S3_ACCESS_KEY=...
export S3_SECRET_KEY=...
./TOOLS/scripts/04_build_and_publish_docs.sh --profile TOOLS/config/dev 2.0.18
```
## Notes
- The GPG private key must remain stable across releases. Do not regenerate per build.
- If the key is regenerated, the registry server must be updated to serve the new public key.
- Terraform will fail with `authentication signature from unknown issuer` if the registry public key does not match the signing key.
- `TOOLS/config/services_list.txt` — источник правды по тому, какие сервисы генерируются.
- Если меняется версия провайдера — обновить `provider/main.go` (ранее `universal_rebuild/main.go` — устаревший путь).
## One-time GPG bootstrap (do this once, keep the key stable)
1) Generate and export keys (no passphrase):
```bash
GPG_DIR=${ROOT_DIR}/secrets
GNUPGHOME=$(mktemp -d)
cat > /tmp/gpg_batch <<'EOF'
%no-protection
Key-Type: RSA
Key-Length: 4096
Subkey-Type: RSA
Subkey-Length: 4096
Name-Real: tazet@narod.ru
Name-Email: tazet@narod.ru
Expire-Date: 0
EOF
gpg --batch --homedir "$GNUPGHOME" --gen-key /tmp/gpg_batch
gpg --batch --homedir "$GNUPGHOME" --armor --export-secret-keys > "$GPG_DIR/private_key.asc"
gpg --batch --homedir "$GNUPGHOME" --armor --export > "$GPG_DIR/public_key.asc"
rm -rf "$GNUPGHOME" /tmp/gpg_batch
```
2) Update registry server public key (ASCII Armor) in:
- `registry-server-build/main.go`
- `operator/cmd/registry/main.go`
3) Rebuild and redeploy the registry server (see `docs/50_history/00_system_mechanics.md`).
4) Build and upload provider artifacts as usual.
# check string
+19 -15
View File
@@ -4,11 +4,15 @@
**Первая цифра версии жёстко привязана к стенду. НЕ ПУТАТЬ.**
| Стенд | Namespace | Первая цифра | Профиль |
| Стенд | Namespace | Диапазон | Профиль |
|---|---|---|---|
| **PROD** | `nubes` | `2.*` | `TOOLS/config/prod` |
| **DEV** | `nubes-dev` | `3.*` | `TOOLS/config/dev` |
| **TEST** | `nubes-test` | `5.*` | `TOOLS/config/test` |
| **PROD** | `nubes` | `1.*` | `TOOLS/config/prod` |
| **DEV** | `nubes-dev` | `2.*` | `TOOLS/config/dev` |
| **TEST** | `nubes-test` | `3.*` | `TOOLS/config/test` |
> ⛔ ЛЕГАСИ (не использовать): `prod=2.*`, `dev=3.*`, `test=5.*`, `0.0.1`.
> Примеры версий ниже в этом файле могут содержать легаси-номера — подставляйте актуальную
> из `../VERSIONS.md`.
## Архитектура конфигурации
@@ -23,7 +27,7 @@ TOOLS/config/
│ NUBES_API_ENDPOINT = ...dev...
│ TOKEN_FILE = secrets/dev.token
│ NAMESPACE = nubes-dev
│ VERSION = 3.x.x
│ VERSION = 2.x.x ← актуальную брать из VERSIONS.md
│
├── test/profile.env
└── prod/profile.env
@@ -65,7 +69,7 @@ cd ~/tf_provider
### Шаг 3 — Собрать и залить в реестр
```bash
./TOOLS/scripts/03_build_and_upload_provider.sh --profile TOOLS/config/dev 3.1.13
./TOOLS/scripts/03_build_and_upload_provider.sh --profile TOOLS/config/dev 2.0.18
```
Компилирует (linux/windows/darwin), подписывает GPG, заливает в S3.
@@ -75,7 +79,7 @@ cd ~/tf_provider
```bash
./TOOLS/scripts/01_generate_yamls.sh --profile TOOLS/config/dev && \
./TOOLS/scripts/02_generate_resources_and_docs_v2.sh --profile TOOLS/config/dev && \
./TOOLS/scripts/03_build_and_upload_provider.sh --profile TOOLS/config/dev 3.1.13
./TOOLS/scripts/03_build_and_upload_provider.sh --profile TOOLS/config/dev 2.0.18
```
## Быстрая заливка (без перегенерации YAML/Go)
@@ -83,14 +87,14 @@ cd ~/tf_provider
Если YAML'ы и Go-код уже сгенерированы и не менялись — только шаг 3:
```bash
# DEV
./TOOLS/scripts/03_build_and_upload_provider.sh --profile TOOLS/config/dev 3.1.13
# DEV (2.*)
./TOOLS/scripts/03_build_and_upload_provider.sh --profile TOOLS/config/dev 2.0.18
# TEST
./TOOLS/scripts/03_build_and_upload_provider.sh --profile TOOLS/config/test 5.1.17
# TEST (3.*)
./TOOLS/scripts/03_build_and_upload_provider.sh --profile TOOLS/config/test 3.0.1
# PROD
./TOOLS/scripts/03_build_and_upload_provider.sh --profile TOOLS/config/prod 2.1.23
# PROD (1.*)
./TOOLS/scripts/03_build_and_upload_provider.sh --profile TOOLS/config/prod 1.0.1
```
Креды S3 подхватываются из `secrets/.s3cfg_registry`. Или через env:
@@ -129,7 +133,7 @@ curl -s https://tf-registry.containerk8s.services.ngcloud.ru/v1/providers/nubes/
## Актуальные версии
Файл [`VERSIONS.md`](VERSIONS.md) — единственный источник правды. После каждой заливки — обновить.
Файл [`../VERSIONS.md`](../VERSIONS.md) — единственный источник правды. После каждой заливки — обновить.
## Terraform-конфиг пользователя
@@ -138,7 +142,7 @@ terraform {
required_providers {
nubes = {
source = "tf-registry.containerk8s.services.ngcloud.ru/nubes-dev/nubes"
version = "3.1.13"
version = "2.0.18"
}
}
}
@@ -1,5 +1,12 @@
# План миграции в Nubes Managed Kubernetes — Инструкции для агента
> ⚠️ **Пути `universal_rebuild/*` в этом плане — от ПРЕЖНЕЙ раскладки репозитория.** Актуально:
> `universal_rebuild/internal/*` → `provider/internal/*`; `universal_rebuild/resources_yaml` →
> `generated/<стенд>/resources_yaml`; `universal_rebuild/tools/gen` → `TOOLS/resource-generator`.
> Также план писался ДО смены схемы версий: актуально `prod=1.*`, `dev=2.*`, `test=3.*`.
> Часть про миграцию `registry.kube5s.ru` → `registry.nubes.ru` — ИСТОРИЧЕСКАЯ: актуальный реестр
> `tf-registry.containerk8s.services.ngcloud.ru` (бакет `nubes-terraform-registry`).
**Создан:** 2026-03-13 (Opus 4.6)
**Исполнитель:** Sonnet 4.6
**Статус:** Ожидает исполнения
@@ -24,7 +31,7 @@ Nubes (nubes.ru) — российский cloud-провайдер, собств
**Обязательно прочитать перед работой:**
- `REPO_CONTENTS.md` — карта репозитория
- `.github/copilot-instructions.md` — правила работы (IMMUTABILITY POLICY)
- `docs/CODEBASE_ANALYSIS_AND_ROADMAP.md` — анализ кодовой базы
- `NOTES/30_analysis/CODEBASE_ANALYSIS_AND_ROADMAP.md` — анализ кодовой базы
---
+66
View File
@@ -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,106 @@
# План: чистка реестра + новая нумерация версий по стендам
> Для Flash. Цель — убрать ВСЕ старые залитые версии (легаси) и ввести единый
> принцип нумерации, чтобы старое (5.1.17 и т.п.) больше нигде не всплывало.
## Новый принцип нумерации (ЗАФИКСИРОВАТЬ)
| Стенд | Namespace | Диапазон версий | Первая версия по новой схеме |
|---|---|---|---|
| **prod** | `nubes` | `1.*.*` | `1.0.0` |
| **dev** | `nubes-dev` | `2.*.*` | `2.0.0` |
| **test** | `nubes-test` | `3.*.*` | `3.0.0` |
> ⛔ Старые схемы (`prod=2.*`, `dev=3.*`, `test=5.*`, а также `0.0.1`) — ЛЕГАСИ.
> Никогда больше не использовать.
## Текущее состояние в S3 (нужно УДАЛИТЬ ВСЁ)
Бакет `nubes-terraform-registry`, префикс `tf-registry.containerk8s.services.ngcloud.ru/<ns>/nubes/`:
- `nubes-dev`: `3.0.2 3.0.3 3.0.4 3.0.5 3.0.6`
- `nubes` (prod): `2.0.2 2.0.3 2.0.5 2.0.6`
- `nubes-test`: `0.0.1 5.0.1 5.0.2 5.0.3 5.0.4 5.0.5 5.1.17`
## Шаг 1 — Удалить все залитые версии из S3
Креды на запись: subuser `super` аккаунта `1112_terraform`,
передаются через переменные окружения `S3_ACCESS_KEY` и `S3_SECRET_KEY`.
```bash
mc alias set super-s3 https://s3.msk-1.ngcloud.ru "$S3_ACCESS_KEY" "$S3_SECRET_KEY" --api S3v4
# Удалить ВСЕ версии каждого стенда (рекursивно, включая подфайлы)
mc rm --recursive --force super-s3/nubes-terraform-registry/tf-registry.containerk8s.services.ngcloud.ru/nubes-test/nubes/
mc rm --recursive --force super-s3/nubes-terraform-registry/tf-registry.containerk8s.services.ngcloud.ru/nubes-dev/nubes/
mc rm --recursive --force super-s3/nubes-terraform-registry/tf-registry.containerk8s.services.ngcloud.ru/nubes/nubes/
```
Проверка после удаления (должно быть пусто):
```bash
mc ls super-s3/nubes-terraform-registry/tf-registry.containerk8s.services.ngcloud.ru/nubes-test/nubes/
mc ls super-s3/nubes-terraform-registry/tf-registry.containerk8s.services.ngcloud.ru/nubes-dev/nubes/
mc ls super-s3/nubes-terraform-registry/tf-registry.containerk8s.services.ngcloud.ru/nubes/nubes/
```
## Шаг 2 — Зафиксировать новую нумерацию в конфигах и док-файлах
Обновить (каждый файл — по новому принципу prod=1.*, dev=2.*, test=3.*):
1. **`VERSIONS.md`** — таблица версий по стендам + схема:
- PROD → `1.*` (первая `1.0.0`)
- DEV → `2.*` (первая `2.0.0`)
- TEST → `3.*` (первая `3.0.0`)
2. **`TOOLS/config/prod/profile.env`** → `VERSION="1.0.0"`
3. **`TOOLS/config/dev/profile.env`** → `VERSION="2.0.0"`
4. **`TOOLS/config/test/profile.env`** → `VERSION="3.0.0"`
5. **`DOCS_PIPELINE/README.md`** — раздел про нумерацию версий (схема выше).
6. **`docs/30_registry/guides/getting-started.md`** — `version = "..."` в примере привести
к актуальной (или оставить как «подставьте нужную», но НЕ 5.0.5 и не 5.1.17).
> ⚠️ Проверить, что в этих файлах нигде не осталось `5.1.17`, `5.0.x`, `3.0.x`
> (кроме новой схемы), `2.0.x` (кроме новой `1.x` для prod). Сделать `grep -rn`.
## Шаг 3 — Перегенерировать провайдеры по новой схеме (01→02→03)
Для каждого стенда (порядок test → dev → prod), версия = первая по новой схеме:
```bash
cd /home/naeel/TF/tf_provider
# TEST → 3.0.0
./TOOLS/scripts/01_generate_yamls.sh --profile TOOLS/config/test
./TOOLS/scripts/02_generate_resources_and_docs_v2.sh --profile TOOLS/config/test
S3_ACCESS_KEY="$S3_ACCESS_KEY" S3_SECRET_KEY="$S3_SECRET_KEY" \
./TOOLS/scripts/03_build_and_upload_provider.sh --profile TOOLS/config/test 3.0.0
# DEV → 2.0.0
./TOOLS/scripts/01_generate_yamls.sh --profile TOOLS/config/dev
./TOOLS/scripts/02_generate_resources_and_docs_v2.sh --profile TOOLS/config/dev
S3_ACCESS_KEY="$S3_ACCESS_KEY" S3_SECRET_KEY="$S3_SECRET_KEY" \
./TOOLS/scripts/03_build_and_upload_provider.sh --profile TOOLS/config/dev 2.0.0
# PROD → 1.0.0
./TOOLS/scripts/01_generate_yamls.sh --profile TOOLS/config/prod
./TOOLS/scripts/02_generate_resources_and_docs_v2.sh --profile TOOLS/config/prod
S3_ACCESS_KEY="$S3_ACCESS_KEY" S3_SECRET_KEY="$S3_SECRET_KEY" \
./TOOLS/scripts/03_build_and_upload_provider.sh --profile TOOLS/config/prod 1.0.0
```
> Документацию (04) НЕ запускать — пользователь пока не просил.
## Предусловия (ПРОВЕРЕНО)
- Go 1.23.1, docker, `mc`, GPG-ключи — на месте.
- Токены API `secrets/{dev,test,prod}.token` — ОБНОВЛЕНЫ 2026-09-03 (валидны, exp 2027-03-02).
- `operation_timeouts.json` в каждом профиле — на месте.
- S3-креды на запись бинарников — `super` subuser (см. выше).
- `registry.env`: hostname `tf-registry.containerk8s.services.ngcloud.ru`, bucket `nubes-terraform-registry`.
## Контроль
После каждого `03` — сообщение `Done. Version X.Y.Z uploaded.`
После всех — в S3 должны остаться ТОЛЬКО:
- `nubes/nubes/1.0.0/`
- `nubes-dev/nubes/2.0.0/`
- `nubes-test/nubes/3.0.0/`
@@ -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) — отдельная тема.
+224
View File
@@ -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.
@@ -0,0 +1,82 @@
> ⚠️ **ПЕРЕКРЫТ (пометка 2026-09-24). НЕ ИСПОЛЬЗОВАТЬ.**
> Версия `0.0.1` объявлена ЛЕГАСИ в `PLAN_FLASH_reversion_cleanup.md`.
> Актуальная схема: prod=`1.*`, dev=`2.*`, test=`3.*`.
# План: перегенерация провайдеров всех стендов (версия 0.0.1)
> Для Flash. Генерацию выполняет Flash по этому плану. Документацию НЕ трогать.
## Цель
Перегенерировать код Terraform-провайдера Nubes для 3 стендов из API и залить
бинарники **версии 0.0.1**. Документацию не генерировать и не публиковать.
## Версия
`0.0.1` — для всех трёх стендов.
## Порядок стендов
`test` → `dev` → `prod`
## Команды (для каждого стенда, по порядку)
```bash
cd /home/naeel/TF/tf_provider
# стенд = test | dev | prod
./TOOLS/scripts/01_generate_yamls.sh --profile TOOLS/config/<стенд>
./TOOLS/scripts/02_generate_resources_and_docs_v2.sh --profile TOOLS/config/<стенд>
./TOOLS/scripts/03_build_and_upload_provider.sh --profile TOOLS/config/<стенд> 0.0.1
```
Полные команды:
```bash
# TEST
./TOOLS/scripts/01_generate_yamls.sh --profile TOOLS/config/test
./TOOLS/scripts/02_generate_resources_and_docs_v2.sh --profile TOOLS/config/test
./TOOLS/scripts/03_build_and_upload_provider.sh --profile TOOLS/config/test 0.0.1
# DEV
./TOOLS/scripts/01_generate_yamls.sh --profile TOOLS/config/dev
./TOOLS/scripts/02_generate_resources_and_docs_v2.sh --profile TOOLS/config/dev
./TOOLS/scripts/03_build_and_upload_provider.sh --profile TOOLS/config/dev 0.0.1
# PROD
./TOOLS/scripts/01_generate_yamls.sh --profile TOOLS/config/prod
./TOOLS/scripts/02_generate_resources_and_docs_v2.sh --profile TOOLS/config/prod
./TOOLS/scripts/03_build_and_upload_provider.sh --profile TOOLS/config/prod 0.0.1
```
## Что делает каждый шаг
| Шаг | Результат |
|---|---|
| `01` | тянет YAML-спеки ресурсов из API стенда → `generated/<стенд>/resources_yaml/` |
| `02` | YAML → **Go-код** (`generated/<стенд>/go/`) + `.md`-доки (`generated/<стенд>/docs/`, побочный продукт — НЕ публикуем) |
| `03` | кросс-сборка linux/windows/darwin amd64 (`go build -ldflags "-X main.version=0.0.1 -X main.address=tf-registry.containerk8s.services.ngcloud.ru/<ns>/nubes"`) → `SHA256SUMS` + GPG-подпись → `mc cp` в S3 |
## Namespace и target S3 (автоматически из profile.env + registry.env)
| Стенд | Namespace | Бинарники в S3 |
|---|---|---|
| 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=terraform-registry`.
## Чего НЕ делать
- НЕ запускать `04_build_and_publish_docs.sh` (документация не нужна сейчас).
- НЕ менять версию `0.0.1` на другую.
- НЕ трогать `.venv`/mkdocs (для 03 не нужны).
## Контроль успеха
Каждый `03` должен завершиться сообщением `Done. Version 0.0.1 uploaded.`
Проверка версии в реестре после заливки (опционально):
```bash
curl -s https://tf-registry.containerk8s.services.ngcloud.ru/v1/providers/<ns>/nubes/versions
```
(должна появиться `0.0.1`).
+23
View File
@@ -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 — см. хендовер.
+45
View File
@@ -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/` — исторические ответы по датам
@@ -15,9 +15,9 @@ Constraints:
---
## 2) Файлы для изучения (указать полные пути)
- docs/ARCHITECTURE_NEW.md — архитектурный обзор
- NOTES/30_analysis/ARCHITECTURE_NEW.md — архитектурный обзор
- docs/README.md, docs/index.md — документация / навигация
- docs/howitwasdone.md — история решений
- NOTES/50_process/howitwasdone.md — история решений
- docs/ai_universal_provider_gen.md — процесс генерации провайдера (YAML → Go)
- universal_rebuild/tools/gen/main.go — генератор (основная логика)
- universal_rebuild/internal/resources_gen/* — примеры сгенерированных ресурсов
@@ -0,0 +1,130 @@
# ТЗ для DeepSeek Flash: убрать create-time проверку существования из `ModifyPlan`
Дата: 2026-09-21 | Статус: не сделано | Версия провайдера на момент бага: 2.0.6
## Цель
Починить `terraform destroy` (и любую `tainted`-замену), который падает с
`РЕСУРС С ТАКИМ ИМЕНЕМ УЖЕ СУЩЕСТВУЕТ (RUNNING)`.
## Контекст
- Репозиторий: `/home/naeel/TF/tf_provider`
- Провайдер: `terraform-provider-nubes`, Go, `terraform-plugin-framework v1.8.0`
- Ресурсы генерируются шаблоном, **НЕ правятся руками**
- Модуль провайдера живёт в `provider/` (не в корне репозитория)
## Симптом
```
$ terraform destroy
nubes_vc_vdc.vdc: Refreshing state... [id=db2cefc3-...]
nubes_vc_nsxt.edge: Refreshing state... [id=8AAEC14D-...]
│ Error: РЕСУРС С ТАКИМ ИМЕНЕМ УЖЕ СУЩЕСТВУЕТ (RUNNING)
│ with nubes_vc_nsxt.edge,
│ on edge.tf line 1, in resource "nubes_vc_nsxt" "edge":
```
То же самое при обычном `terraform plan`.
## Причина (подтверждена фактами)
1. `nubes_vc_nsxt.edge` в state помечен **`tainted`** (следствие прошлой неудачной
apply с `vdc_group_uid`: `Provider returned invalid result object after apply`).
Проверка: `terraform.tfstate` → `instances[].status == "tainted"`.
2. Tainted-ресурс Terraform обязан **заменить** (destroy + create). Это видно в плане:
```
# nubes_vc_nsxt.edge is tainted, so must be replaced
-/+ resource "nubes_vc_nsxt" "edge" {
```
3. `terraform destroy` сначала выполняет **внутренний обычный plan**
(`Context.destroyPlan: calling Context.plan` — видно в `TF_LOG=TRACE`), и уже
на этом шаге планируется замена edge.
4. Create-узел замены вызывает `ModifyPlan` с **prior state = null**, поэтому guard
`if state != nil && !state.ID.IsNull() && ...` пропускается, и доходит до
create-time проверки существования.
5. Проверка находит **живой** инстанс в облаке (старый edge ещё не удалён — удаление
идёт на apply) → `РЕСУРС С ТАКИМ ИМЕНЕМ УЖЕ СУЩЕСТВУЕТ (RUNNING)` → plan падает →
`destroy` не начинается.
Ключевое: на уровне `ModifyPlan` **невозможно** отличить «создание нового ресурса»
от «create-узла замены» — у обоих prior state = null. Поэтому проверки существования
в `ModifyPlan` быть не должно в принципе.
Доказательство, что вызов идёт из `ModifyPlan`: `/tmp/nubes_find_debug.log` содержит
`[FIND-DEBUG] PlanExistingResourceDiagnostics entered: serviceId=22 name="fullpipe-edge"`.
Эту строку пишет только Plan-функция; `Create...` в этот лог не пишет.
## Правка
**Один файл:** `TOOLS/resource-generator/internal/templates/instance.go`, шаблон метода `ModifyPlan`.
Удалить целиком блок от строки
```go
if config.ResourceName.IsNull() || config.ResourceName.IsUnknown() {
return
}
```
до строки
```go
resp.Diagnostics.Append(resources_core.PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices(ctx, r.client, {{.ServiceID}}, config.ResourceName.ValueString(), adoptExistingOnCreate, params, desiredDomain, domainServiceIDs, {{.SupportsSuspendDestroy}})...)
```
включительно. Это весь хвост `ModifyPlan` после блока «Missing required attribute»:
`adoptExistingOnCreate`, resolve refSvc, `params`, `desiredDomain`, `domainServiceIDs`
и сам вызов диагностики.
### Что НЕ трогать
- destroy-guard `if req.Plan.Raw.IsNull() { return }` — **оставить**;
- блок create-only проверок (по `state.ID`) — **оставить**;
- блок «Missing required attribute» — **оставить**;
- `Create` — там вызов `CreateExistingResourceDiagnosticsWithDomainAndServices`
**остаётся**: проверка выполняется на apply, уже после удаления старого инстанса.
### Побочный эффект (принять как норму)
Из plan пропадают проверки ref-параметров / domain / существования по имени. Это
штатное поведение Terraform: на apply `Create` резолвит refSvc (с ошибкой) и делает
проверку существования.
## Проверка
```bash
cd /home/naeel/TF/tf_provider && ./TOOLS/scripts/02_generate_resources_and_docs_v2.sh --profile TOOLS/config/dev
```
```bash
TMP=$(mktemp -d) && cp -R provider "$TMP/provider" && find "$TMP/provider/internal/resources_gen" -maxdepth 1 -type f -name '*.go' -delete && cp generated/dev/go/*.go "$TMP/provider/internal/resources_gen/" && (cd "$TMP/provider" && go build ./...) && echo BUILD_OK && rm -rf "$TMP"
```
Затем проверить сгенерированный код:
- `generated/dev/go/22_vc_nsxt_resource.go`: в `ModifyPlan` вызова
`PlanExistingResourceDiagnosticsWithParamsAndDomainAndServices` больше нет;
- в `Create` вызов `CreateExistingResourceDiagnosticsWithDomainAndServices` остался.
Если после удаления какой-то импорт стал неиспользуемым (`fmt`, `resources_core`) —
проверить сборкой. Обычно `Create` сохраняет те же импорты, отдельная правка флага
`NeedsFmtImport` не требуется.
## Что НЕ делать
- **НЕ собирать и НЕ заливать** провайдер — только правка шаблона + генерация + сборка-проверка.
- Не править сгенерированный код руками.
- Не трогать `provider/internal/resources_core/resource_diagnostics_required.go`.
## Критерий готовности
`BUILD_OK` и в сгенерированном edge `ModifyPlan` нет create-time проверки.
## Обходной путь без правок (если надо убить стенд прямо сейчас)
```bash
terraform untaint nubes_vc_nsxt.edge && terraform destroy
```
@@ -0,0 +1,51 @@
# Промпт для Opus: IaC-развёртывание Штурвала, проблема `modify` и скрытых зависимостей
## Правила ответа (жёстко)
1. НЕ лезь в файлы/репозиторий/сеть. Отвечай ТОЛЬКО по материалу ниже.
2. Отвечай КРАТКО, тезисами, по номерам вопросов. Без простыней.
3. Токены/секреты/креды НЕ нужны — если захочешь, не упоминай и не проси.
4. Если для ответа не хватает данных — прямо пиши «неизвестно», не выдумывай.
5. Не предлагай «ручной ЛК / скрипт / пресеты дефолтного окружения» как решение IaC — это уже отклонено (клиенту нужен полноценный IaC).
## Контекст
Terraform-провайдер для Nubes Cloud. Клиенту нужен IaC: один конфиг + `terraform apply` = вся инфраструктура. Цепочка Штурвала:
```
vcOrg -> create
vcVdc -> create
vcNsxt -> create
vcOrg -> modify (аллокация внешних IP)
vcNsxt -> modify (включить SNAT, указать внешний IP из vcOrg)
k8sShturval -> create
```
Операции строго последовательны.
Факты (подтверждены):
- Провайдер генерируется из YAML-спеков. Схема tf-ресурса строится ТОЛЬКО из операции `create`.
- `vIPConfigure` (array-map-fixed, sub: name/count) есть только в `modify` vc_org (id 207); в `create` (136) его нет.
- `ipSpaceName` (string) есть только в `modify` vc_nsxt (id 111); в `create` (10) его нет.
- `vIPConfigure` — **не накопительный, а replace-семантика** (подтверждено `NOTES/30_analysis/ORG_IP_MODIFIER_TEST_2026-09-22.md`): повторный `modify` с тем же `count` не аккумулирует IP (1→1), работает в обе стороны (вверх/вниз/до 0), `count=0` принимается. Значение задаётся целиком, читается из `state.params`.
- `ipSpaceName` выводится из цепочки `providerVdc -> providerGateway -> ipSpace`, которую пользователь не знает. Сейчас платформа «подкладывает» недостающие параметры при создании пустой орги.
- Допущение платформы: в организации один T0/провайдер-шлюз. Рост числа T0 отложен.
- Платформа в движении: форма ресурсов зависит от новых спеков (ждут, придут сначала в sandbox).
Прецедент (VCD): та же цепочка делается отдельными ресурсами с `depends_on` — `vcd_nsxt_alb_settings` (count + is_active), `vcd_nsxt_alb_edgegateway_service_engine_group` (reserved_virtual_services), `vcd_network_routed_v2`, `vcd_ip_space_custom_quota` (на оргу). Включение/выключение = `count`, inverse = удаление ресурса.
Разница с каноном: у нас нет отдельного API-объекта под модификацию — только операция `modify` над родителем (Read = чтение родителя, Delete = обратный modify, нужна идемпотентность).
## Вопросы
1. `vIPConfigure` уже ведёт себя как replace-состояние (идемпотентно, обе стороны, `count=0` читается из `state.params`). Как это оформить в tf-ресурсе, чтобы Read брал `state.params`, а Delete (inverse) выставлял `count=0` — если отдельного API-объекта нет?
2. Как провайдер должен получать выводимое значение `ipSpaceName` (цепочка providerVdc → providerGateway → ipSpace): data-source, вычисляемое из state родителя, или иное? Где граница «данные vs логика», что хранить в реестре, что выводить из типа/state?
3. Как спроектировать форму ресурсов, чтобы не завязываться на допущение «в организации один T0», и что сломается/что менять, если T0 станет больше одного?
4. Стоит ли ждать новых спеков платформы перед проектированием ресурсов, или форму ресурсов можно зафиксировать уже сейчас так, чтобы она пережила изменение спеков? Что в спеках — блокер, что — нет?
5. Минимально-инвазивный порядок внедрения: что должно прийти от платформы (spec/API) до того, как мы начинаем кодить, а что можем сделать на стороне провайдера уже сейчас?
Отвечай по номерам, кратко.
@@ -0,0 +1,47 @@
# Вопрос: архитектура inverse-отката модификаторов (без чтения файлов)
ЗАПРЕЩЕНО лезть в файлы репозитория. Отвечай только по тексту ниже. Ответ — максимально краткий (тезисы), но исчерпывающий.
## Контекст
Terraform provider для Nubes Cloud. Есть «модификаторы» — отдельные TF-ресурсы, вызывающие операцию `modify` над инстансом по кодам параметров, а не по числовым id. Примеры:
- `vc_org` → модификатор `ip_space`, параметр `vIPConfigure` (array-map-fixed) = `[{"name":"internet-ipv4-v1","count":3}]` — выделение внешних IP.
- `vc_nsxt` → модификатор `network`, параметры `needEnableAVI` (boolean), `ipSpaceName` (string, valueList содержит sentinel `"no-needed"`), `routedNetConfiguration`.
У модификатора есть `delete_strategy`, определяющий что делать при `terraform destroy`:
- `noop_warn` — снять из state, эффект остаётся (предупреждение).
- `error` — запрет удаления (сейчас так на vc_org, из-за чего destroy встаёт).
- `inverse` — при Delete выполнить обратную операцию `modify` с `delete_params` (список `{Code, Value}`).
## Проблема
Хочу, чтобы `destroy` и `apply` были полными и симметричными. При удалении модификатора нужно «откатить» эффект:
1. `needEnableAVI` → `false`.
2. `ipSpaceName` → `"no-needed"`.
3. `vIPConfigure` → `count=0`, имя сохранить (`[{"name":"internet-ipv4-v1","count":0}]`).
Пункты 1-2 — статические константы, текущий механизм `delete_params {Code,Value}` покрывает.
Пункт 3 — динамический: имя берётся из текущего state инстанса, обнуляется только `count`.
Требование: решение должно быть архитектурно чистым и универсальным (привязанным к типам данных из API, `dataType`/`valueList`/`sub_params`), а не хардкодом имён сервисов — чтобы при неглобальных изменениях API перегенерация подхватывала.
## Ключевые факты (уже проверены)
- `count=0` принимается API, несмотря на `minvalue:1`/`integer > 0` в схеме. Идемпотентно.
- `ipSpaceName` sentinel «выключен» = `"no-needed"` (есть в `valueList`).
- `needEnableAVI` — boolean: обратное = `"false"`.
- Типы из API: `needEnableAVI`=`boolean`; `ipSpaceName`=`string`(+`valueList`); `vIPConfigure`=`array-map-fixed` (sub_params: `name`=string, `count`=integer).
## Вопросы (нужны краткие ответы)
1. Как правильно расширить модель delete_params, чтобы поддержать и статичные обратные значения (`false`, `no-needed`), и динамические преобразования (`count→0`)? Оцени вариант «типизированные правила`: `Mode` ∈ {static, zero_count, …}, где static=текущий Value, zero_count=обнулить integer-поле `count` в каждом элементе array-map-fixed, взятом из live state.
2. Универсальнее ли выводить обратные значения ИЗ ТИПА ПАРАМЕТРА (boolean→"false", string+valueList→первый/помеченный sentinel, array-map-fixed→нулевой count в integer-полях), чем задавать их в реестре исключений? Где баланс: что держать в реестре (данные), что выводить из типа (логика)?
3. Нужен ли отдельный маркер «какое поле array-map-fixed обнулять» (сейчас это `count`), или достаточно общего правила «обнулить все integer-поля sub_params»? Риски обоих.
4. Правильный порядок destroy при зависимостях: `edge_net` (SNAT off + ALB off) → `org_ips` (count=0) → `nsxt` → `vdc`. Как Terraform сам выведет порядок из `depends_on`, и где инверсия/откат может конфликтовать с порядком удаления дочерних инстансов?
5. Есть ли подводные камни в самом `inverse`-delete (если дети ещё живы, откат `count=0` на орге может не пройти)? Нужен ли двухфазный подход или достаточно полагаться на порядок?
Формат ответа: пункты пронумерованы под мои вопросы, 1-3 предложения на пункт. Без лишнего.
@@ -0,0 +1,61 @@
# Задача: спроектировать ПРОСТУЮ логику «изменяемости» параметров (CreateOnly vs Modifiable)
## Проблема
Генератор terraform-провайдера строит проверку «Нельзя изменить X» (CreateOnly) на основе
только instance-modify. Из-за этого возникают противоречивые и сломанные ситуации:
`generated/dev/resources_yaml/22_vc_nsxt.yaml`:
- create (id 10), param `needEnableAVI` (id 340) — помечен `is_modifiable: true`;
- instance-modify у `vc_nsxt` НЕТ (modify 111 — это **modifier** `vc_nsxt.network`).
Генератор:
```
ComputeCreateOnly(createParams, instanceModifyParams):
поле считается CreateOnly, если его code нет в instance-modify
```
Следствие: `needEnableAVI` попадает в CreateOnly → генерится жёсткая проверка
«Нельзя изменить need_enable_avi», хотя по YAML параметр `is_modifiable: true`.
Плюс `ConvertParams` вообще **не переносит** `is_modifiable` из ParamSpec в Param —
поле теряется, логика его учесть не может.
## Ключевые файлы (текущая логика)
- `TOOLS/lib/types.go` — `ParamSpec.IsModifiable` (есть, `is_modifiable` сериализуется в YAML)
- `TOOLS/resource-generator/internal/types/types.go` — `Param` (НЕТ поля IsModifiable)
- `TOOLS/resource-generator/internal/loader/loader.go` — `ConvertParams` (не переносит IsModifiable)
- `TOOLS/resource-generator/internal/params/params.go` — `ComputeCreateOnly` (игнорирует is_modifiable и modifier)
- `TOOLS/resource-generator/internal/templates/instance.go` — шаблон, рендерит «Нельзя изменить» из `.CreateOnlyParams`
- YAML: `generated/dev/resources_yaml/22_vc_nsxt.yaml` (modify 111 — `kind: modifier`)
## Существующие понятия операции
В YAML операции бывают видов:
- `kind: instance` (`create` / `modify` / `suspend` / `resume` / `delete`)
- `kind: modifier` (отдельный TF-ресурс, `modify` на родительском инстансе, например `vc_nsxt.network`)
- `kind: subresource`
- `kind: action`
## Цель
Спроектировать **единую, простую и понятную** модель «изменяемости» параметра, чтобы:
1. параметр считался изменяемым, если он изменяем ХОТЯ БЫ через один канал
(instance-modify ИЛИ modifier);
2. «Нельзя изменить» генерировалось ТОЛЬКО для реально create-only параметров;
3. `is_modifiable` из YAML был единственным источником правды (или явно согласован с каналами modify);
4. не было противоречий вида «в YAML is_modifiable:true, а в коде «Нельзя изменить»».
## Вопросы к Opus
1. Какая каноническая модель: вычислять изменяемость по `is_modifiable` (флаг из YAML),
по наличию кода в любом modify (instance + modifier), или по комбинации?
2. Где именно проставлять/вычислять флаг — в yaml-generator (при генерации YAML), или в
resource-generator (при генерации Go)?
3. Как связать modifier-параметры (`vc_nsxt.network`) с parent-инстансом (`vc_nsxt`),
чтобы instance знал, что `needEnableAVI` изменяется через modifier?
4. Минимальный, без legacy-наслоений, набор правил.
Ответ — кратко, с конкретной архитектурой и точками правки (файл + функция).
@@ -0,0 +1,77 @@
> ⛔ **ЛОЖНЫЙ ПУТЬ — ОТМЕНЕНО (пометка 2026-09-24). ТАК ДЕЛАТЬ НЕЛЬЗЯ.**
> Отменённый заход (`kind: modifier` в YAML + реестр в генераторе). Сохранён как история.
> Актуально: `NOTES/40_chat_summaries/CHAT_RESUME_IAC_2026-09-24.md`.
# Спроектировать С НУЛЯ архитектуру/логику «ресурсов-модификаторов» (kind: modifier)
## Цель
Перепроектировать модификаторы целиком, чтобы исключить ВСЕ классы багов, не латать по одному.
Нужна единая, полная модель поведения — без догадок и костылей. Перечислить ВСЕ кейсы.
## Что такое модификатор (текущая фактура)
В YAML (генерируется из API) операции бывают:
- `kind: instance` (create/modify/suspend/resume/delete) — обычный CRUD-ресурс;
- `kind: modifier` + `modifier: <name>` — отдельный TF-ресурс, который вызывает `modify`
на родительском инстансе. Сейчас их два: `vc_org.ip_space`, `vc_nsxt.network`.
Реальные примеры:
- `vc_org` → modifier `ip_space` (modify 207), параметр `vIPConfigure` (array-map-fixed);
- `vc_nsxt` → modifier `network` (modify 111), параметры `needEnableAVI`(bool),
`virtualServicesCount`(int>0), `qosProfile`(string), `ipSpaceName`(string),
`routedNetConfiguration`(map-fixed).
## Текущий механизм (что есть — факты, не догадки)
1. Генератор: `TOOLS/resource-generator/internal/templates/modifier.go`
- Create и Update **идентичны**: оба шлют `modify` с полным набором полей.
- `Delete` — **no-op** (комментарий: «no confirmed inverse payload»).
- Схема: `id` computed, `<service>_id` required, поля Optional (или Required если нет default).
2. `resources_core.CompactParams` — выбрасывает пустые строки из payload.
3. `resources_core.BuildActionID(instanceUID, operation, modifierName)` — константный ID,
не привязан к реальной операции (opUid не сохраняется).
4. `core.RunInstanceOperationUniversalByCode` — резолвит code→id через
`GET /instanceOperations/{opUid}?fields=cfsParams` (fallback на `/default/{opId}`);
отправляет переданные params, затем дозаполняет остальные их live-значением
(guard: пропускает параметр, если нет ни ParamValue, ни DefaultValue).
5. `Read` — через `RefreshResourceState`: читает `state_params` инстанса и
перезаписывает input-поля из них.
## Уже выявленные КЛАССЫ багов (все реально случились)
- **A. Сброс create-поля при modify.** modify со сброшенными (null) параметрами
трактуется бэкендом как reset-to-default: `needEnableAVI` стал false после
create=true. Причина: модификатор шлёт только свои поля, `CompactParams` выкидывает
пустые, бэкенд видит «отсутствующий» и сбрасывает.
- **B. Досылка синтетики.** фикс «досылать всё» слал `"0"` для `integer > 0`
(параметр `virtualServicesCount`), API 400 «Invalid format integer > 0».
- **C. Ложное «Нельзя изменить».** `ComputeCreateOnly` считал `needEnableAVI`
CreateOnly (change-forbidden), хотя в YAML `is_modifiable: true` — потому что
генератор не учитывал modifier-канал и терял `IsModifiable`. (Зафиксировано отдельно.)
- **D. No-op Delete оставляет эффект на платформе.** destroy модификатора убирает
ресурс из state, но выделенные IP / включённый ALB остаются на платформе → drift.
- **E. Повторный apply после taint/replace** снова гонит modify — риск повторной
аллокации (для `ip_space`), идемпотентность не гарантирована.
## Вопросы к Опусу (ответить ПОЛНО, по пунктам, с точными местами правки)
1. **Канон «как сравнить и применить».** Должен ли модификатор перед modify
читать текущее состояние и слать ДЕЛЬТУ (только реально изменившиеся поля),
или ПТЦ полный payload? Как детектить drift в Read?
2. **Досылка незаданных полей (паер-заливы A и B).** Какое каноническое правило:
когда досылать live-значение, когда дефолт, когда пропускать? Как не сломать
`integer > 0` и прочие constraints?
3. **Delete/rollback.** Где искать обратный payload? Как правильно поступить, пока
обратный payload НЕ подтверждён API (no-op допустим? явная ошибка? suspend?).
4. **Idempotency + ID.** Как сделать ID модификатора отражающим фактическую операцию
(opUid?) и как предотвратить двойную аллокацию при replace/повторном apply?
5. **Связь с родителем.** Должен ли модификатор использовать `<service>_id` как ссылку
на родителя (depends_on / borrow state), и как читать UUID родителя?
6. **Create vs Update.** Допустимо ли иметь их идентичными, или нужен строго Update-семантик
(нет create, только apply-по-десяти)?
7. **Полный перечень кейсов.** Перечислить ВСЕ edge-кейсы, которые надо покрыть:
create родителя → modifier; remove modifier; replace; partial params; unknown/absent.
Ответ — архитектурный документ (краткий, структурированный), с конкретными файлами
и функциями. НЕ код-ревью, а ПРОЕКТ.
@@ -0,0 +1,68 @@
> ⛔ **ЛОЖНЫЙ ПУТЬ — ОТМЕНЕНО (пометка 2026-09-24). ТАК ДЕЛАТЬ НЕЛЬЗЯ.**
> Отменённый заход (`kind: modifier` в YAML + реестр в генераторе). Сохранён как история.
> Актуально: `NOTES/40_chat_summaries/CHAT_RESUME_IAC_2026-09-24.md`.
# Уточнения к архитектуре модификаторов — расхождения с фактическим кодом
Не принимаю предыдущие ответы за истину. Сверка с реальным кодом выявила расхождения.
Прошу пересмотреть/уточнить.
## Факт №1: `OperationSpec` — это алиас `lib.OperationSpec`, не локальный тип
В `TOOLS/resource-generator/internal/types/types.go`:
```go
type OperationSpec = lib.OperationSpec
type ParamSpec = lib.ParamSpec
```
Канонический YAML-контракт лежит в `TOOLS/lib/types.go` (пакет `tf-tools/lib`),
где уже определены `OperationSpec` (Name/ID/Kind/Action/Modifier/Subresource/Man/Params)
и `ParamSpec`.
Ошибка в прошлом ответе: «добавить в types.go:39» — НЕ указано, что это `lib`.
Новые поля `delete_strategy` / `idempotency` / `delete_params` должны быть
в `TOOLS/lib/types.go`, иначе yaml-generator (который тоже импортирует lib)
и resource-generator разойдутся.
Вопрос: подтверждаешь, что новый контракт добавляется в `lib/types.go\` (OperationSpec),
а `resource-generator` получает его через алиас? Или нужно отдельное
resource-generator-специфичное поле (не в lib, а в GenModifier)? Где граница:
что в lib, что локально в GenModifier?
## Факт №2: `normalizeUniversalValueV6` — приватная, живёт в core, принимает core-структуру
Прошлый ответ: «сравнивать desired vs current после normalizeUniversalValueV6».
Но:
- `normalizeUniversalValueV6(val string, param universalCfsParam)` — **приватная** (маленькая буква);
- принимает `universalCfsParam` (структуру пакета `core`);
- сравнение pre-check «desired == current» предполагалось в `resources_core`
(там `RunOperationByCodeWithTimeout`) или в шаблоне модификатора.
Вопрос: ГДЕ правильно делать pre-check и нормализованное сравнение?
- вариант A: в `core` (там доступны и cfsParams, и normalize), экспортировать сравнение;
- вариант B: в `resources_core` — тогда нужен экспортированный компаратор
(`JSONStringsEquivalent` там уже есть), но `universalCfsParam` недоступен;
- вариант C: сравнение только через `JSONStringsEquivalent` по JSON-строкам,
без `normalizeUniversalValueV6`? (но тогда `" 5"` vs `"5"`, `true` vs `1` дадут ложный diff).
Как совместить нормализацию типов (bool→"true", int→"5") с местом, где сравнение
происходит? Конкретный файл+функция.
## Дополнительные сомнения (прошу подтвердить/опровергнуть)
1. **Idempotency pre-check и «полный payload» конфликтуют?** Если desired==current → skip.
Но при этом «полный payload» не шлётся вообще (skip). Это согласуется? Или при
расхождении одного поля всё равно слать полный payload (и это нормализует всё)?
2. **`delete_strategy: inverse` + параметр, у которого НЕЛЬЗЯ обнулить** (напр.
`virtualServicesCount` integer>0): прошлый ответ — «inverse недопустим, fail-fast».
Но что если inverse-стратегия нужна только для ЧАСТИ полей, а не для всех?
Т.е. `delete_params` покрывает `needEnableAVI:false`, а `virtualServicesCount`
просто остаётся как есть. Допустимо ли «частичный inverse» (обратить только
обратимое, остальное не трогать)? Или inverse обязан покрывать все поля?
3. **`noop_warn` (дефолт) — всегда ли безопасен?** Удаление модификатора из state
при оставшемся эффекте на платформе — это drift. Допустимо ли вообще иметь
`noop_warn` как ДЕФОЛТ, или для необратимых (ip_space) правильнее дефолт `error`
(запретить destroy, пока не разберутся)? Что каноничнее?
Ответ — кратко, по пунктам.
@@ -0,0 +1,50 @@
> ✅ **АКТУАЛЬНОЕ НАПРАВЛЕНИЕ (пометка 2026-09-24), НО ЕЩЁ НЕ РЕАЛИЗОВАНО.**
> Требования отсюда действительны: YAML — чистая выгрузка API без доменных меток; модификаторы — НЕ ветка
> универсального генератора. Ответ Opus по нему см. в `NOTES/30_analysis/OPUS_ANSWER_IAC_SHTURVAL_MODIFY_2026-09-23.md`.
> Состояние и развилка: `NOTES/40_chat_summaries/CHAT_RESUME_IAC_2026-09-24.md`.
# Глобальная архитектура модификаторов: как сделать их НЕ инвазивным дополнением
ЗАПРЕЩЕНО лезть в файлы репозитория. Отвечай только по тексту. Формат: тезисы, кратко, по пунктам моего вопроса. Без лишнего.
## Контекст
Terraform provider для Nubes Cloud. Цепочка кодогенерации:
1. `01_generate_yamls` — идёт по API, по каждому облачному сервису тянет операции и параметры, пишет универсальный YAML (`resources_yaml/<id>_<svc>.yaml`).
2. `02_generate_resources` — по этому YAML генерирует Go-ресурсы провайдера (`<id>_<svc>_resource.go`).
Обычные ресурсы (`nubes_vc_nsxt`, `nubes_vc_vdc` и т.д.) — это операции `create`/`delete`/`suspend`/`resume`/`reconcile` над инстансом. Их apply/destroy давно стабильны и оттестированы.
## Что такое «модификатор» (доменная суть)
Некоторые операции `modify` сервиса — это не «изменить инстанс», а **отложенный дочерний шаг** цепочки, который нельзя мешать с create инстанса:
- `vc_org` → `modify` с параметром `vIPConfigure=[{"name":...,"count":N}]` — выделение внешних IP организации.
- `vc_nsxt` → `modify` с `needEnableAVI`, `ipSpaceName`, `routedNetConfiguration` — настройка ALB/SNAT уже созданного Edge.
Такой `modify` семантически НЕ принадлежит lifecycle самого инстанса: это отдельный TF-ресурс, который должен создаваться/удаляться независимо от `create`/`delete` родителя.
## Проблема (как сделано сейчас — неправильно)
Сейчас «модификаторность» вплетена в универсальную генерацию:
- реестр `serviceSpecificModifiers` зашит в исходник yaml-generator и **помечает** операцию `modify` как `kind: modifier` + пишет в YAML `delete_strategy`, `delete_params` и т.п.
- Это ломает главный принцип: YAML должен быть чистой универсальной выгрузкой из API, а обычные ресурсы — не зависеть ни от какого реестра.
Требования:
1. YAML — универсальная выгрузка ВСЕГО из API, без доменных меток (`kind: modifier`, `delete_strategy`).
2. Ресурсы облачных сервисов НЕ должны зависеть от модификаторов. Если модификаторов нет — поведение идентично прежнему (до их внедрения).
3. Модификаторы — чистое ДОПОЛНЕНИЕ: отдельная сущность, отдельный ресурс, со своей семантикой (inverse-откат при destroy, idempotency), которая НЕ просачивается в базовую генерацию.
4. При полном `destroy` должен быть корректный обратный откат: ALB off, SNAT `no-needed`, IP `count=0` — при этом симметричный `apply` возрождает всё.
## Вопросы (ответь по пунктам)
1. **Правильное место доменной семантики модификатора.** Где её хранить, чтобы она была «данными-наложением», а не веткой в универсальном генераторе? Варианты: (а) отдельный конфиг-файл данных (`modifiers.yaml`), который второй проход накладывает на базовый YAML, порождая ОТДЕЛЬНЫЕ YAML-записи модификаторов, не трогая базовые; (б) отдельный `kind` в самих YAML без доменных меток; (в) иное. Обоснуй.
2. **Разделение «модификатор» vs «обычный modify».** Как архитектурно отделить modify-как-модификатор от modify-инстанса, НЕ меняя универсальную выгрузку? Как гарантировать, что при отсутствии модификаторов обычный modify-поток ресурса вообще не затрагивается?
3. **Как структурировать inverse-откат**, чтобы он был: (а) генерализуемым (по типам: boolean→"false", string+valueList→off_value sentinel, array-map-fixed→zero integer-полей), (б) идемпотентным (не дёргать run, если live уже целевое), (в) не влиял на обычные ресурсы. Нужна ли отдельная модель `delete_rule` у модификатора.
4. **Порядок destroy** при цепочке модификаторов, зависящих от обычных ресурсов и друг от друга (`SNAT-модификатор → IP-модификатор → edge → vdc`). Как выразить зависимость модификатора от ресурса так, чтобы Terraform сам вывел обратный порядок, не завязываясь на хрупкий `depends_on`?
5. **Минимально-инвазивная миграция.** Как перейти от текущего (модификаторы «вросли» в базовую генерацию) к целевой (модификаторы — наложение) без регресса уже стабильных обычных ресурсов? Что трогать НЕЛЬЗЯ.
Ответь кратко, по номерам, 2-4 предложения на пункт.
@@ -0,0 +1,45 @@
> ⛔ **ЛОЖНЫЙ ПУТЬ — ОТМЕНЕНО (пометка 2026-09-24). ТАК ДЕЛАТЬ НЕЛЬЗЯ.**
> Баг относится к отменённому заходу (`kind: modifier` в YAML + реестр в генераторе).
> Сохранён как история. Актуально: `NOTES/40_chat_summaries/CHAT_RESUME_IAC_2026-09-24.md`.
# Баг: modify-модификатор сбрасывает create-поля в дефолт (needEnableAVI true→false)
## Симптом
`nubes_vc_nsxt_network` (modifier vc_nsxt.network, modify 111) после create Edge с `needEnableAVI=true`, `virtualServicesCount=3` сбрасывает `needEnableAVI` на платформе обратно в `false`.
## Подтверждено по API (cfsParams операций)
Create Edge (op `0c169353`):
- 340 needEnableAVI = **true**
- 341 virtualServicesCount = **3**
Modify (SNAT-модификатор, op `d14a149e`):
- 368 needEnableAVI = **null**
- 369 virtualServicesCount = **null**
- 856 qosProfile = **null**
- 372 ipSpaceName = internet-ipv4-v1
- 1112 routedNetConfiguration = {...}
Итоговый state.params Edge: `needEnableAVI = false`.
## Гипотеза
Модификатор строится через `resources_core.CompactParams`, который выбрасывает пустые `Optional`-поля. Бэкенд для `modify` трактует **пропущенный/null** параметр как «сбросить в дефолт» (false/0), а не «оставить как есть». Итог: modify с частичным payload затирает create-поля.
## Файлы
- `provider/internal/core/client.go` — `RunInstanceOperationUniversalByCode` (отправка params), `normalizeUniversalValueV6`
- `provider/internal/resources_core/crud.go` — `RunOperationByCodeWithTimeout`, `CompactParams`
- генератор: `TOOLS/resource-generator/internal/templates/modifier.go`, `internal/writers/writers.go` (WriteModifierResource)
- сгенерированное: `generated/dev/go/22_vc_nsxt_network_modifier.go`
- YAML: `generated/dev/resources_yaml/22_vc_nsxt.yaml` (modify 111, поля is_modifiable)
## Задание
Определить каноническое поведение:
1. Должен ли modify слать **все** параметры операции (полный payload, включая необязательные с их текущими значениями), или допустимо слать только переданные?
2. Где правильнее чинить: в генераторе (шаблоне modifier), в `CompactParams`, или в `RunInstanceOperationUniversalByCode` (досылать дефолты/текущие значения незаданных полей)?
3. Есть ли риск, что «досылать дефолты» сломает другие модификаторы (напр. vc_org.ip_space)?
Ответ кратко, тезисно, с указанием конкретной строки/места фикса.
@@ -0,0 +1,43 @@
> ⛔ **ЛОЖНЫЙ ПУТЬ — ОТМЕНЕНО (пометка 2026-09-24). ТАК ДЕЛАТЬ НЕЛЬЗЯ.**
> Ревью плана отменённого захода (`kind: modifier` в YAML + реестр в генераторе).
> Сохранён как история. Актуально: `NOTES/40_chat_summaries/CHAT_RESUME_IAC_2026-09-24.md`.
# Ревью плана реализации: редизайн модификаторов
Прошу отревьюить план `PLAN_modifier_redesign.md` (10 шагов). Это проект к реализации,
не код. Вызовись: найди дыры, пропущенные кейсы, ошибки в порядке шагов, нестыковки.
## Контекст решения (уже согласовано, НЕ пересматривать)
- Модификатор = декларативная проекция полей родителя, единый `reconcile()` (Create≡Update).
- Полный payload (не дельта), досылка: задан→значение, иначе live→default→skip.
- `delete_strategy`: noop_warn | inverse | error (дефолт noop_warn), `idempotency`: none | check_before_run.
- Pre-check `desired==current` в `core` (не в resources_core, не в шаблоне), по живому `state_params`.
- `is_modifiable` — единственный сигнал изменяемости (фикс CreateOnly уже есть).
## Ключевые файлы-факты (сверены с кодом)
- `TOOLS/lib/types.go` — `OperationSpec`/`ParamSpec` (алиасы в обоих генераторах).
- `TOOLS/yaml-generator/main.go` — `serviceSpecificModifiers` (реестр исключений, источник канона).
- `TOOLS/resource-generator/internal/loader/loader.go` — ветка `kind==modifier`, `ValidateSpec`.
- `TOOLS/resource-generator/internal/templates/modifier.go` — шаблон.
- `provider/internal/resources_core/crud.go` — `RunOperationByCodeWithTimeout`.
- `provider/internal/resources_core/json_normalize.go` — `JSONStringsEquivalent` (импорт в core = цикл).
- `provider/internal/core/operation_run_bycode.go` — клиентский запуск.
## Вопросы к ревью (ответить кратко, по пунктам)
1. Порядок шагов 1–10 корректен? Где есть скрытая зависимость, которую я пропустил?
2. Шаг 5 (вынос JSON-эквивалентности в `core/jsonutil`) — правильный путь снять цикл
импорта, или есть чище (напр. оставить `JSONStringsEquivalent` в resources_core и
передавать нормализованные строки в core уже готовыми)?
3. Шаг 6 — сигнатура `modifierDesiredEqualsCurrent(desired map[string]string, cfsParams []universalCfsParam) bool`
корректна? Хватает ли данных для сравнения всех типов (bool/int/string/map-fixed/array-map-fixed)?
4. Шаг 4.4 Delete=inverse — как именно слать modify: `delete_params` + досылка live остальных
(полный payload) — это правильно, или есть подводный камень?
5. Шаг 8 — расширение реестра `serviceSpecificModifiers` до структуры: верный источник?
Или `delete_strategy`/`idempotency` правильнее держать отдельным реестром (не трогая тип map)?
6. Пропущен ли какой-то кейс из 16 (13 + taint/replace/partial/unknown)?
7. Есть ли риск сломать instance-ресурсы (не модификаторы) любым из шагов 1–8?
Ответ — тезисно, с указанием конкретного шага и что в нём поправить.
@@ -0,0 +1,84 @@
> ⛔ **ЛОЖНЫЙ ПУТЬ — ОТМЕНЕНО (пометка 2026-09-24). ТАК ДЕЛАТЬ НЕЛЬЗЯ.**
> Ревью реализации отменённого захода (`kind: modifier` в YAML + реестр в генераторе,
> досылка modify-params через `paramValue`). Сохранён как история.
> Актуально: `NOTES/40_chat_summaries/CHAT_RESUME_IAC_2026-09-24.md`.
# Ревью: модификаторы vc_nsxt / vc_org + досылка modify-params (Terraform Provider Nubes)
Ты — ревьюер. Ничего не правь. Прочитай и выдай:
1) подтверждение/опровержение каждого утверждения ниже;
2) список багов/рисков, которые я НЕ заметил;
3) список проблем, которые я заметил ошибочно (ложные тревоги);
4) чёткую рекомендацию по каждому корректному фиксу (минимальную, без scope creep).
## Контекст системы
Go Terraform provider `terraform-provider-nubes` (plugin-framework), сервис Nubes Cloud.
Генератор ресурсов: `TOOLS/resource-generator` (шаблон `instance.go`, `modifier.go`).
Ядро: `provider/internal/core/` (HTTP + операции), `provider/internal/resources_core/` (обёртки).
Сервисы, о которых речь:
- `vc_nsxt` (serviceId 22). Операции: create (10), delete (25), **modify (111, kind: modifier, modifier: network)**, reconcile. У resource НЕТ instance-modify.
- `vc_org` (serviceId 19). Модификатор `ip_space` (modify 207).
Модификатор = отдельный TF resource (`nubes_vc_nsxt_network`, `nubes_vc_org_ip_space`), который вызывает операцию `modify` с параметрами.
## Цель (FullPipe) — что должно работать
1. Создать VDC (vc_vdc).
2. Создать Edge (vc_nsxt) с ALB (`needEnableAVI=true`, `virtualServicesCount=3`).
3. Выделить IP организации (vc_org → modifier ip_space, `vIPConfigure`).
4. Применить SNAT для Edge (vc_nsxt → modifier network, `ipSpaceName` + `routedNetConfiguration`).
## Что УЖЕ сделано (факты, проверь корректность)
### Факт 1. `resource "nubes_vc_nsxt"` Update — no-op
Шаблон `instance.go` генерирует `hasServiceParamChanges := false`, а цикл по `.ModifyParams` пуст (у vc_nsxt нет instance-modify). Поэтому `Update` всегда уходит в `if !hasServiceParamChanges { ...; return }` и НЕ вызывает `modify` (111). Изменение ALB/VS/qos через resource невозможно. Изменения Edge идут ТОЛЬКО через модификатор `nubes_vc_nsxt_network`.
### Факт 2. Досылка незаданных modify-params (мой свежий фикс, коммиты a011358)
Раньше незаданные params операции `modify` досылались значением `paramValue` из `GET /instanceOperations/{opUid}?fields=cfsParams`. Это ОШИБОЧНО: `paramValue` — дефолт ФОРМЫ операции, а не состояние инстанса. Для `needEnableAVI` там `"false"`, хотя live-значение инстанса `true` (подтверждается HAR/edge_.har и HAR/ipSpace0.har). Из-за этого каждый `modify` через модификатор сбрасывал ALB в false.
Фикс: в `operation_cfs.go` добавлены `instanceLiveParams()` (читает live из `GET /instances/{uid}` → `state.params`) и `lookupLiveParam(live, cfsParam)`. В `runInstanceOperationByCode` и `RunInstanceOperationUniversalWithDefaults` приоритет теперь: **live state.params → paramValue → defaultValue**.
### Факт 3. `ShouldRemoveFromState` (коммит 94c4c44)
Раньше вызывал валидирующий `GetInstanceState`, который на статусе `deleted` кидал `instanceDeletedError` — и `Read` модификатора падал с "экземпляр … удалён" вместо тихого удаления из state. Переписан на `GetInstanceStateRaw` + различение 404/deleted (remove=true) vs сеть/5xx/403 (нужно `false, err`).
## ОШИБКА, которую наблюдаю СЕЙЧАС (главное)
`terraform apply` падает:
```
Error: Provider returned invalid result object after apply
After the apply operation, the provider still indicated an unknown value for
nubes_vc_nsxt.edge.qos_profile. All values must be known after apply...
```
`qos_profile` у resource `nubes_vc_nsxt` = `Optional+Computed` БЕЗ Default (`ShouldBeOptionalComputed` → true, потому что param qosProfile: not required, RefSvcId=0, Default=""). В конфиге не задаётся → в плане unknown. А `Update` (`hasServiceParamChanges=false` → ранний return) копирует только `State*`/`Vault*` outputs, но НЕ вызывает `RefreshResourceState`, поэтому `qos_profile` остаётся unknown.
## МОИ ДИАГНОЗЫ (проверь каждый, а не только текущий)
### Диагноз A (текущая ошибка)
Ранний return в `Update` (шаблон instance.go) не схлопывает unknown→null read-back-computed поля. Нужно в ветке `!hasServiceParamChanges` вызывать тот же `RefreshResourceState`, а не копировать `State*`/`Vault*` вручную.
### Диагноз B (вылезет после A)
Тот же ранний return оставляет unknown для `need_enable_avi` и `virtual_services_count`, если их убрать из `edge.tf` (а их и должны убрать, раз ALB перенесён в модификатор). Один корень с A.
### Диагноз C (дублирование конфига — НЕ починен)
`edge.tf` ДО СИХ ПОР задаёт `need_enable_avi` и `virtual_services_count` (create), а `edge_network.tf` — те же значения (modifier). Это двойное задание одного и того же → возможен дрейф. Нужно определить: где канонически задавать ALB?
### Диагноз D (ловушка destroy/delete)
Модификатор имеет `delete_strategy: inverse`, override `needEnableAVI="false"`. При `terraform destroy` ALB выключится. Повторный `apply` через `resource "nubes_vc_nsxt"` (no-op Update) НЕ включит обратно, а включит только модификатор второй apply-волной. Нужно проверить порядок зависимостей.
### Диагноз E
`FetchInstanceOutputs` глотает любую API-ошибку (5xx/404) и возвращает пустые outputs без diagnostic → молчаливый дрейф. Нужен warning.
## Конкретные вопросы
1. Подтверди/опровергни Диагноз A как корень текущей ошибки.
2. Есть ли проблема в моём фиксе досылки (Факт 2)? В частности:
- верно ли, что `state.params` — единственный достоверный источник live?
- не сломает ли `lookupLiveParam` (по Code/SvcOperationCfsParam/Name/Label) какие-то кейсы, где имя в state.params отличается регистром/форматом от этих ключей?
- не создаёт ли `instanceLiveParams` лишний сетевой вызов на каждый modify (перф)?
3. Верна ли трактовка Факт 1 (resource Update — no-op)? Или правильнее ДОБАВИТЬ instance-modify в генератор?
4. Какое каноническое место для `need_enable_avi`/`virtual_services_count`/`qos_profile`: create (edge.tf) или modifier (edge_network.tf)? Что делать с текущим дублированием?
5. Что ещё я упустил в цепочке create→modify→read→destroy?
@@ -0,0 +1,33 @@
> ⛔ **ЛОЖНЫЙ ПУТЬ — ОТМЕНЕНО (пометка 2026-09-24). ТАК ДЕЛАТЬ НЕЛЬЗЯ.**
> Код-ревью отменённого захода (`kind: modifier` в YAML + реестр в генераторе).
> Сохранён как история. Актуально: `NOTES/40_chat_summaries/CHAT_RESUME_IAC_2026-09-24.md`.
# Код-ревью: модификаторы (kind: modifier) в универсальном провайдере
## Контекст
terraform-provider-nubes (universal). Операции с `kind: modifier` генерируются как отдельные TF-ресурсы и запускают операцию `modify`, передавая параметры **по коду** (`vIPConfigure`, `needEnableAVI`...), а не по числовому id.
Актуальные модификаторы:
- `nubes_vc_org_ip_space` (vc_org.ip_space, modify 207) — выделение внешних IP (`vIPConfigure`).
- `nubes_vc_nsxt_network` (vc_nsxt.network, modify 111) — сеть/SNAT Edge.
## Ключевые файлы
- генератор: `TOOLS/resource-generator/internal/templates/modifier.go`, `internal/loader/loader.go` (LoadSpecs → GenModifier), `internal/writers/writers.go` (WriteModifierResource)
- рантайм: `provider/internal/resources_core/crud.go` (RunOperationByCodeWithTimeout)
- клиент: `provider/internal/core/client.go` (RunInstanceOperationUniversalByCode)
- сгенерированное: `generated/dev/go/19_vc_org_ip_space_modifier.go`, `22_vc_nsxt_network_modifier.go`, `registry.go`
## Известная проблема (уже диагностирована — НЕ ревьюить)
`GET /instanceOperations/{opUid}?fields=cfsParams` падает 500 (`getResourceRealmConfig` Struct→string) на проблемных инстансах. Fallback на `/instanceOperations/default/{opId}` планируется отдельно.
## Задание — короткий код-ревью
1. Корректность жизненного цикла modifier-ресурса: Create/Update/Read/Delete, идемпотентность, refresh из API.
2. Реального Delete нет (destroy не откатывает операцию) — это ожидаемо? Подводные камни при повторном apply.
3. Риски передачи параметров по коду (code → id) в `RunInstanceOperationUniversalByCode`.
4. ТОП-3 самых критичных замечания именно по модификаторам.
Ответ — кратко, тезисно, без кода-простыней.
@@ -0,0 +1,41 @@
# Промпт для Opus 4.8: код-ревью и оценка архитектуры (3 задачи roadmap)
Дата: 2026-09-22 | Статус: для отправки
```text
Роль: ревьюер архитектуры Go-провайдера Terraform.
ПРАВИЛА:
- Файлы НЕ открывай, в репозиторий не лезь — отвечай только по контексту ниже.
- Анализируй ТОЛЬКО 3 указанные задачи, не весь проект.
- Ответ максимально сжатый: только выводы/риски/рекомендации. Без вводных, без «рассмотрим», без повторов. Списки или таблица. Риск помечай 🔴/🟡/🟢.
- Если для ответа не хватает факта — пиши «НЕДОСТАТОЧНО ДАННЫХ: …», не выдумывай.
КОНТЕКСТ (достаточен, файлы не нужны):
- terraform-provider-nubes, Go, terraform-plugin-framework v1.8.0. Ресурсы ГЕНЕРИРУЮТСЯ из YAML-спек сервисов (не рукописные).
- Один сервис → один ресурс инстанса nubes_<service> (CRUD). В YAML: operations (create/modify/suspend/…), params с type (bool/int64/string/map-fixed/array-map-fixed), required, default, is_modifiable, ref_svc_id.
- Schema: param → Required (если required, без default, не refSvc); Optional; Computed+Default (если default); Optional+Computed (если параметр читается обратно из state_params инстанса, или это JSON).
- Create: резолвит refSvc-параметры (принимают display name ИЛИ UUID → uid в API), вызывает create-op, затем читает state обратно.
- Update: если изменились modify-параметры → вызывает modify-op с ними. ModifyPlan запрещает менять create-only атрибуты (ошибка).
- Read: читает state_params инстанса обратно в input-поля (drift), и выставляет computed-мапы: state_params, state_out, state_params_flat, state_out_flat + vault_*.
- Delete: suspend / delete / state_only — в зависимости от наличия suspend-op у сервиса.
- Межресурсные зависимости: пользователь в .tf ссылается на атрибуты других ресурсов (напр. nubes_vc_vdc.vdc.id). ref_svc_id валидирует значение по инстансам целевого сервиса и резолвит в uid.
- Data sources НЕ генерируются (только ресурсы).
- map-fixed → SingleNestedAttribute (HCL: `x = { … }`); array-map-fixed → StringAttribute (JSON-строка).
- Сервисы: vc_org (19), vc_vdc (21), vc_nsxt (22). Спека k8s_shturval уже есть.
УЖЕ СДЕЛАНО: vcVdc create, vcNsxt create — работают (v2.0.8).
АНАЛИЗИРОВАТЬ (только это):
1. vcOrg modify — аллокация внешних IP в организацию ПО МЕРЕ НЕОБХОДИМОСТИ (динамически, число заранее не фиксировано).
2. vcNsxt modify — включить SNAT и указать внешний IP, взятый из уже аллоцированного пула vcOrg.
3. k8sShturval create.
ВОПРОСЫ (ответь по пунктам, кратко):
1. Покрывает ли текущая модель эти 3 задачи, или для какой-то нужна новая абстракция (data source / action / subresource)? По каждой задаче — вердикт.
2. vcOrg IP-аллокация: как моделировать пул внешних IP, растущий по мере необходимости — (а) атрибут-массив на nubes_vc_org, (б) отдельный ресурс/подресурс на каждый IP? Что лучше согласуется с текущей архитектурой и почему.
3. vcNsxt: как передать «внешний IP из vcOrg»? Сравни: (а) refSvc-параметр, (б) computed-атрибут vcOrg + ссылка nubes_vc_org.<name>.<attr>, (в) data source. Учти ограничение: refSvc ссылается на инстанс/uid, но не на конкретный элемент списка.
4. Риски текущего кода именно для этих потоков: update/modify с массивными параметрами; create-only guard; read-back; порядок плана между зависимыми ресурсами.
5. k8sShturval create: что критично проверить (refSvc к vdc/org, долгий create, типы параметров)?
6. Итог: 3–5 конкретных рекомендаций по приоритету — что добавить/изменить в генераторе или ресурсах.
```
@@ -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`.
@@ -0,0 +1,45 @@
# Проверка решения бага Dev-генератора
Ты выполняешь короткий read-only review. Ничего не меняй, не запускай генерацию,
не собирай и не публикуй провайдер.
## Задача
Проверь, правильно ли диагностирован баг и правильно ли предложено решение:
1. `create.jsonEnv` может быть nested (`sub_params`), а `modify.jsonEnv` — без
`sub_params`.
2. `Merge` формирует каноническую схему из параметров операций.
3. `AlignParamTypes` выравнивает типы, но не переносит `HasSubParams/SubParams`,
если у operation-параметра `HasSubParams` изначально false.
4. Шаблон `Update` поэтому генерирует scalar-вызовы для поля, которое в модели
является nested-структурой.
5. Универсальное решение — нормализовать каждый набор operation params
относительно канонической `SchemaParams`, рекурсивно наследуя структурные
свойства, без условий по стенду или сервису.
## Прочитать только эти файлы
1. `TOOLS/resource-generator/internal/params/params.go`
2. `TOOLS/resource-generator/internal/loader/loader.go`
3. `TOOLS/resource-generator/internal/helpers/helpers.go` — только функции
`IsNested` и связанные с nested-моделями
4. `TOOLS/resource-generator/internal/templates/instance.go` — только участки
`Update` и проверки `IsNested`
5. `TOOLS/resource-generator/internal/types/types.go`
6. `generated/dev/resources_yaml/95_nodejs.yaml` — только `jsonEnv` в create и modify
7. `generated/dev/go/95_nodejs_resource.go` — только модель `JsonEnv` и `Update`
Не изучай остальные сервисы, стенды, историю проекта или API вне этих файлов.
## Формат ответа
Ответь максимум в 5 коротких пунктах:
- **Вердикт:** прав / частично прав / неправ.
- **Доказательство:** одна конкретная цепочка от YAML до ошибочного Go-кода.
- **Решение:** корректно ли выравнивать operation params по канонической схеме.
- **Риск:** один главный риск предлагаемого решения.
- **Итог:** что именно нужно изменить или что менять не следует.
Не предлагай реализацию, diff, рефакторинг или дополнительные исследования.
@@ -280,7 +280,7 @@ CSS скрывает обе боковые панели mkdocs:
2. **TOOLS/docs-generator/internal/writers/writers.go** — ВСЯ генерация .md (~1000 строк, ключевой файл)
3. **TOOLS/docs-generator/main.go** — CLI, флаги, оркестрация
4. **TOOLS/scripts/05_generate_docs_llm.py** — LLM-обогащение, SYSTEM_PROMPT
5. **docs/LLM_DOCS_GENERATION.md** — архитектурная документация
5. **NOTES/50_process/LLM_DOCS_GENERATION.md** — архитектурная документация
6. **docs/30_registry/assets/extra.css** — CSS-стили
7. **docs/30_registry/javascripts/fix-slash.js** — JS (trailing slash fix)
@@ -43,6 +43,25 @@
- `universal_rebuild/tools/gen/main.go`
- Читает YAML и генерирует ресурсы + `registry.go`.
### 1.6 Отдельные modifier-ресурсы
Для parent-level операций `modify`, которые должны выполняться отдельным шагом Terraform-цепочки, используется `kind: modifier`.
Пример:
```yaml
- name: modify
kind: modifier
action: modify
modifier: ip_space
params: []
```
Такой блок не попадает в обычный instance CRUD. Go-генератор создаёт отдельный ресурс с именем `nubes_<service>_<modifier>`. Ресурс принимает ID родительского инстанса и параметры операции, выполняет parent `modify` при Create/Update и читает актуальные значения из `state_params` при Read.
Для `vcOrg` используется modifier `ip_space` с параметром `vIPConfigure`; для `vcNsxt` используется modifier `network` с параметрами операции сетевой настройки. Nested API-параметры modifier-ресурсов передаются как JSON-строки, поэтому их Terraform-значения должны быть валидным JSON.
Удаление modifier пока является no-op: подтверждённого обратного payload для отмены выделенных IP или SNAT нет. Операции удаления родительского сервиса не являются rollback и намеренно не вызываются.
---
## 2) Как получить параметры сервиса (без instanceUid)
@@ -0,0 +1,63 @@
# Отчет о проблеме: Ошибка 500 при получении cfsParams для vc_vdc и план исправления
## 1. Проблема
При создании виртуального дата-центра `nubes_vc_vdc` (сервис 21 `vc_vdc`, операция создания 9) Terraform завершается с ошибкой клиента:
```text
не удалось получить детали операции: ошибка API 500: Invalid call of the function [getResourceRealmConfig], first Argument [resourceRealm] is of invalid type, Cannot cast Object type [Struct] to a value of type [string]: the function is located at [/app/api/v1/resources/instance_operation_cfs_param.cfc]
```
## 2. Анализ причины
1. **Место падения в провайдере**: `provider/internal/core/client.go` (строка 272 в `createInstanceWithContext`):
```go
opDetailsResp, _, err := c.doRequest(ctx, "GET", fmt.Sprintf("/instanceOperations/%s?fields=cfsParams", opUid), nil)
if err != nil {
return "", fmt.Errorf("не удалось получить детали операции: %w", err)
}
```
2. **Поведение бэкенда Nubes (Lucee/ColdFusion)**:
- В файле `/app/api/v1/resources/instance_operation_cfs_param.cfc` при обработке запроса `?fields=cfsParams` для операции 9 вызывается функция `getResourceRealmConfig(resourceRealm)`.
- Для сервиса `vc_vdc` поле `resourceRealm` в БД DEV-окружения хранится как комплексный объект (`Struct`), а не скалярная строка (`string`).
- При попытке приведения типа `Struct -> string` бэкенд падает с HTTP 500.
3. **Сравнение с веб-интерфейсом (HAR/vdc.har)**:
- В официальном веб-интерфейсе ЛК запрос `GET /instanceOperations/{opUid}?fields=cfsParams` **вообще не выполняется**.
- Браузер выполняет строго следующий флоу:
1. `POST /instanceOperations` -> получает `instanceOperationUid`
2. `POST /instanceOperationCfsParams` -> отправляет каждое значение CFS-параметра
3. `GET /instanceOperations/{opUid}/validate-cfs` -> валидация бэкендом
4. `POST /instanceOperations/{opUid}/run` -> запуск операции в оркестраторе
5. Polling `GET /instanceOperations/{opUid}` -> ожидание статуса завершения
4. **Зачем провайдер делает шаг 2**:
- Шаг 2 в провайдере использовался исключительно для вызова `resolveRefSvcParamValues(ctx, opDetails.InstanceOperation.CfsParams, params)` — чтобы узнать `refSvcId` параметров и попробовать отрезолвить имена в UUID.
- Для ресурса `nubes_vc_vdc` параметр `organization_uid` (CFS param 30, refSvc 19) **уже гарантированно отрезолвлен в UUID** до создания операции (на этапе `ModifyPlan` и в начале `Create`).
- Остальные параметры `vc_vdc` (провайдер сети, профиль Provider VDC, CPU, RAM, резервирование, storage_config) являются скалярами/числами/JSON-строками и не содержат `refSvcId`.
- Соответственно, данные запроса `?fields=cfsParams` для `vc_vdc` фактически не требуются.
## 3. План изменений в провайдере (что будем менять)
### Целевой файл: `provider/internal/core/client.go`
В функции `createInstanceWithContext` (и при необходимости в `runInstanceOperationUniversalByCodeWithTimeout` / `updateResourceWithTimeout`) заменяется жёсткое падение на условный graceful fallback:
```go
opDetailsResp, _, err := c.doRequest(ctx, "GET", fmt.Sprintf("/instanceOperations/%s?fields=cfsParams", opUid), nil)
if err != nil {
// Проверяем, есть ли среди переданных строковых параметров не-UUID значения,
// требующие резолвинга через refSvcId.
// Если все строковые параметры уже UUID или числа/литералы — логируем предупреждение и продолжаем.
c.logWarn(ctx, "не удалось получить cfsParams для операции %s (%v), продолжаем отправку параметров", opUid, err)
} else {
var opDetails universalOpResponse
if err := json.Unmarshal(opDetailsResp, &opDetails); err == nil {
params, err = c.resolveRefSvcParamValues(ctx, opDetails.InstanceOperation.CfsParams, params)
if err != nil {
return "", err
}
}
}
```
### Критерии безопасности:
1. Fallback условный: если бэкенд упал, но параметры уже валидны / являются UUID — выполнение продолжается.
2. Логируется явный warning с идентификатором операции `opUid` и телом ошибки.
3. Валидация значений параметров не теряется — её по-прежнему выполняет бэкенд на этапе `GET /validate-cfs`.
@@ -0,0 +1,217 @@
# Forensic Analysis: API Model to Ordinary YAML
## Цель
Исследовать существующую цепочку:
```text
API model
↓
ordinary generator
↓
API YAML
```
Цель этапа — получить подтверждённую картину движения данных и установить, что сохраняется, преобразуется или теряется до формирования API YAML.
Этот документ предназначен для передачи Opus перед анализом.
## Строгие ограничения
На этом этапе запрещено:
- изменять файлы;
- писать код;
- менять ordinary generator;
- проектировать `ModifierSpec`;
- проектировать `modifiers.yaml`;
- проектировать `delete_rule`;
- проектировать output layout или orchestration;
- обсуждать inverse и dependency ordering;
- придумывать API identifiers;
- придумывать `parameter_path`;
- придумывать HTTP method или payload structure;
- считать API YAML полным источником данных без доказательства.
Если факт невозможно установить, его нужно обозначить как `unknown` или как требующий проверки фактическим API. Нельзя закрывать неизвестность архитектурным предположением.
## Что исследовать
### 1. Исходная API-модель
Установить:
- где находится canonical API model;
- в каком формате она представлена;
- как представлены services и operations;
- как представлены параметры;
- как представлены nested objects и arrays;
- как представлены типы параметров;
- существует ли стабильный operation ID;
- существует ли стабильный parameter ID;
- какие данные доступны до запуска ordinary generator.
### 2. Ordinary generator
Установить:
- какой input получает generator;
- где он читает API-модель;
- какие внутренние структуры строит;
- какие преобразования выполняет;
- какие поля нормализует или переименовывает;
- какие поля вычисляет;
- какие поля отбрасывает;
- где формируется API YAML;
- где именно могут происходить потери данных.
Обязательно различать:
```text
данные отсутствуют уже в API
```
и:
```text
данные присутствуют в API, но теряются ordinary generator
```
### 3. API YAML
Установить:
- какие поля сохраняются;
- какие поля представлены иначе, чем в API-модели;
- сохраняются ли nested objects и arrays;
- сохраняются ли типы;
- сохраняются ли operation identity и parameter identity;
- сохраняются ли HTTP method и path, если они есть в исходной модели;
- какие данные доступны будущему modifier layer;
- какие данные потенциально доступны только в исходной API-модели.
## Обязательные трассировки
### `vc_org`
Отдельно проследить `vIPConfigure` и `count`:
```text
API model
→ generator input
→ internal generator representation
→ generator transformation
→ API YAML
```
Для каждого этапа указать:
- присутствует ли `vIPConfigure`;
- присутствует ли `count`;
- в каком типе они представлены;
- в какой структуре находятся;
- изменяются ли их имена или типы;
- теряются ли они;
- если теряются, в какой точке.
Не считать заранее известной структуру `vIPConfigure` или `count`.
### `vc_nsxt`
Отдельно проследить `ipSpaceName` и связанные параметры по той же цепочке:
```text
API model
→ generator input
→ internal generator representation
→ generator transformation
→ API YAML
```
Установить:
- где появляется `ipSpaceName`;
- к какой operation или структуре он относится;
- в каком типе представлен;
- является ли обычным полем, nested field или частью массива;
- какие связанные параметры находятся рядом;
- сохраняется ли он в API YAML;
- изменяются ли его значение или тип;
- теряются ли связанные поля.
## Требования к доказательности
Каждый вывод разделять на:
- **Подтверждённый факт** — непосредственно виден из кода, структуры данных, фактического API input, generator input или API YAML.
- **Неизвестное** — информация отсутствует или неоднозначна.
- **Предположение** — не использовать как основание для архитектуры; только явно перечислять как неподтверждённое.
Для каждого важного вывода указывать конкретное основание: файл, функцию, структуру, входной документ или фрагмент YAML/JSON. Если точное место невозможно назвать, это указать как ограничение анализа.
## Формат итогового отчёта
Отчёт должен содержать только следующие разделы:
1. **Подтверждённые факты**
2. **Исходная API-модель**
3. **Как ordinary generator читает модель**
4. **Как формируется API YAML**
5. **Цепочка данных для `vc_org.vIPConfigure.count`**
6. **Цепочка данных для `vc_nsxt.ipSpaceName` и связанных параметров**
7. **Что сохраняется**
8. **Что преобразуется или нормализуется**
9. **Что теряется**
10. **Где происходят потери**
11. **Какие данные доступны в API YAML**
12. **Какие данные доступны только в API-модели**
13. **Неизвестные места**
14. **Что необходимо проверить фактическим API**
## Критерий завершения
Forensic analysis завершён только тогда, когда для каждого потенциально необходимого modifier-элемента можно проследить происхождение:
```text
API model
→ generator input
→ internal representation
→ transformation
→ API YAML
```
без неизвестных промежуточных преобразований.
Для `vc_org` и `vc_nsxt` должны быть подтверждены:
```text
operation identity
parameter identity
parameter structure
parameter type
API model representation
generator representation
YAML representation
transformation
loss or absence
```
Если на существенном этапе остаётся `???`, исследование не завершено. Этот пункт нужно зафиксировать как `unknown`, а не проектировать решение.
## Главный принцип
Сначала:
```text
исследовать
↓
зафиксировать факты
↓
зафиксировать неизвестное
↓
отличить отсутствие данных от потери данных
```
Только после отдельного согласования forensic report можно переходить к проектированию `ModifierSpec`, `modifiers.yaml`, `delete_rule`, modifier generator, output boundary и orchestration.
На текущем этапе никаких решений по этим компонентам принимать нельзя.
@@ -0,0 +1,234 @@
# Forensic Analysis: API Model to Ordinary YAML
**Анализ от Gemini 3.8 flash.**
Исследование проведено строго в границах требований [FORENSIC_ANALYSIS_BRIEF_2026-09-23.md](FORENSIC_ANALYSIS_BRIEF_2026-09-23.md).
---
## 1. Подтверждённые факты
- **Цепочка генерации YAML:** Инструмент `yaml-generator` ([TOOLS/yaml-generator/main.go](../TOOLS/yaml-generator/main.go)) опрашивает live HTTP REST Gateway API Nubes по сети и сериализует результат в YAML-файлы спецификаций (`generated/{stand}/resources_yaml/{service_id}_{name}.yaml`), используя структуры контракта [TOOLS/lib/types.go](../TOOLS/lib/types.go).
- **Спецификации сервисов в репозитории:** Канонические сгенерированные спеки сервисов физически размещены в [generated/dev/resources_yaml/](../generated/dev/resources_yaml/). В частности, [19_vc_org.yaml](../generated/dev/resources_yaml/19_vc_org.yaml) и [22_vc_nsxt.yaml](../generated/dev/resources_yaml/22_vc_nsxt.yaml).
- **В `vc_org.yaml`:**
- Операция `modify` имеет числовой ID `207`, `kind: instance`, `action: modify`.
- Параметр `vIPConfigure` присутствует как параметр операции `modify` с числовым ID `662`, типом `data_type: array-map-fixed`, `required: true`.
- Поле `count` присутствует внутри `sub_params` параметра `vIPConfigure` с числовым ID `40`, `data_type: integer > 0`, `required: true`, `is_modifiable: false`.
- Поле `name` присутствует внутри `sub_params` параметра `vIPConfigure` с числовым ID `39`, `data_type: string`, `required: true`, `is_modifiable: false`.
- **В `vc_nsxt.yaml`:**
- Операция `modify` имеет числовой ID `111`, `kind: instance`, `action: modify`.
- Параметр `ipSpaceName` присутствует в операции `modify` с числовым ID `372`, `data_type: string`, `required: false`, `sort: 50`.
- С ним рядом в операции `modify` присутствуют:
- `needEnableAVI` (ID `368`, `data_type: boolean`, `required: false`, `value_list: ["false", "true"]`);
- `virtualServicesCount` (ID `369`, `data_type: integer > 0`, `required: false`, `minvalue: 1`, `maxvalue: 4`);
- `qosProfile` (ID `856`, `data_type: string`, `required: false`);
- `routedNetConfiguration` (ID `1112`, `data_type: map-fixed`, `required: true`, с вложенными подполями `ipAddrPool`, `mainDns`, `secondDns`).
- **Генератор ресурсов (Ordinary Resource Generator):**
- Расположен в [TOOLS/resource-generator/main.go](../TOOLS/resource-generator/main.go).
- При `op.Kind == "instance"` (что установлено для `modify` в [generated/dev/resources_yaml/19_vc_org.yaml](../generated/dev/resources_yaml/19_vc_org.yaml) и [generated/dev/resources_yaml/22_vc_nsxt.yaml](../generated/dev/resources_yaml/22_vc_nsxt.yaml)) генератор объединяет параметры `create` и `modify` в схему одного общего ресурса `nubes_vc_org` / `nubes_vc_nsxt`.
- Параметры, отсутствующие в операции `create`, но присутствующие в операции `modify` (как `vIPConfigure` в `vc_org`), не включаются в жизненный цикл `create`, а при отсутствии отдельной разметки `kind: modifier` в YAML генератор не создаёт под них отдельного ресурса модификатора.
---
## 2. Исходная API-модель
- **Источник и формат:** Модель получается HTTP-клиентом ([TOOLS/yaml-generator/internal/client/client.go](../TOOLS/yaml-generator/internal/client/client.go#L44-L62)) по протоколу HTTP GET в формате JSON.
- **Эндпоинты API:**
- Метаданные сервиса и список операций: `GET /services/{svcId}` (возвращает `types.ServiceResponse`).
- Метаданные конкретной операции: `GET /instanceOperations/default/{svcOperationId}` (возвращает `types.ServiceOperationResponse`).
- **Идентификаторы операций и параметров:**
- Операция идентифицируется стабильным числовым `svcOperationId` (int) и строковым именем `operation` (например, `"modify"`, `"create"`).
- Параметры идентифицируются стабильным числовым `svcOperationCfsParamId` (int) и строковым кодом `svcOperationCfsParam` (например, `"vIPConfigure"`, `"ipSpaceName"`).
- **Вложенные объекты и массивы (`dataDescriptor`):**
- В ответе эндпоинта `/instanceOperations/default/{id}` сложная структура передаётся в поле `dataDescriptor: map[string]CfsSubParam`.
- Каждое подполе имеет свой числовой `svcOperationCfsSubparamId`, строковый ключ (код подполя), `dataType`, `isRequired`, `isModifiableDefinition`, `defaultValue`, `valueList` (строка через запятую или массив).
---
## 3. Как ordinary generator читает модель
- **Входной поток:**
`yaml-generator` вызывает `cli.GetService(svc.ID)` и перебирает список `info.Operations`.
- **Чтение операций и параметров:**
Для каждой операции вызывается `cli.GetServiceOperation(op.SvcOperationID)` ([TOOLS/yaml-generator/internal/client/client.go](../TOOLS/yaml-generator/internal/client/client.go#L182-L240)).
- **Преобразования и нормализация:**
- Имена сервисов и операций нормализуются в snake_case функцией `normalize.Identifier` ([TOOLS/yaml-generator/internal/normalize/normalize.go](../TOOLS/yaml-generator/internal/normalize/normalize.go#L17-L50)).
- Классификация операции: функция `classifyOperation` делит операции на `instance` (для `create`, `modify`, `delete`, `suspend`, `resume`), `subresource` (если есть символ подчеркивания) или `action`.
- Разворачивание `dataDescriptor`: генератор обходит `map[string]CfsSubParam`, преобразует подполя в срез `types.ParamSpec` и детерминированно сортирует по `subParams[i].ID`.
- Сортировка верхнеуровневых параметров по `params[i].ID`.
- Сортировка операций по имени и ID.
- **Что отбрасывается / не сохраняется в YAML:**
- Конкретные HTTP method и URL-пути эндпоинтов API платформы (они зашиты в код клиента, в спек YAML не пишутся).
- Вспомогательные поля `CfsParam`, не имеющие тега `yaml:` в [TOOLS/lib/types.go](../TOOLS/lib/types.go#L70-L101), если они не сериализуются или приходят пустыми: `IsModifiable`, `IsSensitive` (указаны с `omitempty`). Поле `dataDescriptor` как мапа отбрасывается — сохраняется преобразованный срез `sub_params`.
---
## 4. Как формируется API YAML
- Сериализация структуры `types.ServiceSpec` в YAML выполняется через библиотеку `gopkg.in/yaml.v3` ([TOOLS/yaml-generator/main.go](../TOOLS/yaml-generator/main.go#L107-L114)).
- В результирующий файл пишутся:
- Метаданные сервиса (`name`, `service_id`, `service_display_name`, `service_short_name`, `service_man`).
- Стандартная секция `lifecycle` и `outputs`.
- Секция `operations` со списком операций и всеми их параметрами (`id`, `code`, `data_type`, `required`, `default`, `value_list`, `descr`, `man`, `sort`, `sub_params`).
---
## 5. Цепочка данных для `vc_org.vIPConfigure.count`
1. **API Model:**
- Эндпоинт `/instanceOperations/default/207` возвращает параметр с `svcOperationCfsParamId: 662`, `svcOperationCfsParam: "vIPConfigure"`, `dataType: "array-map-fixed"`.
- Внутри него поле `dataDescriptor` содержит ключ `"count"`:
- `svcOperationCfsSubparamId: 40`,
- `dataType: "integer > 0"`,
- `isRequired: true`,
- `defaultValue: ""`,
- `descr: "Пример: \`1\`"`.
2. **Generator Input:**
- Читается в структуру `types.CfsParam` с мапой `DataDescriptor map[string]CfsSubParam` ([TOOLS/yaml-generator/internal/types/types.go](../TOOLS/yaml-generator/internal/types/types.go#L49-L72)).
3. **Internal Generator Representation:**
- В [TOOLS/yaml-generator/internal/client/client.go](../TOOLS/yaml-generator/internal/client/client.go#L210-L232) мапа `dataDescriptor` разворачивается в `types.ParamSpec.SubParams`. Поле `count` становится элементом среза `SubParams` с `ID: 40`, `Code: "count"`, `DataType: "integer > 0"`.
4. **Generator Transformation:**
- Сортируется по ID (`sort.Slice(subParams, ...)`).
5. **API YAML:**
- Записывается в [generated/dev/resources_yaml/19_vc_org.yaml](../generated/dev/resources_yaml/19_vc_org.yaml#L131-L150):
```yaml
- id: 662
code: vIPConfigure
data_type: array-map-fixed
required: true
sort: 10
sub_params:
- id: 39
code: name
data_type: string
required: true
default: ""
is_modifiable: false
- id: 40
code: count
data_type: integer > 0
required: true
default: ""
descr: 'Пример: `1`'
is_modifiable: false
```
- **Потерь в YAML нет:** `vIPConfigure` и `count` полностью и без искажений сохранены в каноническом YAML спецификации.
---
## 6. Цепочка данных для `vc_nsxt.ipSpaceName` и связанных параметров
1. **API Model:**
- Эндпоинт `/instanceOperations/default/111` возвращает операцию `modify` сервиса 22.
- Параметр `ipSpaceName` возвращается с:
- `svcOperationCfsParamId: 372`,
- `svcOperationCfsParam: "ipSpaceName"`,
- `dataType: "string"`,
- `isRequired: false`,
- `descr: "Имя ip Space для внешнего IP"`,
- `man: "Необходимо указывать, если включён параметр \`Выделить VIP для SNAT\`"`,
- `sort: 50`.
- В HAR-дампах ([HAR/edge_.har](../HAR/edge_.har#L7985)) на живом инстансе в рантайме возвращается `valueList: ["no-needed", ...]`.
2. **Generator Input:**
- Читается в структуру `types.CfsParam` ([TOOLS/yaml-generator/internal/types/types.go](../TOOLS/yaml-generator/internal/types/types.go#L49-L72)).
3. **Internal Generator Representation:**
- Преобразуется в `types.ParamSpec` со значениями `ID: 372`, `Code: "ipSpaceName"`, `DataType: "string"`.
4. **Generator Transformation:**
- Нормализуются defaults и value_list через `normalizeDefault` и `normalizeValueList`.
5. **API YAML:**
- Записывается в [generated/dev/resources_yaml/22_vc_nsxt.yaml](../generated/dev/resources_yaml/22_vc_nsxt.yaml#L149-L155):
```yaml
- id: 372
code: ipSpaceName
data_type: string
required: false
descr: Имя ip Space для внешнего IP
man: Необходимо указывать, если включён параметр `Выделить VIP для SNAT`
sort: 50
```
- Рядом в той же операции сохранены: `needEnableAVI` (ID 368), `virtualServicesCount` (ID 369), `qosProfile` (ID 856), `routedNetConfiguration` (ID 1112 с sub_params).
- **Особенность по `value_list`:** в статическом `22_vc_nsxt.yaml` поле `value_list` для `ipSpaceName` отсутствует (`null` в ответе static-дефолтов эндпоинта `/instanceOperations/default/111`), хотя в рантайме на конкретном инстансе `valueList` динамически содержит `["no-needed", ...]`.
---
## 7. Что сохраняется
- Полная идентичность сущностей: `service_id`, `svcOperationId` (как `id` операции), `svcOperationCfsParamId` (как `id` параметра), `svcOperationCfsSubparamId` (как `id` в `sub_params`).
- Строковые коды: `operation`, коды параметров (`code`).
- Исходные типы платформы: `dataType` (`string`, `boolean`, `integer > 0`, `array-map-fixed`, `map-fixed`).
- Вся структура вложенности (`dataDescriptor` → `sub_params`).
- Флаги `required`, валидационные regex, min/max, описания (`descr`, `man`), порядок (`sort`).
---
## 8. Что преобразуется или нормализуется
- Имена операций и сервисов приводятся к ASCII snake_case через `normalize.Identifier`.
- `dataDescriptor` из мапы ключей преобразуется в упорядоченный срез `sub_params` с сортировкой по числовому `id`.
- Значения `valueList` и `default` приводятся к строковым представлениям (убираются пробелы, пустые значения приводятся к `nil`).
---
## 9. Что теряется
- HTTP-метод и путь обращения к API (в YAML отсутствуют; генератор считает их внешним знанием рантайма).
- Динамические значения списков выбора (`valueList`): эндпоинт дефолтов `/instanceOperations/default/{id}` возвращает пустой `valueList` для полей, зависящих от конкретного тенанта/инстанса (например, доступные `ipSpaceName` для конкретного VDC/Org).
---
## 10. Где происходят потери
- Потери HTTP-метаданных (метод, URL) происходят на этапе маршалинга структуры `types.ServiceSpec` в YAML ([TOOLS/yaml-generator/main.go](../TOOLS/yaml-generator/main.go#L94-L105)), так как они изначально отсутствуют в контракте [TOOLS/lib/types.go](../TOOLS/lib/types.go).
- Отсутствие runtime `valueList` обусловлено вызовом шаблонного эндпоинта `/instanceOperations/default/{id}` вместо запроса контекста живого инстанса.
---
## 11. Какие данные доступны в API YAML
- Полный перечень всех сервисов, операций (`create`, `modify`, `delete`, `suspend`, `resume`, сабресурсов) и их параметров.
- Точные типы платформы (`data_type`) и иерархия подполей (`sub_params`).
- Идентификаторы `id` (CFS param IDs) и символические коды (`code`).
- Метаданные валидации (обязательность, регулярные выражения, ограничения диапазонов).
---
## 12. Какие данные доступны только в API-модели
- Динамические списки допустимых значений (`valueList`), вычисляемые бэкендом для конкретного состояния конкретного инстанса (например, список реально существующих ipSpaces организации при вызове modify на Edge).
- Внутренние служебные поля платформы CFS, отфильтрованные моделью генератора (`contractId`, `contragentId`, `nestedRefData`, `config`, `statePath`, `expression`).
---
## 13. Неизвестные места
- Неизвестно, возвращает ли платформа Nubes какую-либо схему валидации для эндпоинтов отката/деаллокации (например, принимает ли `vc_org.modify` пустой массив `vIPConfigure: []` для полного снятия или требует только уменьшения `count: 0`), так как в дефолтной модели операции 207 описан только общий формат `vIPConfigure`.
---
## 14. Что необходимо проверить фактическим API
- Поведение `vc_org` (операция 207) при передаче `vIPConfigure: []` против `vIPConfigure: [{"name": "...", "count": 0}]` при попытке полной деаллокации IP-пространства.
- Поведение `vc_nsxt` (операция 111) при передаче `ipSpaceName: "no-needed"` на различных окружениях (Dev/Test/Prod).
---
## Мнение Opus: финальное ревью отчёта
Отчёт признан годным и принят как вход для дальнейшего архитектурного этапа.
### Достаточно для дальнейшей работы
- Цепочка `API model → yaml-generator → API YAML` прослежена по коду, а не по догадкам: `classifyOperation`, `params.Merge`, разворачивание `dataDescriptor` в `sub_params`.
- Обе обязательные трассировки (`vc_org.vIPConfigure.count`, `vc_nsxt.ipSpaceName`) доведены до YAML с подтверждением «потерь нет».
- Зафиксирован ключевой факт: динамический `valueList` есть только в рантайме живого инстанса, а в API YAML его нет. Это прямое ограничение для будущего modifier-слоя.
- Неизвестное поведение payload деаллокации (`vIPConfigure: []` против `count: 0`) помечено как `unknown`, а не закрыто предположением.
### Следствия перед архитектурным этапом
- Под ярлыком «Ordinary Resource Generator» в отчёте упоминаются два разных инструмента: `yaml-generator` пишет спеки, а `resource-generator` создаёт Go-ресурсы. Для проектирования `ModifierSpec` это две разные точки вмешательства.
- `vIPConfigure` и `ipSpaceName` присутствуют только в `modify` и отсутствуют в `create`. В текущей схеме они сливаются в общий ресурс и не имеют отдельного жизненного цикла. Это корень задачи модификаторов.
- Runtime-`valueList` придётся получать не из дефолтного эндпоинта, а из контекста инстанса. Это вопрос рантайма провайдера, а не генератора.
### Итоговая оценка
Ошибок, которые ломали бы выводы отчёта, не выявлено. Отчёт можно использовать как подтверждённую основу для дальнейшего проектирования.
@@ -0,0 +1,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.
+38
View File
@@ -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.)

Some files were not shown because too many files have changed in this diff Show More