69 Commits
Author SHA1 Message Date
Repinoid 4197a76aba 1 2026-09-30 21:01:05 +03:00
Repinoid 575f1e29a4 docs(prompt): промпт Opus — код-ревью правок (раунд 5), ответ <= 10 строк 2026-09-30 20:55:05 +03:00
Repinoid b3cce5bb70 docs(history): раунд 4 — решения Q1-Q5 и их статус 2026-09-30 20:53:41 +03:00
Repinoid 7a6f6650c6 docs(architecture): зафиксированы решения Q1/Q2/Q4
- Q1: POST не ретраится (не идемпотентен; Idempotency-Key у API нет) — решение.
- Q2: при ошибке после POST /instanceOperations и до run операция остаётся черновиком;
  отмены нет (ни в коде, ни в HAR — DELETE /instanceOperations/{uid} отсутствует).
- Q4: единый контракт жизненного цикла — keep_on_destroy + suspend_on_destroy;
  delete_strategy (YAML) = маппинг на них (noop_warn/inverse/error).
2026-09-30 20:53:31 +03:00
Repinoid 9da97663c6 fix(Q5): сохранять instanceUid при ошибке после создания + partial state
Проблема: CreateGenericInstanceUniversalV6 при ЛЮБОЙ ошибке после создания инстанса
возвращал "", а шаблон Create при ошибке не писал ID в state => облачный инстанс
осиротевал (Terraform о нём не знает, повторный apply упирается в страж дубликатов).

- core/instance_create.go: ошибки после получения instanceUid возвращают uid вместе
  с ошибкой (POST /instanceOperations, пустой opUid, разбор cfsParams, resolve,
  отправка параметров, validate, run, waitForOperationFinish, ensureInstanceCreated).
  До создания uid — по-прежнему "".
- templates/instance.go: при err != nil и id != "" -> data.ID + resp.State.Set (partial
  state), затем AddError.
- client_test.go: TestCreateGenericInstance_KeepsUIDWhenOperationCreateFails,
  TestCreateGenericInstance_EmptyUIDWhenInstanceCreateFails.
- ARCHITECTURE.md: пункт про partial state.

Проверено: 02 (dev) + dev-materialize -> 40 файлов resources_gen содержат фикс;
go build ./... OK; go test ./internal/... -short PASS.
2026-09-30 20:53:14 +03:00
Repinoid 54f036e228 docs(history): замер Q3 — угадывание типа по имени не подтверждается
Замер по generated/dev/resources_yaml (40 файлов, только чтение):
- required-параметров: 852; с пустым data_type — 5;
- всего с пустым data_type: 12 (~1.2%); угадывание по имени даёт != '' только
  для 1 тестового (1_dummy.jsonExample) => гипотеза A4 не подтверждается,
  правка косметическая, не исправление дефекта.
2026-09-30 20:48:18 +03:00
Repinoid 5a0bb432ab docs: промпт Opus раунд 4 + журнал правок ядра/модификаторов
- NOTES/20_prompts/prompt_for_opus_remediation_round4.md — 5 коротких вопросов
  (retry POST, осиротевшая операция, zero-value по подстроке, единый словарь
  жизненного цикла, что упущено в ядре); ответ ≤ 25 строк.
- HISTORY/2026-09-30_core_and_modifiers_remediation.md — журнал: 6 коммитов,
  что отклонено (A3/A4/A5) и почему, что отложено (A2/B8/B9).
2026-09-30 20:42:30 +03:00
Repinoid 0e26e98384 docs(uuid-case): зафиксировать фактический механизм сохранения регистра
- core/refsvc.go: комментарий ссылался на несуществующий блок 'Restore user-provided
  casing' в instance.go. Факт: ref_svc исключены из read-back (InputField только при
  RefSvcId==0), UUID внутри JSON нормализуются при отправке (BuildJSON ->
  LowercaseUUIDsInText).
- docs/60_strategy/terraform_case_sensitivity_fix.md: §4 помечен как историческая
  справка (подхода originalVappUid в коде нет); актуальные §10-§11.

Проверено: go build ./... OK; go test ./internal/... -short -> PASS.
2026-09-30 20:38:37 +03:00
Repinoid 8519ba0f44 fix(modifiers): ImportState заполняет Required-атрибуты
Было: ImportState ставил только id + organization/nsxt_uid, оставляя Required-атрибуты
(vip_configure / ip_space_name) в null — Terraform не мог свести импорт с конфигом.

- nsxt_snat: importIpSpaceName(live) — live-значение, иначе канон no-needed.
- org_ip_allocation: importVipConfigure(live) — канонический JSON из live, иначе [].
- Чистые хелперы + тесты TestImportVipConfigure/TestImportIpSpaceName.

Проверено: go build ./... OK; go test ./internal/resources_core/... -short -> PASS.
Terraform import проверяется владельцем.
2026-09-30 20:36:47 +03:00
Repinoid ea75cac1a8 fix(core): idempotency pre-check сравнивает с live, а не с paramValue формы
Проблема: modifierDesiredEqualsCurrent сравнивает desired с paramValue из cfsParams
(дефолт ФОРМЫ операции), а не с состоянием инстанса. Пропуск modify на такой основе
может быть ложным (HAR/edge_.har: needEnableAVI paramValue=false при live=true).

- core/modifier_compare.go: добавлен modifierDesiredEqualsLive (источник —
  instanceLiveParams/state.params; неопределённость => не пропускаем) и хелперы
  modifierValuesEqual / modifierCodeMap; modifierDesiredEqualsCurrent переведён на них.
- core/operation_run_bycode.go: idempotent-путь использует live-pre-check; при ошибке
  чтения live modify НЕ пропускается.
- resources_core/nsxt_snat_resource.go: setSnat -> RunInstanceOperationUniversalByIdempotent
  (лишний modify на повторном apply больше не отправляется).
- modifier_compare_test.go: TestModifierDesiredEqualsLive (совпало/отличается/live недоступен).

Проверено: go build ./... OK; go test ./internal/core/... -short -> PASS.
2026-09-30 20:35:09 +03:00
Repinoid 383f8ea321 fix(core): транзиентный 401 теперь ретраится для GET
isRetryable (core/http.go): добавлен StatusUnauthorized (401) к {429,502,503,504}.
Gateway может временно отклонять валидный JWT — без этого одиночный 401 ронял
read/plan/поллинг.

- client_test.go: юнит-тест TestIsRetryable (401/429/502/503/504 = true; 400/403/404/500 = false).
- ARCHITECTURE.md: раздел API Resilience приведён к фактическому поведению
  (401 реализован; POST не ретраится намеренно).

Проверено: go build ./... OK; go test ./internal/core/... -short OK.
2026-09-30 20:33:48 +03:00
Repinoid c5a4499094 chore(tools): страж хардкодов покрыл provider/ + id сервиса в именованную константу
check_hardcoded_service_ids.sh:
- область расширена с TOOLS/ на TOOLS/ + provider/internal/ (кроме generated resources_gen/);
- второй паттерн: литеральный ref-service id в ResolveRefSvcParamValue/DisplayName(ctx, N,...);
- справка обновлена (убран удалённый serviceSpecificModifiers);
- сервис-специфичные литералы пояснены как 'именованные константы'.

org_ip_allocation_resource.go: ResolveRefSvcParamValue(ctx, 19, ...) -> svcIDVcOrg.

Проверено: bash TOOLS/scripts/check_hardcoded_service_ids.sh -> OK (exit 0).
2026-09-30 20:33:09 +03:00
Repinoid 047d53a67b docs(architecture): привести TOOLS/ARCHITECTURE.md в соответствие с кодом
- Core Principles 2-3: 'универсально/генерируется' отнесено к core/ и resources_gen/,
  а не ко всему сервис-коду.
- API Resilience: 401 НЕ ретраится (isRetryable = 429/502/503/504, только GET) —
  помечено как незакрытый разрыв со спекой.
- Exception Registry: убран удалённый реестр serviceSpecificModifiers
  (yaml-generator/main.go), описаны ручные модификаторы + несуществующий modifiers.yaml.
- Новый раздел 'Lifecycle Vocabulary': три несогласованных словаря destroy.
- Rules: 'two registries' -> один реестр + ручные ресурсы.
2026-09-30 20:32:29 +03:00
Repinoid 2c51b0392d docs(prompt): промпт для DeepSeek Pro — план правок кода/доков/архитектуры + план проверки
DeepSeek верифицирует гипотезы групп A (ядро: 401/POST-ретраи, обрыв modify, zero-value
fallback, нормализация Read) и B (модификаторы/спека), затем даёт план правок кода, правок
документации и архитектурных решений, трёхуровневый план проверки (юнит / plan-apply на
DEV_STAND/FullPipe / регрессия + стражи) и порядок работ по коммитам. Код не пишет — исполнять
будет Copilot. Журнал Opus-диалога дополнен ссылкой на этот шаг.
2026-09-30 20:20:31 +03:00
Repinoid 47e010edf3 docs(opus): дописаны пропущенные ходы журнала (Ход 10a, 12a, 14)
Записаны: запрос «твоё мнение?» по раунду 2; сообщение с путём к файлу раунда 3 и
отменённый уточняющий вопрос агента; запрос «мнение?» по раунду 3 и полное мнение агента
(в т.ч. сомнение в абсолютном выводе «401 не ретраится нигде» и в трактовке отсутствия
ретрая POST как дефекта). Статус обновлён: чат с Opus исчерпан по токенам.
2026-09-30 20:18:41 +03:00
Repinoid 96adf958bc docs(opus): раунд 3 отвечен — запись в журнал диалога
Дословно сырой лог и отчёт: 5 находок по ядру — 401 не ретраится (расхождение с
ARCHITECTURE.md:105-108), ретрай только для GET, обрыв modify после создания операции,
zero-value fallback по подстроке имени, нормализация Read только для jsonEnv/ref_svc.
Находки 1-3,5 не подтверждены замером.
2026-09-30 20:15:43 +03:00
Repinoid e46bc35b98 docs(opus): раунд 3 — фокус переведён на сам провайдер (устойчивость/корректность)
Жёсткий бюджет ради токенов: ≤10 файлов, ≤5 находок по ≤3 строки, плюс одна строка
«что проверяемо только замером». M6 снят, модификаторы — фон. Границы: ядро, resources_core,
шаблоны/хелперы генератора. В журнал добавлены крит-мнение по раунду 2 и текст раунда 3.
2026-09-30 20:11:24 +03:00
Repinoid e1e45423fa docs(opus): раунд 2 отвечен + вводная пользователя — запись в журнал диалога
Дословно: сырой лог и дельта Opus (M1 сверка номеров строк, M2 факт из check_hardcoded_service_ids.sh,
M3 снятие S4 и переклассификация S2, M4 шкала R2>R3>R6>R5, M5 разбор Read/вечного diff,
U1 modifiers.yaml не существует, U2 тройной словарь жизненного цикла, U3 варианты, M6 запрос двух файлов).
Плюс указание пользователя: модификаторы — небольшая часть, главное — сам провайдер.
2026-09-30 20:07:34 +03:00
Repinoid ea75507fe4 docs(opus): раунд 2 — замечания к отчёту Opus + запись в журнал диалога
Замечания M1–M5 (сверка номеров строк, нарушение «без догадок» в R2, натянутые S2/S4,
приоритет R5, пробел по устойчивости Read/вечный diff) и U1–U3 (modifiers.yaml не существует,
рассинхрон suspend_on_destroy/keep_on_destroy, корневая причина ручных модификаторов).
Разрешён дополнительный список файлов; право копать глубже передано Opus.
2026-09-30 19:59:44 +03:00
Repinoid dc85e7b4e0 docs(opus): полная запись диалога 2026-09-30 — промпт + отчёт Opus по архитектуре и модификаторам
Дословно, без сокращений: задание пользователя, разведка агента, содержимое промпта,
сырой лог сессии Opus и его отчёт (расхождения спека↔код S1–S5, риски R1–R6),
открытый вопрос Opus. Статус: диалог не завершён.
2026-09-30 19:57:36 +03:00
Repinoid 752244fa26 docs(prompt): промпт для Opus 4.8 — анализ архитектуры провайдера + ресурсов-модификаторов
Задача: разбор универсальной архитектуры и слоя модификаторов (nubes_vc_org_ip_allocation,
nubes_vc_nsxt_snat). Жёсткие границы доступа (запрет на HISTORY/NOTES/TMP/HAR/docs и git-историю),
исчерпывающий список из 22 файлов (1 спека + код + YAML-спеки + пример применения),
сжатый формат ответа, режим диалога с правом задать уточняющий вопрос.
2026-09-30 19:44:57 +03:00
Repinoid bed269cf27 release: перезаливка провайдера во все 3 стенда — prod 1.0.0, dev 2.0.0, test 3.0.0
Полный цикл 03 (01+02 → сборка 3 платформ → GPG → заливка S3) по каждому стенду:
- prod nubes/nubes 1.0.0;
- dev  nubes-dev/nubes 2.0.0  (VERSION в profile.env: 2.0.24 → 2.0.0);
- test nubes-test/nubes 3.0.0.

Проверено: sha256 залитого linux-бинарника == локальной сборке на всех трёх;
API /versions отдаёт 1.0.0 / 2.0.0 / 3.0.0; в S3 по 5 объектов на версию.

Последствия (приняты владельцем): версии X.0.0 перезаписаны → у пользователей
с .terraform.lock.hcl будет checksum mismatch (лечится terraform init -upgrade).
Подробности: HISTORY/2026-09-30_release_1_0_0_2_0_0_3_0_0_all_stands.md
2026-09-30 19:16:48 +03:00
Repinoid fd3ab32534 chore(dev-registry): физически удалены версии провайдера старше 2.0.21
В dev-реестре (s3://nubes-terraform-registry/.../nubes-dev/nubes/) удалены
версии 2.0.0–2.0.20 — 21 версия, 105 объектов (~800 MiB). Бакет un-versioned.

Осталось: 2.0.21, 2.0.22, 2.0.23, 2.0.24 (20 объектов).
Проверено: mc ls (20 объектов), API /versions (4), download 2.0.23/2.0.24 → HTTP 206
(ZIP), 2.0.0/2.0.20 → HTTP 404; nubes-test (3.0.0) и nubes (1.0.0) не тронуты.

Резервные копии zip не делались (прямое указание «стереть физически»);
восстановление — только пересборкой из git-истории.
Документация: HISTORY/2026-09-30_dev_registry_prune_versions.md, VERSIONS.md.
2026-09-30 10:50:45 +03:00
Repinoid 46dc548af4 docs(history): UUID-регистр — процедура релиза и карта точек нормализации
Перенесено из служебной памяти VS Code в файл репозитория:
- пошаговая процедура релиза dev (VERSION → 03 → curl-проверка → VERSIONS.md →
  dev-materialize), включая, что доки в реестр не публиковались;
- карта: нормализация нужна в двух местах (сравнение и отправка BuildJSON),
  перечень функций и правила (lower() — костыль; план целиком не нормализуем).
2026-09-30 10:32:22 +03:00
Repinoid 89bfb46b1c docs(history): этап 4 — перепроверка после перегенерации + подводные камни
Перенесено в файл репозитория (а не только в служебную память VS Code):
- результаты полной перегенерации и перепроверки всех трёх стендов;
- подводные камни: случайные default от API (детектор дрейфа по побайтовому
  сравнению не работает); случайный default вшивается в Go-код → drift 15 файлов
  сразу после перегенерации; generated/<стенд>/go|docs стареют после шага 01;
  403 без браузерного User-Agent (DDoS-Guard);
- актуальная карта пайплайна, токены, что удалено и что оставлено осознанно.
2026-09-30 10:30:41 +03:00
Repinoid 5d29010352 docs(history): этап 3 — сверка списков стендов с облачным каталогом
Зафиксирована методика: истина = GET /services?isProductionReady=true
(без фильтра API отдаёт все сервисы платформы, включая DEPRECATED).
Результаты по dev/test/prod и исправление prod (Vault).
2026-09-30 10:13:35 +03:00
Repinoid a68a36a0d2 fix(config): prod — включить Vault (151 k8sOpenbao) по сверке с облачным каталогом
Сверка TOOLS/config/prod/services_list.txt с облачным каталогом
(GET /api/v1/svc/services?isProductionReady=true → 36 сервисов):
- активных в файле было 35, лишних нет, но не хватало 151 k8sOpenbao (Vault);
- комментарий «нет в PROD UI» устарел: сервис отдаётся каталогом prod
  (isProductionReady=true, resourceRealmTypeId=3).

После правки: 36 активных = 36 в облаке, перегенерация prod дала 36 YAML,
включая 151_k8s_openbao.yaml. dev (40/40) и test (36/36) расхождений не имели.
2026-09-30 10:13:13 +03:00
Repinoid 5e1a6f06bc docs(history): этап 2 — удаление мёртвого легаси (что удалено, что оставлено)
- раздел «Этап 2» с таблицей удалённых файлов и обоснованием;
- зафиксировано, что НЕ удалено и почему (index.cfm-совместимость,
  справочная копия DOCS_PIPELINE/publish-docs.sh, исторические документы);
- результаты проверок после удаления (bash -n, smoke-прогон 01 dev).
2026-09-30 09:53:57 +03:00
Repinoid c822ae2f2a docs: актуализировать ссылки после удаления общего services_list.txt
- README.md, HOW_TO/README.md, HOW_TO/DEVOPS_BUILD_PIPELINE.md,
  HOW_TO/HOWTO_ADD_NEW_SERVICE.md, DOCS_PIPELINE/README.md:
  TOOLS/config/services_list.txt → TOOLS/config/<стенд>/services_list.txt;
- HOW_TO/HOWTO_ADD_NEW_SERVICE.md: блок «Быстрый старт» переписан с
  устаревших devops/-путей на канонические (./TOOLS/scripts/*, generated/<стенд>/);
- scripts/publish-doc-page.sh: примеры devops/profiles/<стенд> → TOOLS/config/<стенд>;
- .gitignore: убрана мёртвая строка devops/profiles/*/generated/.

Проверено: bash -n для всех TOOLS/scripts/*.sh и scripts/publish-doc-page.sh — OK.
2026-09-30 09:50:37 +03:00
Repinoid 1e796c8eb1 chore(tools): удалить мёртвые легаси-скрипты и общий services_list.txt
Удалено (100% мёртвое, ничего не вызывает):
- TOOLS/scripts/10_yaml_stability_run.sh
- TOOLS/scripts/11_yaml_stability_run_latest.sh
- TOOLS/scripts/12_generate_yamls_latest.sh
- TOOLS/scripts/13_generate_yamls_clean.sh
- TOOLS/scripts/02_generate_resources_and_docs_template_v2.sh
- TOOLS/config/services_list.txt (общий список: код его не читает, а как
  «объединение» он устарел — в нём активен id 27, который в test/prod
  закомментирован как «нет в UI», и отсутствуют 87/88/97/153 из dev)

Все ссылки в документации указывали на профильные списки либо будут
исправлены отдельным коммитом.
2026-09-30 09:50:28 +03:00
Repinoid cbd559d767 docs(tools): канонический пайплайн + история ужесточения генерации YAML
- TOOLS/README.md: раздел «Канонический пайплайн (порядок шагов)» и описание
  безопасной генерации (staging → атомарная замена, бэкапы, маркер .stand);
- TOOLS/ARCHITECTURE.md: ссылки devops/… → TOOLS/config/<stand>/…;
- HISTORY/2026-09-30_yaml_pipeline_hardening.md: полная история изменений
  (что было не так, что сделано, прогон по стендам, коммиты, проверки).
2026-09-30 09:39:26 +03:00
Repinoid ad4daab358 chore(tools): легаси-скрипты генерации отключены (fail-fast DEPRECATED)
10/11/12/13 + 02_..._template_v2 нерабочие (зовут 01 без --profile, ищут
*.token в корне репо) и не вызываются никаким рабочим скриптом (ссылки —
только в исторических HISTORY/NOTES). Теперь падают сразу с подсказкой
канонического пути вместо мнимой работы; 13 вдобавок больше не может
сделать rm -f provider/resources_yaml/*.yaml.

Проверено: bash -n OK, guard отдаёт exit 2.
2026-09-30 09:38:42 +03:00
Repinoid 12b3932817 fix(tools): безопасная генерация YAML — staging + атомарная замена, без легаси-фолбэков
01_generate_yamls.sh:
- генерация в staging-каталог; рабочий каталог не удаляется заранее;
  атомарная замена (mv) только при полном успехе, старый каталог → бэкап
  resources_yaml.bak-<UTC> (ротация KEEP_BACKUPS=5);
- убран легаси-фолбэк токена 'ls -t ROOT/*.token' (подхватывал чужой токен);
- NUBES_API_ENDPOINT обязателен (убран молчаливый PROD-дефолт);
- имя сервиса берётся из 2-го поля services_list.txt (убран лишний HTTP-запрос);
- убран безусловный rm старого YAML перед генерацией (неатомарность);
- удалён дубль SERVICES_FILE_DEFAULT; маркер .stand защищает от чужого стенда;
- синхронизированы комментарии (REQUEST_DELAY 0.5, пути, формат списка).

yaml-generator/internal/config:
- NUBES_API_ENDPOINT обязателен (убран PROD-дефолт);
- убран легаси-поиск последнего *.token в корне репо (loadToken/findLatestToken);
- убран несуществующий путь provider/devops/config/services_list.txt
  (теперь требуется явный NUBES_SERVICES_FILE);
- удалены мёртвые getenvDefault/findLatestToken.

Проверено: bash -n OK, go vet/go build OK.
2026-09-30 09:28:41 +03:00
Repinoid 190fc68f93 chore(gitignore): игнорировать каталог secrets/ 2026-09-30 09:07:47 +03:00
Repinoid b81dc88abe docs(uuid-case): отметить, что фикс выпущен в dev 2.0.24
- HISTORY/2026-09-30: раздел «Следствия» — релиз 2.0.24 (залито в реестр), доки не публиковались.
- terraform_case_sensitivity_fix.md §11: «не выпущено» -> «выпущено в 2.0.24»; уточнение по костылю lower().
2026-09-30 08:50:35 +03:00
Repinoid c1b02a1c2d release(dev): 2.0.24 — фикс регистра UUID при отправке map-fixed JSON залит в реестр
Собрано и загружено в nubes-dev/nubes/2.0.24/ (linux/windows/darwin amd64
+ SHA256SUMS + .sig), версия видна в реестре.
2026-09-30 08:50:17 +03:00
Repinoid d6d0b733ae chore(dev): поднять версию провайдера 2.0.17 -> 2.0.24
Конфиг отставал от реестра (там уже 2.0.23), из-за чего генератор доков
подставлял неверную версию. Дальше — генерация и релиз 2.0.24.
2026-09-30 08:35:59 +03:00
Repinoid 9aed2dcdd9 fix(provider): нормализация регистра UUID при отправке map-fixed JSON в API
- resources_core.BuildJSON оборачивает результат в jsonutil.LowercaseUUIDsInText:
  платформа сравнивает регистр UUID при create, а ресурсы отдают id в UPPERCASE
  (nsxtUid/vdcUid) -> без нормализации create Штурвала падал 'Edge не развёрнут
  в указанном vDC' (обнаружено на провайдере 2.0.23 из-под Windows).
- Одна точка покрывает все map-fixed-параметры (create/modify/redeploy),
  регенерация не требуется.
- Документация: HISTORY/2026-09-30, docs/60_strategy/terraform_case_sensitivity_fix.md §11,
  NOTES/30_analysis/ARCHITECTURE_NEW.md §6.5, docs/help/architecture-and-methods.md §7.

Не выпущено: версия не поднималась, релиз/регенерация не выполнялись.
2026-09-30 08:34:04 +03:00
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
125 changed files with 9105 additions and 560 deletions
+31 -17
View File
@@ -1,17 +1,31 @@
НИКАКОЙ САМОДЕЙТЕЛЬНОСТИ !!! делать ТОЛЬКО ТО НА ЧТО ПОЛУЧЕНО РАЗРЕШЕНИЕ !!!! # Правила работы в этом репозитории
НИКАКИХ ДОГАДОК !!! ЕСТь сомнения - СПРОСИ !!!
НИКОГДА НЕ ДЕЛАЙ ПРЕДПОЛОЖЕНИЙ !!! ## Разрешения и самодеятельность
ВСЕГДА СПРАШИВАЙ, ЕСЛИ НЕ УВЕРЕН !!!
НИКОГДА НЕ ИГНОРИРУЙ ПОЛУЧЕННЫЕ ИНСТРУКЦИИ !!! - **Никакой самодеятельности**: делать только то, на что получено разрешение.
ВСЕГДА ПОДТВЕРЖДАЙ ПОЛУЧЕННЫЕ ИНСТРУКЦИИ !!! - Полученные инструкции **не игнорировать**: соблюдать их и подтверждать.
НИКОГДА НЕ ИЗМЕНЯЙ ИНСТРУКЦИИ БЕЗ РАЗРЕШЕНИЯ !!! - Соблюдать порядок и последовательность инструкций.
ВСЕГДА СОБЛЮДАЙ ПОРЯДОК И ПОСЛЕДОВАТЕЛЬНОСТЬ В ИНСТРУКЦИЯХ !!! - Не превышать свои полномочия.
НИКОГДА НЕ ПРЕВЫШАЙ СВОИ ПОЛНОМОЧИЯ !!! - Соблюдать безопасность и конфиденциальность.
ВСЕГДА СОБЛЮДАЙ БЕЗОПАСНОСТЬ И КОНФИДЕНЦИАЛЬНОСТЬ !!! - Не изменять инструкции без разрешения.
НИКОГДА НЕ ПЕРЕДАВАЙ СЕКРЕТЫ ИНТЕРНЕТУ БЕЗ РАЗРЕШЕНИЯ !!! - Не вызывать другие агенты без разрешения.
НЕ ВЫЗЫВАТЬ ДРУГИЕ АГЕНТЫ БЕЗ РАЗРЕШЕНИЯ !!! - Не передавать секреты в интернет без разрешения.
коммитить после каждой правки, чтобы зафиксировать текущее состояние и избежать потери изменений. Использовать осмысленные сообщения коммитов, отражающие суть изменений.
ВСЕГДА СОХРАНЯТЬ РЕЗЕРВНЫЕ КОПИИ ВАЖНЫХ ФАЙЛОВ ПЕРЕД ВНЕСЕНИЕМ ИЗМЕНЕНИЙ. ## Сомнения и вопросы
НИКОГДА НЕ ПОЛАГАЙСЯ НА ПАМЯТЬ — ВСЕГДА ПРОВЕРЯЙ АКТУАЛЬНОСТЬ ИНСТРУКЦИЙ.
ВСЕГДА СОБЛЮДАЙ ИНСТРУКЦИИ, ДАВАЙТЕ ПОДТВЕРЖДЕНИЯ И НЕ ДЕЛАЙТЕ САМОСТОЯТЕЛЬНЫХ ИЗМЕНЕНИЙ. - **Никаких догадок**: есть сомнения — спроси.
Если не на 100% уверен в распоряжениях - СПРОСИ СНОВА И ПОДТВЕРДИ. НЕ ГАДАЙ ЧТО Я ИМЛ ВВИДУ !!!! - Никогда не делать предположений и не действовать по догадкам.
- Всегда спрашивать, если не уверен.
- Если уверенности в распоряжении нет на 100 % — остановиться, спросить снова и подтвердить, что имел в виду пользователь. Не гадать.
- Перепроверять всё несколько раз.
## Коммиты и бэкапы
- Коммитить после каждой правки — чтобы зафиксировать текущее состояние и не потерять изменения.
- Сообщения коммитов — осмысленные, отражающие суть изменений.
- Всегда сохранять резервные копии важных файлов перед внесением изменений.
## Общий принцип
- Соблюдать инструкции, давать подтверждения и не делать самостоятельных изменений.
- Не полагаться на память — всегда проверять актуальность инструкций.
+1 -3
View File
@@ -36,9 +36,6 @@ provider/generated/
# Локально собранный провайдер под dev_overrides (см. TMP/terraformrc.dev) # Локально собранный провайдер под dev_overrides (см. TMP/terraformrc.dev)
TMP/devbin/ TMP/devbin/
terraform-provider-nubes terraform-provider-nubes
# === Build artifacts (generated by devops scripts) ===
devops/profiles/*/generated/
*.zip *.zip
*.tar.gz *.tar.gz
*.sha256 *.sha256
@@ -78,6 +75,7 @@ secrets/.s3cfg_provider
secrets/.s3cfg* secrets/.s3cfg*
secrets/pearlharbor_registry.txt secrets/pearlharbor_registry.txt
secrets/id_ed25519.txt secrets/id_ed25519.txt
secrets/
# === MkDocs === # === MkDocs ===
site/ site/
+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
}
+1 -1
View File
@@ -4,7 +4,7 @@ terraform {
required_providers { required_providers {
nubes = { nubes = {
source = "tf-registry.containerk8s.services.ngcloud.ru/nubes-dev/nubes" source = "tf-registry.containerk8s.services.ngcloud.ru/nubes-dev/nubes"
version = "2.0.22" version = "2.0.23"
} }
} }
} }
+1 -1
View File
@@ -102,7 +102,7 @@ API стенда ──▶ generated/<стенд>/resources_yaml/ ──▶ gene
| `scripts/publish-docs.sh` | актуальная заливка docs в S3 (без версии) | | `scripts/publish-docs.sh` | актуальная заливка docs в S3 (без версии) |
| `scripts/publish-doc-page.sh` | заливка одной страницы | | `scripts/publish-doc-page.sh` | заливка одной страницы |
| `TOOLS/config/<стенд>/profile.env` | параметры стенда (endpoint, NAMESPACE, VERSION, токен, GPG) | | `TOOLS/config/<стенд>/profile.env` | параметры стенда (endpoint, NAMESPACE, VERSION, токен, GPG) |
| `TOOLS/config/registry.env`, `services_list.txt`, `operation_timeouts.json` | конфиги реестра/генерации | | `TOOLS/config/registry.env`, `TOOLS/config/<стенд>/{services_list.txt,operation_timeouts.json}` | конфиги реестра/генерации |
| `TOOLS/bin/` | генераторы: `resource-generator`, `docs-generator`, `yaml-generator` | | `TOOLS/bin/` | генераторы: `resource-generator`, `docs-generator`, `yaml-generator` |
| `secrets/{dev,test,prod}.token`, `.s3cfg_registry`, `private_key.asc` | токены API, S3-креды, GPG | | `secrets/{dev,test,prod}.token`, `.s3cfg_registry`, `private_key.asc` | токены API, S3-креды, GPG |
| `mkdocs.yml` | базовый конфиг MkDocs (тема material, exclude_docs, extra) | | `mkdocs.yml` | базовый конфиг MkDocs (тема material, exclude_docs, extra) |
@@ -14,6 +14,17 @@
| Режим «заморозки» в конфиге стенда: `keep_on_destroy=true` (эдж/SNAT/квота IP), adopt для эджа, явный `suspend_on_destroy` у кластера | `DEV_STAND/FullPipe/{edge.tf,modifiers.tf,shturval.tf}` | `40aef87` | | Режим «заморозки» в конфиге стенда: `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` | | Релиз 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`, ошибок нет. - `terraform destroy` (провайдер `2.0.22`): `0 added, 0 changed, 5 destroyed`, ошибок нет.
@@ -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,109 @@
# Правки ядра и модификаторов по итогам анализа 2026-09-30 (раунд Flash)
**Репо:** `/home/naeel/TF/tf_provider`. **Дата:** 2026-09-30.
**Источник заданий:** `NOTES/20_prompts/prompt_for_deepseek_pro_remediation_plan.md`
(гипотезы A1–A5, B1–B9) + диалог с Opus `HISTORY/OPUS/2026-09-30_provider_architecture_modifiers_dialog.md`.
---
## Что сделано (6 коммитов)
| Коммит | Пункт | Файлы | Суть |
|---|---|---|---|
| `047d53a` | C | `TOOLS/ARCHITECTURE.md` | Спека приведена к коду: ручные ресурсы, 401, удалённый `serviceSpecificModifiers`, раздел «Lifecycle Vocabulary». |
| `c5a4499` | B3 | `TOOLS/scripts/check_hardcoded_service_ids.sh`, `org_ip_allocation_resource.go` | Страж сканирует `TOOLS/` + `provider/internal/` (кроме `resources_gen/`), второй паттерн — литеральный ref-svc id. `ResolveRefSvcParamValue(ctx, 19, …)` → константа `svcIDVcOrg`. |
| `383f8ea` | A1 | `core/http.go`, `core/client_test.go`, `ARCHITECTURE.md` | 401 добавлен в `isRetryable` (действует для GET). Тест `TestIsRetryable`. |
| `ea75cac` | B6 | `core/modifier_compare.go`, `operation_run_bycode.go`, `nsxt_snat_resource.go` | Idempotency pre-check сравнивает с **live** (`state.params`), а не с `paramValue` формы. `setSnat` → `ByIdempotent`. Тест `TestModifierDesiredEqualsLive`. |
| `8519ba0` | B7 | `org_ip_allocation_resource.go`, `nsxt_snat_resource.go`, `org_ip_allocation_test.go` | `ImportState` заполняет Required (`vip_configure` из live/`[]`; `ip_space_name` из live/`no-needed`). Тесты на чистые хелперы. |
| `0e26e98` | — | `core/refsvc.go`, `docs/60_strategy/terraform_case_sensitivity_fix.md` | Убран устаревший комментарий про несуществующий блок «Restore user-provided casing»; §4 помечен как исторический. |
**Проверка после каждого коммита:** `go build ./...` OK, `go test ./internal/... -short` PASS,
`bash TOOLS/scripts/check_hardcoded_service_ids.sh` → OK.
**Бэкапы:** `TMP/backup_2026-09-30/` (исходные версии всех правимых файлов).
---
## Что ОТКЛОНЕНО после проверки по коду (важно)
- **A5 (нормализация регистра в `Read`) — был бы РЕГРЕССОМ.**
Принятое решение (проверено): state хранит регистр **пользователя**; ref_svc-атрибуты **исключены
из read-back** (шаблон `instance.go` добавляет `InputField` только при `eq .RefSvcId 0`); UUID
внутри JSON нормализуются при **отправке** (`resources_core.BuildJSON` →
`jsonutil.LowercaseUUIDsInText`). См. `docs/60_strategy/terraform_case_sensitivity_fix.md` §4 (пометка),
§10–§11. Нормализация state к lowercase сломала бы соответствие plan=config.
- **A3 (не обрывать modify при сбое live) — осознанная защита, а не дефект.**
`instanceLiveParams` намеренно возвращает ошибку: тихий fallback на `paramValue` (дефолт ФОРМЫ)
возвращает reset-баг (затирание параметров инстанса, HAR/edge_.har: `needEnableAVI`). Требуется
отдельное решение (см. Q2 промпта раунда 4).
- **A4 (угадывание типа по подстроке имени) — нужен замер.**
Fallback применяется только к required-параметру без `paramValue`/`defaultValue`
(`instance_create.go:105-113`) и при досылке modify. Гарантированного улучшения нет, риск сломать
больше, чем починить. Оставлено как есть.
---
## Отложено
- **A2 — retry POST.** Слепой ретрай создающего `POST /instanceOperations` опаснее обрыва
(дубликат операции). Решение — за владельцем (варианты в промпте раунда 4, Q1).
- **B8** — создавать ли оверлей `modifiers.yaml` или узаконить ручные модификаторы категорией в спеке.
- **B9** — единый словарь жизненного цикла (в спеку внесён как незакрытый вопрос; решение — Q4 промпта).
---
## Артефакты
- Промпт раунда 4 для Opus: `NOTES/20_prompts/prompt_for_opus_remediation_round4.md`
(5 коротких вопросов, лимит ответа ≤ 25 строк).
- Ограничение сессии: чат с Opus по раундам 1–3 исчерпан по токенам → раунд 4 в новом чате.
---
## Замер Q3 (2026-09-30): безопасно ли угадывание типа по имени?
**Источник:** `generated/dev/resources_yaml/*.yaml` (40 файлов), поля `data_type` / `required`.
**Метод:** подсчёт + эмуляция `normalizeUniversalValueV6` (ветка `nameHint`). Только чтение.
| Метрика | Значение |
|---|---|
| required-параметров всего | 852 |
| из них с пустым `data_type` | 5 |
| всего параметров с пустым `data_type` | 12 (~1.2 %) |
| из них угадывание по имени даёт ≠ `""` | **1** — `1_dummy.yaml` (`jsonExample` → `{}`), тестовый сервис |
Required с пустым `data_type` (все получают `""`; угадывание не срабатывает):
`120_clickhouse/delete:username`, `12_s3/create:resourceRealm`, `13_s3bucket/create:maxSize`,
`151_k8s_openbao/create:policyName`, `28_vc_vm_v3/create:userLogin`.
**Вывод.** Гипотеза A4 («риск неверной типизации» из-за подстроки имени) на dev-спеках
**не подтверждается**: для всех реальных сервисов угадывание по имени не срабатывает (итог `""`);
единственный эффект — тестовый `1_dummy.jsonExample`. То есть правка косметическая (упрощение),
а не исправление дефекта. Решение «снимать/оставлять» — за владельцем.
---
## Раунд 4 (Opus, новый чат) — решения и правки
Промпт: `NOTES/20_prompts/prompt_for_opus_remediation_round4.md` (5 вопросов, ответ ≤ 25 строк).
Ответ получен; ниже — что принято и что сделано.
| Q | Решение | Статус |
|---|---|---|
| Q1 retry POST | Не ретраить. `POST /instanceOperations` не идемпотентен, `Idempotency-Key` у API нет. | Зафиксировано в `TOOLS/ARCHITECTURE.md` (коммит `7a6f665`) |
| Q2 черновик операции | Отмены нет: `DELETE /instanceOperations/{uid}` отсутствует и в коде, и в HAR (проверено: `grep '"method": "DELETE"'` по `HAR/*.har` — ноль совпадений). Оставляем как есть, задокументировано. | `7a6f665` |
| Q3 zero-value по имени | **Закрыт замером**: угадывание не срабатывает (см. выше) — не дефект. | замер `54f036e` |
| Q4 словарь жизненного цикла | Единый контракт: `keep_on_destroy` + `suspend_on_destroy`; `delete_strategy` = маппинг (`noop_warn`→keep, `inverse`→destroy, `error`→валидация). | `7a6f665` |
| Q5 осиротевший инстанс | **Исправлено** (критичный). Ядро возвращает `instanceUid` вместе с ошибкой после создания; шаблон пишет partial state. | `9da9766` |
**Q5 детали:** `core/instance_create.go` — все ошибки ПОСЛЕ получения `instanceUid` возвращают
`instanceUid` (до создания — `""`); `templates/instance.go` — при `err != nil && id != ""` пишет
`data.ID` + `resp.State.Set` перед `AddError`. Регенерация dev (`02` + `dev-materialize`) → фикс в
40 файлах `resources_gen` (эфемерные, не в git). Тесты: `TestCreateGenericInstance_KeepsUIDWhenOperationCreateFails`,
`TestCreateGenericInstance_EmptyUIDWhenInstanceCreateFails`.
**Коммиты раунда 4:** `54f036e` (замер), `9da9766` (Q5), `7a6f665` (Q1/Q2/Q4).
**Осталось:** замер владельцем (`terraform plan` ×2 на `DEV_STAND/FullPipe`); B8 (`modifiers.yaml`).
@@ -0,0 +1,89 @@
# 2026-09-30 — Очистка dev-реестра: удалены версии провайдера старше 2.0.21
> Команда владельца: «в деве — сотри ФИЗИЧЕСКИ все провайдеры старше 21 версии».
> Операция **необратимая** (бакет без версионирования), выполнена 2026-09-30.
## Что и где
| Параметр | Значение |
|---|---|
| Хранилище | S3 `https://s3.msk-1.ngcloud.ru`, бакет `nubes-terraform-registry` |
| Префикс | `tf-registry.containerk8s.services.ngcloud.ru/nubes-dev/nubes/` |
| Стенд | **только dev** (`nubes-dev`); `nubes-test` и `nubes` не тронуты |
| Инструмент | `mc` (`/usr/bin/mc`), alias `prod-s3`, `--api S3v4`, креды из `secrets/.s3cfg_provider` |
| Версионирование бакета | `un-versioned` — удаление физическое, без «теневых» копий |
Состав одной версии — 5 объектов:
```
terraform-provider-nubes_<v>_darwin_amd64.zip (~13 MiB)
terraform-provider-nubes_<v>_linux_amd64.zip (~13 MiB)
terraform-provider-nubes_<v>_windows_amd64.zip (~13 MiB)
terraform-provider-nubes_<v>_SHA256SUMS
terraform-provider-nubes_<v>_SHA256SUMS.sig
```
## Было → стало
| | До | После |
|---|---|---|
| Версий | 25 (`2.0.0` … `2.0.24`) | **4** (`2.0.21`, `2.0.22`, `2.0.23`, `2.0.24`) |
| Объектов | 125 | **20** |
| Объём (zip) | ~975 MiB | ~156 MiB |
## Удалено (21 версия, 105 объектов)
```
2.0.0 2.0.1 2.0.2 2.0.3 2.0.4 2.0.5 2.0.6 2.0.7
2.0.8 2.0.9 2.0.10 2.0.11 2.0.12 2.0.13 2.0.14 2.0.15
2.0.16 2.0.17 2.0.18 2.0.19 2.0.20
```
Каждая версия: 5 объектов (3 zip ~13 MiB + `SHA256SUMS` + `SHA256SUMS.sig`).
Команда (по версии):
```bash
mc rm --recursive --force \
"prod-s3/nubes-terraform-registry/tf-registry.containerk8s.services.ngcloud.ru/nubes-dev/nubes/<v>/"
```
Перед удалением снят полный манифест (125 строк):
```bash
mc ls -r "prod-s3/nubes-terraform-registry/tf-registry.containerk8s.services.ngcloud.ru/nubes-dev/nubes/"
```
## Оставлено
```
2.0.21 2.0.22 2.0.23 2.0.24 (20 объектов, 4 × 5)
```
Актуальная версия dev — `2.0.24` (см. `VERSIONS.md`).
## Проверки после удаления
| Проверка | Результат |
|---|---|
| `mc ls` префикса dev | только `2.0.21/`, `2.0.22/`, `2.0.23/`, `2.0.24/` |
| `mc ls -r` (всего объектов) | 20 (по 5 на версию) |
| API `…/nubes-dev/nubes/versions` | `['2.0.21','2.0.22','2.0.23','2.0.24']`, count 4 |
| Скачивание `2.0.24`/`2.0.23` (linux/amd64) | HTTP **206**, ZIP-магия `50 4b 03 04` |
| Скачивание `2.0.20`/`2.0.0` (linux/amd64) | HTTP **404** (объекта нет) |
| Контроль: `nubes-test` | `3.0.0` — не тронуто |
| Контроль: `nubes` (prod) | `1.0.0` — не тронуто |
> Примечание: эндпоинт `…/<v>/download/<os>/<arch>` отдаёт метаданные (JSON с `download_url`)
> **не проверяя наличие объекта** — статус 200 у него ничего не доказывает. Фактическая
> доступность проверяется загрузкой по `download_url` (как в таблице выше).
## Риски и восстановление
- ⛔ **Резервные копии zip не делались** — по прямому указанию «стереть физически»
(плюс канал до S3 из локальной сети медленный). Восстановление возможно **только
пересборкой** нужной версии из git-истории пакета;
`download_url`/`SHA256SUMS` удалённых версий не сохранялись.
- Пользователи, закрепившие в dev-стендах версии `< 2.0.21`, получат 404 при `terraform init`
и должны перейти на `2.0.21+`.
- `test` и `prod` не затронуты.
@@ -0,0 +1,60 @@
# 2026-09-30 — Перезаливка провайдера во все три стенда под версиями 1.0.0 / 2.0.0 / 3.0.0
> Команда владельца: «надо — чтобы в 1.0.0 2.0.0 3.0.0 стали НОВЫЕ провайдеры…
> ПОХУЙ на пользователей! ПОХУЙ на старые версии!!! генери всё новое и ЗАЛИВАЙ».
## Что сделано
Полный цикл по каждому стенду: перегенерация (`01` YAML → `02` Go+доки) и
сборка+подпись+заливка (`03`) — всё одной командой `03` (она сама вызывает `01` и `02`).
| Стенд | Namespace | Версия | `VERSION` в profile.env | Результат |
|---|---|---|---|---|
| prod | `nubes` | `1.0.0` | `1.0.0` (без изменений) | `Done. Version 1.0.0 uploaded.` |
| dev | `nubes-dev` | `2.0.0` | `2.0.24` → **`2.0.0`** | `Done. Version 2.0.0 uploaded.` |
| test | `nubes-test` | `3.0.0` | `3.0.0` (без изменений) | `Done. Version 3.0.0 uploaded.` |
Команды:
```bash
export MC_CONFIG_DIR=/tmp/mc-cfg
./TOOLS/scripts/03_build_and_upload_provider.sh --profile TOOLS/config/dev 2.0.0
./TOOLS/scripts/03_build_and_upload_provider.sh --profile TOOLS/config/test 3.0.0
./TOOLS/scripts/03_build_and_upload_provider.sh --profile TOOLS/config/prod 1.0.0
```
Что делает `03`: `01` (YAML-спеки стенда) → `02` (Go-ресурсы + доки) → сборка
3 платформ (`linux/windows/darwin amd64`, `CGO_ENABLED=0`, `-ldflags=-X main.version=…`)
→ `terraform-provider-nubes_<v>_SHA256SUMS` → GPG-подпись (`secrets/private_key.asc`,
`CB3A0DF161ECC416`) → заливка 5 объектов в S3.
## Проверки после заливки
| Стенд | Версия | `linux/amd64` скачивание | sha256 залитого vs локальной сборки |
|---|---|---|---|
| dev | `2.0.0` | HTTP 200, 13 169 112 B | ✅ совпадает |
| test | `3.0.0` | HTTP 200, 13 095 908 B | ✅ совпадает |
| prod | `1.0.0` | HTTP 200, 13 103 414 B | ✅ совпадает |
Дополнительно:
- API `/versions`: `nubes-dev` → `['2.0.0','2.0.21','2.0.22','2.0.23','2.0.24']`,
`nubes-test` → `['3.0.0']`, `nubes` → `['1.0.0']`;
- в S3 у каждой версии ровно 5 объектов (3 zip + `SHA256SUMS` + `SHA256SUMS.sig`).
## ⚠️ Последствия (приняты владельцем сознательно)
- Версии `1.0.0`, `2.0.0`, `3.0.0` **перезаписаны** — под теми же номерами теперь
другие бинарники. У всех, у кого есть `.terraform.lock.hcl`, будет
`checksum mismatch` при `terraform init`; лечится `terraform init -upgrade`.
- Старые версии **не удалялись** (кроме ранее вычищенного dev `< 2.0.21`):
в dev остаются `2.0.21`–`2.0.24`, в test `3.0.0` и в prod `1.0.0` — теперь уже как
свежие сборки.
- `VERSION` в `TOOLS/config/dev/profile.env` понижен `2.0.24` → `2.0.0`
(чтобы доки и артефакты генерировались с новой версией); закоммичено.
## Связанные документы
- `VERSIONS.md` — обновлённая таблица текущих версий.
- `HISTORY/2026-09-30_dev_registry_prune_versions.md` — предыдущая очистка dev-реестра.
- `HISTORY/2026-09-30_yaml_pipeline_hardening.md` — состояние пайплайна генерации.
@@ -0,0 +1,130 @@
# 2026-09-30 — Регистр UUID: нормализация на ОТПРАВКЕ в API (create/modify/redeploy)
> Разбор: `docs/60_strategy/terraform_case_sensitivity_fix.md` §11 (главный документ по теме),
> `NOTES/30_analysis/ARCHITECTURE_NEW.md` §6.5.
## Что обнаружилось
Костыль `lower(...)` в конфиге стенда Штурвала — **не «просто проще», а обязателен**.
Без него `terraform apply` (create кластера) падает: платформа отвечает
«Edge не развёрнут в указанном vDC».
Обнаружено при запуске Terraform **из-под Windows**, на провайдере **2.0.23**
(то есть после всех «фиксов регистра», выпущенных 24.09).
Костыль живёт в примере (и в gitea `Nail/tf_examples`):
```hcl
# tf_examples/fullpipe_chain/shturval.tf:139-140
vdc_uid = lower(nubes_vc_vdc.vdc.id)
nsxt_uid = lower(nubes_vc_nsxt.edge.id)
```
## Почему прошлые фиксы не помогли (главная мысль)
Провайдер `2.0.23` нормализует регистр UUID **только при СРАВНЕНИИ**:
план vs state, adopt/suspend/resume, modifier-compare, диагностика
(`jsonutil.LowercaseUUIDsInText` → `JSONStringsEquivalent`, `JsonNormalize()`,
`ParamsMatchForResume`, `normalizeCompareValue`).
**Путь ОТПРАВКИ в API остался без нормализации.** Все map-fixed JSON-параметры
собираются одной функцией `resources_core.BuildJSON`
(`provider/internal/resources_core/helpers.go`), а её вызывает сгенерированный код
(`NestedJSONExpr`, шаблон `TOOLS/resource-generator/internal/templates/instance.go`,
ветки Create / Modify / Redeploy). `BuildJSON` берёт `ValueString()` подполей **как есть**.
Ресурс `nubes_vc_nsxt` отдаёт `id` в UPPERCASE (`2C37FED1-…`), платформа хранит
UUID в lowercase и **сравнивает регистр при create** → `startupConfiguration.nsxtUid`
в верхнем регистре отвергается.
Почему не спас `resolveRefSvcParamValues` (`core/refsvc.go`,
`core/refsvc_resolve.go`): он нормализует только **top-level** refSvc-параметры и
`s3.*uid` **внутри** map-fixed. `vdcUid`/`nsxtUid` — обычные строковые подполя
JSON, refSvcId у них нет, под шаблон `s3.*uid` они не подпадают.
## Что сделано
| Файл | Изменение |
|---|---|
| `provider/internal/resources_core/helpers.go` | `BuildJSON` оборачивает результат в `jsonutil.LowercaseUUIDsInText(...)` (+ импорт `core/jsonutil`, комментарий-обоснование) |
Одна точка → покрыты **все** map-fixed-параметры всех ресурсов на
create / modify / redeploy (19 сгенерированных ресурсов, `resources_gen/`).
Регенерация не требуется (логика сериализации одна).
## Оценка риска (почему это безопасно)
- `BuildJSON` используется **только для отправки** в API, не для построения state.
- Regex `uuidAnywhereRegex` = `[0-9a-f]{8}-xxxx-xxxx-xxxx-xxxxxxxxxxxx` — совпадает
только с UUID; пароли/имена/произвольные строки не задевает.
- Проверено по спекам: внутри map-fixed **нет** строковых секретных полей
(password/secret/token) — только `*Uid`-ссылки на ресурсы.
- Это **выравнивание** с уже принятым в провайдере правилом «регистр UUID незначим»
(то же приведение уже делается на сравнении), а не новое поведение.
Остаточный риск: если в map-fixed когда-нибудь появится строковое поле, где
пользователь хранит **свой** UUID, и регистр там семантически важен (не ссылка на
ресурс) — он будет приведён к lowercase. Сейчас таких полей нет.
## Следствия
- `lower(...)` в HCL становится **не нужен** — убирать в конфигах и в примере
(отдельной командой, после релиза провайдера).
- **Выпущено 30.09.2026: dev `2.0.24`** — собрано (linux/windows/darwin amd64),
подписано GPG и залито в реестр (`nubes-dev/nubes/2.0.24/`), версия видна
в `/v1/providers/nubes-dev/nubes/versions`. `VERSIONS.md` обновлён.
Доки в реестр (шаг `04_build_and_publish_docs.sh`) **не публиковались**.
- В локальных стендах костыля нет: `DEV_STAND/FPipeGmail/shturval.tf:125-126` и
`DEV_STAND/FullPipe/shturval.tf1:123` передают `nubes_vc_vdc.vdc.id` /
`nubes_vc_nsxt.edge.id` напрямую → на create у них тот же риск.
## Процедура релиза (dev) — воспроизводимо (проверено 30.09.2026)
1. Поднять `VERSION` в `TOOLS/config/dev/profile.env` и закоммитить
(иначе доки генерируются со старой версией).
2. Собрать и залить:
```bash
./TOOLS/scripts/03_build_and_upload_provider.sh --profile TOOLS/config/dev 2.0.24
```
Скрипт сам выполняет шаги `01` + `02`, собирает 3 платформы
(linux/windows/darwin amd64), подписывает GPG и заливает в S3.
3. Проверить публикацию:
```bash
curl -s https://tf-registry.containerk8s.services.ngcloud.ru/v1/providers/nubes-dev/nubes/versions
```
4. Обновить `VERSIONS.md` и закоммитить.
5. Для локального `go build`/`go test`: `02_generate_resources_and_docs_v2.sh --profile TOOLS/config/dev`
и `TOOLS/scripts/dev-materialize.sh dev` (эфемерная копия в `provider/`).
⚠️ Доки в реестр (шаг `04_build_and_publish_docs.sh`) в этом релизе **не публиковались**.
## Где нормализация нужна (карта, чтобы не потерять)
Нормализация регистра UUID нужна в **двух независимых местах**:
1. **Сравнение** (план ↔ state, adopt, suspend/resume, modifier-compare, диагностика):
`jsonutil.LowercaseUUIDsInText` → `JSONStringsEquivalent`, `JsonNormalize()`,
`ParamsMatchForResume`, `normalizeCompareValue`.
2. **Отправка в API** — единственная точка `resources_core.BuildJSON`
(`provider/internal/resources_core/helpers.go`), вызывается сгенерированным кодом
через `NestedJSONExpr` (`TOOLS/resource-generator/internal/templates/instance.go`:
Create ~302, Modify ~488, Redeploy ~505).
Правила:
- ⛔ `lower(...)` в HCL — костыль, а не решение (был нужен только из-за ненормализованной отправки).
- ⛔ Не нормализовать план целиком (скаляры→строки, сортировка ключей) — вечный diff;
менять только регистр UUID-подстрок.
- `resolveRefSvcParamValues` (`core/refsvc.go`) покрывает только top-level `refSvcId`
и `s3.*uid` внутри map-fixed; `vdcUid`/`nsxtUid` — нет.
- Спеки map-fixed без строковых секретов (только `*Uid`) → regex `uuidAnywhereRegex` безопасен.
## Открытые вопросы (не закрыты)
1. Проверить на живом стенде: create кластера Штурвала **без** `lower(...)` на сборке
с этим фиксом — `apply` запускает только пользователь.
2. `core/params.go` → `normalizeUniversalValueV6`: скалярные UUID, попадающие в
дефолты create (`instance_create.go`) и в досылку modify (`operation_run.go`),
к lowercase не приводятся (вторично, нужен замер).
3. `resources_core/ref_validation.go` (`ValidateRefParamsOnAdopt`): ref-параметр
внутри JSON не валидируется при adopt (открыто с 24.09).
@@ -0,0 +1,258 @@
# 2026-09-30 — Ужесточение пайплайна генерации YAML (безопасная замена, чистка легаси)
> Связанные материалы:
> `TOOLS/README.md` (канонический пайплайн, порядок шагов),
> `TOOLS/ARCHITECTURE.md` (спецификация),
> память репозитория: `pipeline-legacy.md`.
## Задача
1. Разобрать, что в генерации YAML устарело.
2. Перегенерировать YAML по всем стендам так, чтобы **старое удалялось безопасно**,
а новое создавалось атомарно.
3. Задокументировать всё, чтобы история изменений прослеживалась.
Команда пользователя: «делай как ПОЛОЖЕНО, как в best practices».
## Что было не так (до правок)
### `TOOLS/scripts/01_generate_yamls.sh`
| Место (до) | Проблема |
|---|---|
| стр. 230 `rm -f "$output_glob"` | старый YAML удалялся **до** генерации → при сбое API файл исчезал, новый не создавался (неатомарно per-service) |
| стр. 122 «Полной очистки нет» | YAML исключённого сервиса оставался в каталоге и попадал в сборку |
| стр. 91 `ls -t "${ROOT_DIR}"/*.token` | легаси-фолбэк токена: в корне токенов нет; поиск «последнего» мог подхватить чужой токен |
| стр. 109 `API_ENDPOINT="${NUBES_API_ENDPOINT:-https://lk-api-gateway.ngcloud.ru/...}"` | молчаливый уход в **PROD**, если в профиле нет endpoint |
| стр. 167 `svc_name=""` | имя всегда пустое, хотя шапка обещала парсинг из списка → лишний HTTP-запрос на каждый сервис (2 запроса вместо 1) |
| стр. 46–50 | дубль `SERVICES_FILE_DEFAULT` (одинаковое присваивание в `if`) |
| шапка vs код | «REQUEST_DELAY по умолчанию 0.2», в коде 0.5; путь вывода указан как `provider/resources_yaml` (устарел) |
### `TOOLS/yaml-generator/internal/config/config.go`
| Место (до) | Проблема |
|---|---|
| `Load()` стр. 33 | свой дефолт endpoint = **PROD** gateway |
| `loadToken()` + `findLatestToken()` | легаси-фолбэк «последний `*.token` в корне репо» |
| `Load()` стр. 60 | путь `filepath.Join(repoRoot, "devops", "config", "services_list.txt")`, причём `FindRepoRoot()` возвращает каталог `provider/` → путь заведомо не существовал |
### Легаси-скрипты
`10_yaml_stability_run.sh`, `11_yaml_stability_run_latest.sh`,
`12_generate_yamls_latest.sh`, `13_generate_yamls_clean.sh`,
`02_generate_resources_and_docs_template_v2.sh` — **мертвы**: зовут `01` без
`--profile` (→ `exit 2`), ищут `*.token` в корне репо, а `13` вдобавок делал
`rm -f provider/resources_yaml/*.yaml`. Ни один рабочий скрипт их не вызывает
(ссылки есть только в исторических `HISTORY/`, `NOTES/`).
## Что сделано
### 1. `01_generate_yamls.sh` — безопасная запись по принципу staging → атомарная замена
Новый алгоритм:
```
staging = generated/<stand>/resources_yaml.staging.<pid>
↓ генерация всех сервисов пачкой per-service в staging
↓ при пустом failures:
mv resources_yaml → resources_yaml.bak-<UTC> (бэкап, ротация KEEP_BACKUPS=5)
mv staging → resources_yaml (атомарный rename в том же FS)
↓ при непустом failures:
замена ОТМЕНЯЕТСЯ, рабочий каталог не тронут, staging оставлен для разбора, exit 1
```
Прочие изменения:
- каталог помечается маркером `.stand`; генерация в каталог чужого стенда запрещена (`exit 2`);
- `embed.go` создаётся теперь в staging (обязательный `go:embed *.yaml`);
- `NUBES_API_ENDPOINT` обязателен, иначе `exit 2`;
- токен берётся только из `NUBES_API_TOKEN`/`TOKEN_FILE`; легаси-поиск удалён;
- имя сервиса — из 2-го поля `services_list.txt`; лишний python-запрос удалён;
- удалён дубль `SERVICES_FILE_DEFAULT`; синхронизированы комментарии.
### 2. `yaml-generator/internal/config/config.go`
- `NUBES_API_ENDPOINT` обязателен (нет PROD-дефолта);
- `loadToken()` больше не ищет `*.token` в корне репо; `findLatestToken` и `getenvDefault` удалены как мёртвые;
- при отсутствии `NUBES_SERVICE_ID` требуется явный `NUBES_SERVICES_FILE` (угадывание пути удалено).
### 3. Легаси-скрипты отключены (fail-fast)
В начало каждого добавлен guard: сообщение `DEPRECATED` + `exit 2`. Файлы **не удалены**
(удаление — отдельное решение владельца), но теперь они не могут сделать ничего вредного.
### 4. Документация
- `TOOLS/README.md` — добавлен раздел «Канонический пайплайн (порядок шагов)» и описание безопасной генерации;
- `TOOLS/ARCHITECTURE.md` — ссылки `devops/…` заменены на `TOOLS/config/<stand>/…`;
- память репозитория — `pipeline-legacy.md` уточнена.
## Прогон по всем стендам (результат)
Токены проверены прямыми запросами к API (с браузерным `User-Agent`, иначе DDoS-Guard отдаёт 403):
| Стенд | Endpoint | HTTP | YAML после генерации | Stale | Failures |
|---|---|---|---|---|---|
| dev | `lk-api-gateway-dev.ngcloud.ru` | 200 | 40 | нет | нет |
| test | `lk-api-gateway-test.ngcloud.ru` | 200 | 36 | нет | нет |
| prod | `lk-api-gateway.ngcloud.ru` | 200 | 35 | нет | нет |
Команды:
```bash
./TOOLS/scripts/01_generate_yamls.sh --profile TOOLS/config/dev
./TOOLS/scripts/01_generate_yamls.sh --profile TOOLS/config/test
./TOOLS/scripts/01_generate_yamls.sh --profile TOOLS/config/prod
```
Проверка целостности: `diff -rq` нового каталога dev с бэкапом даёт различия только
в случайных `default`-суффиксах, которые API генерирует при каждом запросе
(`db-ievgpdvu` → `db-ujama5rb`, `flask-seqtiq3t` → `flask-xwfdxdqh` и т.п.) —
структурной регрессии нет. Это же объясняет, почему побайтовое сравнение двух
прогонов не может быть использовано как «детектор дрейфа».
## Коммиты
- `12b3932` — `fix(tools): безопасная генерация YAML — staging + атомарная замена, без легаси-фолбэков`
- `ad4daab` — `chore(tools): легаси-скрипты генерации отключены (fail-fast DEPRECATED)`
## Проверки
- `bash -n` для `01_generate_yamls.sh` и всех guard-скриптов — OK;
- `go vet ./...` + `go build` для `yaml-generator` — OK;
- guard отдаёт `exit 2`;
- прогон dev/test/prod — 0 failures, stale отсутствует, staging не остаётся;
- бэкапы создаются: `generated/dev/resources_yaml.bak-20260930T063152Z` и т.д.
## Этап 2 — удаление мёртвого (по команде «удаляй всё старое, аккуратно»)
**Удалено (`1e796c8`)** — 100% мёртвый код/данные, ничего их не вызывает:
| Файл | Почему удалён |
|---|---|
| `TOOLS/scripts/10_yaml_stability_run.sh` | зовёт `01` без `--profile` (exit 2), ищет `*.token` в корне |
| `TOOLS/scripts/11_yaml_stability_run_latest.sh` | цепочка на `10`, та же поломка |
| `TOOLS/scripts/12_generate_yamls_latest.sh` | зовёт `01` без `--profile`, ищет `*.token` в корне |
| `TOOLS/scripts/13_generate_yamls_clean.sh` | цепочка на `12` + делал `rm -f provider/resources_yaml/*.yaml` |
| `TOOLS/scripts/02_generate_resources_and_docs_template_v2.sh` | легаси-дубль канонического `02_generate_resources_and_docs_v2.sh` |
| `TOOLS/config/services_list.txt` (общий) | код его не читает; как «объединение» устарел: активный `27` (в test/prod — «нет в UI»), нет `87/88/97/153`, которые есть в dev |
Проверка «ничего не вызывает»: `grep` по всему репо находил ссылки только в
исторических `HISTORY/`, `NOTES/`, `docs/` (не исполняются).
**Правки ссылок (`c822ae2`)**: `README.md`, `HOW_TO/README.md`,
`HOW_TO/DEVOPS_BUILD_PIPELINE.md`, `HOW_TO/HOWTO_ADD_NEW_SERVICE.md` (включая
переписанный блок «Быстрый старт» с `devops/` на `./TOOLS/scripts/*`),
`DOCS_PIPELINE/README.md`, `scripts/publish-doc-page.sh`, `.gitignore`.
**Проверка после удаления**: `bash -n` для всех `TOOLS/scripts/*.sh` и
`scripts/publish-doc-page.sh` — OK; smoke-прогон `01 --profile TOOLS/config/dev` —
40 YAML, замена атомарная, бэкап создан.
**Не удалено (осознанно):**
- поддержка легаси-прокси `index.cfm` в `01` и `yaml-generator` — это совместимость
с работающими пользователями старого API (провайдер v5.0.75, `secrets/stands.md`);
- `DOCS_PIPELINE/publish-docs.sh` — сам файл помечен «справочная копия, не подменяет пайплайн»;
- `HISTORY/`, `NOTES/`, `docs/` — исторические документы (в них `devops/` и легаси-скрипты
упоминаются как история, это нормально);
- `scripts/*.py` и `s3_notification_example.sh` — ручные утилиты, вызываются вручную.
## Этап 3 — сверка списков стендов с облачным каталогом (источник истины)
Принято: **истина — то, что перечислено в облаке**. Определяется эндпоинтом каталога:
```bash
# «перечислено в облаке» (продакшен-готовые сервисы стенда)
GET {NUBES_API_ENDPOINT}/services?limit=200&isProductionReady=true
# для сравнения: без фильтра отдаются ВСЕ сервисы платформы, включая
# DEPRECATED и не заявленные в каталоге (48 у prod, 49 у test, 60 у dev)
```
Требуется браузерный `User-Agent` (иначе DDoS-Guard отдаёт 403) и `Referer`.
Результат на 2026-09-30:
| Стенд | Облако (`isProductionReady=true`) | Активных в `services_list.txt` | Лишние в файле | Не хватало |
|---|---|---|---|---|
| dev | 40 | 40 | нет | нет |
| test | 36 | 36 | нет | нет |
| prod | 36 | 35 → **36** | нет | **`151 k8sOpenbao` (Vault)** |
У остальных 12 закомментированных prod-сервисов, присутствующих в API, `isProductionReady=false` —
они закомментированы обоснованно. Четыре id в файле отсутствуют в каталоге prod вовсе
(`32 vmpostgre`, `87 k8svalkey`, `153 nifi`, `175 k8sGo`).
Исправлено коммитом `a68a36a`: `151 k8sOpenbao` раскомментирован (комментарий «нет в PROD UI»
устарел), prod перегенерирован — 36 YAML, ровно как в облаке.
## Этап 4 — перепроверка после полной перегенерации + подводные камни
Команда: «сгенери YAML для всех стендов, проследи чтобы старого ничего не осталось,
перепроверь после генерации всё». Выполнено три прогона `01`:
```bash
for s in dev test prod; do ./TOOLS/scripts/01_generate_yamls.sh --profile TOOLS/config/$s; done
# все три: exit=0
```
### Результат перепроверки (2026-09-30)
| Стенд | YAML | = активных в списке | = облако (`isProductionReady=true`) | Stale | Дубли id | Failures |
|---|---|---|---|---|---|---|
| dev | 40 | ✅ 40 | ✅ 40 | нет | нет | пусто |
| test | 36 | ✅ 36 | ✅ 36 | нет | нет | пусто |
| prod | 36 | ✅ 36 | ✅ 36 | нет | нет | пусто |
Дополнительно проверено:
- staging-каталоги (`resources_yaml.staging.*`) — не осталось ни одного;
- в `resources_yaml/` только `*.yaml`, `.stand`, `embed.go` — посторонних файлов нет;
- `.stand` в каждом каталоге совпадает с профилем (`dev`/`test`/`prod`);
- бэкапы прошлых версий: dev 3, test 2, prod 2 (ротация `KEEP_BACKUPS=5`);
- `generated/<стенд>/tmp/yaml_gen_failures.txt` — пусты;
- `git status` — чисто.
### ⛔ Подводные камни, найденные при перепроверке (важно на будущее)
1. **API отдаёт случайные `default`.** Часть параметров приходит со случайным
суффиксом (`db-ievgpdvu` → `db-ujama5rb`, `kvname-grzjes7l` → `kvname-g3s0uof2`,
`flask-seqtiq3t` → `flask-xwfdxdqh`). Поэтому **побайтовое сравнение двух прогонов
не является детектором дрейфа** — различия в этих строках не регрессия.
2. **Случайный `default` вшивается в сгенерированный Go-код.**
Пример: `generated/dev/go/151_k8s_openbao_kv_resource.go` содержит
`Default: stringdefault.StaticString("kvname-XXXX")`. Следствие:
`check_generated_drift.sh dev` показывает **дрейф 15 файлов сразу после любой**
перегенерации YAML — это не ошибка оператора.
3. **Производные артефакты стареют молча.** `generated/<стенд>/go` и `generated/<стенд>/docs`
создаются шагом `02` и после нового `01` становятся старше своих источников
(на момент проверки: `go`/`docs` dev — 08:46, YAML dev — 10:18). Отдельно живёт
эфемерная копия `provider/internal/resources_gen` + `provider/resources_yaml`
(её кладёт `dev-materialize.sh`, маркер `.stand` = стенд). Их нужно обновлять
шагом `02` после каждого `01`.
4. **Прямые HTTP-запросы к API без браузерного `User-Agent` получают 403**
(DDoS-Guard). С `User-Agent` + `Referer` — 200.
### Актуальная карта пайплайна на 2026-09-30
- Единственный путь генерации YAML: `TOOLS/scripts/01_generate_yamls.sh --profile TOOLS/config/<стенд>`
(`--profile` обязателен, без него `exit 2`).
- Один универсальный движок на все стенды: `TOOLS/bin/yaml-generator`, стенд задаётся
переменными окружения (`NUBES_API_ENDPOINT`, `NUBES_API_TOKEN`, `NUBES_SERVICE_ID`,
`NUBES_SERVICE_NAME`, `NUBES_OUTPUT_DIR`); список сервисов — свой у каждого стенда.
- Стенд-специфичных хардкодов в коде нет — контролируется `check_hardcoded_service_ids.sh`.
- Токены: `secrets/{dev,test,prod}.token` (валидны на 2026-09-30, срок до 2026-12-27);
обновление — `TOOLS/scripts/00_token_manager.sh` (keycloak refresh, `THRESHOLD_MIN=10`).
- Удалены как мёртвые (`1e796c8`): `10/11/12/13_yaml_*.sh`,
`02_generate_resources_and_docs_template_v2.sh`, общий `TOOLS/config/services_list.txt`.
- Оставлены осознанно: поддержка легаси-прокси `index.cfm`, справочная копия
`DOCS_PIPELINE/publish-docs.sh`, исторические `HISTORY/`/`NOTES/`/`docs/`,
ручные утилиты `scripts/*.py`.
## Открытые вопросы (на решение владельца)
1. Поддержка легаси-прокси `index.cfm`: оставляем или выпиливаем (README уже помечает
закрытые API как «не использовать»)?
2. Прочие `.gitignore`-паттерны мёртвых каталогов (`universal_rebuild/*`, `provider/generated/`)
— чистить?
@@ -0,0 +1,933 @@
# Диалог: Opus 4.8 — анализ архитектуры провайдера и ресурсов-модификаторов (2026-09-30)
> **Что это.** Полная, без сокращений, запись диалога: постановка задачи пользователем,
> ход работы агента (GitHub Copilot) до создания файла-промпта, содержимое промпта,
> ответ Opus 4.8 (приведён дословно в том виде, как получен) и открытый вопрос Opus.
> **Формат.** Хронология ходов. Реплики пользователя и ответ Opus — дословно, включая опечатки.
> **Статус.** Диалог НЕ завершён: Opus задал вопрос, ответ ему ещё не передан.
---
## Ход 1 — Задание пользователя (дословно)
```
составь промпт в файл для Опус 4.8
пусть проанализирует архитектуру провайдера ... с дополнительными ресурсами-модификаторами
ограничь зону его доступносьти - пусть НИЧЕГО не читает из хистори, только файл с архитектурой
и точно выдай ему список файлов которые надо изучить, более пусть никуда не лезет
если что непончятно - пусть напишет тебе вопрос, я тебе передам
взаимодейтвуйте в редиме диалога, НО НЕ НАДО злоупотреблять !
ответы от него - МАКСИМАЛЬНО СЖАТО, без лирики и воды, лишь бы ты сам как агент понял
```
Контекст задания: активный файл — `DEV_STAND/FullPipe/shturval.tf1`; активное выделение — строка 117
(`suspend_on_destroy`).
---
## Ход 2 — Работа агента до создания файла (разведка, только чтение)
Поиск архитектурных документов и существующих промптов для Opus:
- `NOTES/20_prompts/` — 20 файлов промптов; среди них `prompt_for_opus_modifier_architecture_full.md`,
`prompt_for_opus_modifiers_review.md`, `prompt_for_opus_review_modify_resources_2026-09-24.md`,
`prompt_for_opus_modifier_global_architecture.md`. Часть помечена баннером ⛔ «ЛОЖНЫЙ ПУТЬ — ОТМЕНЕНО».
- `HISTORY/OPUS/` — 16 файлов диалогов с Opus (напр. `2026-09-22_modifier_architecture_project.md`).
- Кандидаты на «файл архитектуры»: `TOOLS/ARCHITECTURE.md`, `NOTES/30_analysis/ARCHITECTURE_NEW.md`,
`docs/60_strategy/provider_philosophy.md`, `docs/60_strategy/modifier_resources_ideology_and_specification.md`.
Прочитано:
- `TOOLS/ARCHITECTURE.md` (246 строк) — в первых строках помечен «PRIMARY SOURCE OF TRUTH»,
«THIS FILE IS THE FOUNDATION».
- `NOTES/30_analysis/ARCHITECTURE_NEW.md` — «Universal Rebuild — Архитектура и рабочая цепочка»
(внутри — ссылки на пути `/home/naeel/terra/universal_rebuild`, т.е. иная раскладка репозитория).
- `NOTES/20_prompts/prompt_for_opus_modifier_architecture_full.md`, `…_modifiers_review.md`,
`…_review_modify_resources_2026-09-24.md`.
- `NOTES/40_chat_summaries/CHAT_RESUME_IAC_2026-09-24.md` — карта файлов, разделение «актуально / LEGACY».
Проверка актуальности через git:
```
git log -3 -- TOOLS/ARCHITECTURE.md
cbd559d 2026-09-30 docs(tools): канонический пайплайн + история ужесточения генерации YAML
78f9dfb 2026-09-22 refactor(build): эфемерные generated-копии …
c14f7de 2026-09-22 docs: раздел «Реестр исключений» + диалог код-ревью opus/astra
```
Установленная фактическая реализация модификаторов (на 2026-09-30):
- База: YAML на сервис — «чистая выгрузка» из API, без доменных меток. В `TOOLS/yaml-generator/main.go`
(комментарий перед `CollectOperations`) прямо сказано: реестр `serviceSpecificModifiers`, ранее
помечавший `modify` как `kind=modifier`, удалён при рефакторинге 2026-09-23; модификаторы теперь —
«отдельный оверлей (см. `modifiers.yaml` + второй проход)», базовый YAML о них не знает.
- Реальные модификаторы — РУЧНЫЕ Go-ресурсы:
- `provider/internal/resources_core/org_ip_allocation_resource.go` —
`nubes_vc_org_ip_allocation` (сервис 19, `modify`, `vIPConfigure` id 662);
- `provider/internal/resources_core/nsxt_snat_resource.go` —
`nubes_vc_nsxt_snat` (сервис 22, `modify`, `ipSpaceName` id 372; канон-значение `no-needed`).
- Регистрация: `provider/internal/provider/provider.go`, функция `Resources()` (строки 182–190) —
`append(resources, resources_core.NewOrgIpAllocationResource, resources_core.NewNsxtSnatResource)`.
- Вызываемые методы ядра: `RunInstanceOperationUniversalByCode` (`provider/internal/core/operation_run_bycode.go`),
`GetInstanceStateParams` (`provider/internal/core/instance_params.go`),
`LockInstance`/`UniversalClient` (`provider/internal/core/client.go`),
`ResolveRefSvcParamValue` (`provider/internal/core/refsvc_resolve.go`),
`ShouldRemoveFromState` (`provider/internal/resources_core/resource_diagnostics.go`).
- Спеки операций/параметров: `generated/dev/resources_yaml/19_vc_org.yaml`, `22_vc_nsxt.yaml`.
- Пример применения цепочки: `DEV_STAND/FullPipe/modifiers.tf`.
Агент задал пользователю 3 уточняющих вопроса (какой файл считать «архитектурой»; что именно должен
выдать Opus; имя файла промпта) — **пользователь вызов отклонил (cancelled)**.
---
## Ход 3 — Ответ пользователя (дословно)
```
думай сам ! возможно несколько файлов ему дать
```
---
## Ход 4 — Решение агента и создание файла
Агентом принято самостоятельно (без дополнительных вопросов):
- «Файл с архитектурой» — единственный: `TOOLS/ARCHITECTURE.md`.
- Формат выхода Opus — архитектурный разбор: устройство по слоям, спека ↔ код, жизненный цикл
модификаторов, риски с указанием `файл:строка`.
- Путь промпта — `NOTES/20_prompts/prompt_for_opus_provider_architecture_modifiers.md`
(соответствует конвенции папки `NOTES/20_prompts/`).
Проверка существования всех 22 путей из списка (скрипт `test -f` по каждому) — результат:
`ALL FILES PRESENT`. Отсутствующих путей нет.
Коммит: `752244f` —
`docs(prompt): промпт для Opus 4.8 — анализ архитектуры провайдера + ресурсов-модификаторов`.
### Содержимое созданного файла `NOTES/20_prompts/prompt_for_opus_provider_architecture_modifiers.md` (дословно)
```markdown
# Промпт для Opus 4.8 — анализ архитектуры провайдера + ресурсов-модификаторов
**Репо:** `/home/naeel/TF/tf_provider` — `terraform-provider-nubes` (Go, Terraform Plugin Framework).
**Дата:** 2026-09-30. **Стенд-источник фактов:** `dev`.
---
## Роль и режим работы
- Ты — архитектор/ревьюер универсального Terraform-провайдера.
- Работаем **в диалоге**: я (агент) передаю твои вопросы пользователю и возвращаю его ответы.
- Вопрос задавай ТОЛЬКО если без него ответить нельзя. Максимум 1–2 вопроса за раз, предельно коротко.
- Не догадываться. Нет данных — вопрос, а не допущение.
- Область не расширять: отвечать ровно на поставленную задачу.
## Задача
Проанализировать архитектуру универсального провайдера Nubes и встроенный в неё слой
**ресурсов-модификаторов** — отдельных ресурсов, которые вызывают операцию `modify`
у родительского инстанса (когда нужного параметра нет в операции `create`).
Оценить:
1. Как устроена архитектура по слоям и как течёт поток данных (API → YAML → код → API).
2. Соответствие заявленной спеки (`TOOLS/ARCHITECTURE.md`) фактической реализации — все
расхождения, с указанием `файл:строка`.
3. Корректность жизненного цикла модификаторов: `Create` / `Read` / `Update` / `Delete`,
идемпотентность, дрейф (drift), поведение при `replace` / повторном `apply`, импорт.
4. Место модификаторов в универсальном ядре: где и как нарушается принцип
«ядро универсально, доменные знания — только данные». Насколько оправдано текущее
решение (ручные Go-ресурсы, зарегистрированные поверх генерируемых).
5. Границы ответственности: что модификатор делает сам, что отдаёт платформе; как
выражается обратная операция (откат при `destroy`, значение «выключено»).
6. Риски и топ-проблемы — по убыванию критичности, каждое с `файл:строка`.
## Границы доступа (ЖЁСТКО)
Читать РАЗРЕШЕНО **только** файлы из списка ниже. Всё остальное — ЗАПРЕЩЕНО, в частности:
- `HISTORY/**`, `NOTES/**`, `TMP/**`, `HAR/**`, `docs/**`, `site/**`, `site_test/**`,
`apps/**`, `charts/**`, `FIYR_MGU/**`, `gateway/**`, `scripts/**`, `secrets/**`,
`tfflaskcrud/**`, `tfluceecrud/**`, `tfnodejscrud/**`, `DEV_STAND/**` (кроме одного файла
из списка), `TEST_STAND/**`, `PROD_STAND/**`, `provider/artifacts/**`, `provider/bin/**`;
- история git (`git log`, `git show`, `git diff` с коммитами), коммиты, теги, ветки;
- любой файл репозитория, которого нет в списке ниже.
Нужен файл вне списка → НЕ читать, а задать мне вопрос.
## Файлы к изучению (исчерпывающий список)
### Группа 1. Архитектура (спека)
- `TOOLS/ARCHITECTURE.md`
### Группа 2. Ресурсы-модификаторы и их регистрация
- `provider/internal/provider/provider.go`
- `provider/internal/resources_core/org_ip_allocation_resource.go`
- `provider/internal/resources_core/nsxt_snat_resource.go`
- `provider/internal/resources_core/org_ip_allocation_test.go`
### Группа 3. Рантайм-зависимости модификаторов (ядро)
- `provider/internal/core/client.go`
- `provider/internal/core/operation_run_bycode.go`
- `provider/internal/core/instance_params.go`
- `provider/internal/core/refsvc_resolve.go`
- `provider/internal/resources_core/resource_diagnostics.go`
- `provider/internal/resources_core/crud.go`
### Группа 4. Генератор (как рождается «универсальная» часть)
- `TOOLS/yaml-generator/main.go`
- `TOOLS/resource-generator/internal/loader/loader.go`
### Группа 5. Факты API (спеки операций/параметров)
- `generated/dev/resources_yaml/19_vc_org.yaml`
- `generated/dev/resources_yaml/22_vc_nsxt.yaml`
### Группа 6. Применение модификаторов (композиция цепочки)
- `DEV_STAND/FullPipe/modifiers.tf`
### Группа 7. Только если без них нельзя ответить (иначе не открывать)
- `provider/internal/resources_core/state_refresh.go`
- `provider/internal/resources_core/params_compare.go`
- `provider/internal/resources_core/helpers.go`
- `TOOLS/resource-generator/main.go`
- `TOOLS/resource-generator/internal/templates/instance.go`
## Что нужно на выходе
Структурированный отчёт, разделы строго в этом порядке:
1. **Устройство архитектуры** — слои и поток данных, 5–10 строк.
2. **Спека ↔ код** — список расхождений `ARCHITECTURE.md` с реализацией (`файл:строка`).
3. **Дефекты и риски модификаторов** — по убыванию критичности. По каждому:
суть → место (`файл:строка`) → последствие → предлагаемое направление (одна строка).
4. **Открытые вопросы** — списком, если есть.
## Формат ответа
- Максимально сжато: тезисы, без вступлений, воды и «лирики».
- Каждое утверждение проверяемо: ссылка `файл:строка`.
- Код — только короткие фрагменты, и лишь если без них тезис не понятен.
- Никаких «а ещё могу», никаких предложений расширить работу.
## Правило «стоп»
Если задание неоднозначно или данных не хватает — **остановиться и задать один короткий
вопрос**. Не достраивать смысл и не действовать по догадке.
```
### Итоговое сообщение агента пользователю (дословно)
```
Готово.
`NOTES/20_prompts/prompt_for_opus_provider_architecture_modifiers.md` (коммит `752244f`).
Что внутри:
- **Задача** — анализ архитектуры провайдера + слоя модификаторов (`Create/Read/Update/Delete`, идемпотентность, drift, спека↔код, место в универсальном ядре, границы/откат, топ-рисков с `файл:строка`).
- **Границы (жёстко)** — запрет на `HISTORY/**`, `NOTES/**`, `TMP/**`, `HAR/**`, `docs/**`, стенды, git-историю; нужен файл вне списка → вопрос мне, не чтение.
- **Список файлов** — 22 шт., исчерпывающий, 7 групп: спека (`TOOLS/ARCHITECTURE.md`) → модификаторы + регистрация → рантайм-ядро → генератор → YAML-спеки 19/22 → пример (`DEV_STAND/FullPipe/modifiers.tf`) → «только при необходимости».
- **Формат ответа** — сжато, тезисы, каждое утверждение с `файл:строка`, без «лирики».
- **Режим** — диалог, но без злоупотребления; при неясности один короткий вопрос и стоп.
Все 22 пути проверены — существуют.
```
---
## Ход 5 — Ответ Opus 4.8 (дословно, как получен от пользователя)
Ниже — полный сырой лог сессии Opus (включая служебные строки о чтениях и неудачных поисках),
затем его отчёт.
```
Read
Прочитаю файлы строго по разрешённому списку. Начну с групп 1–3.
Read
Read
Прочитаю группу 2 (модификаторы) и группу 3 (ядро).
Read
Read
Прочитаю группу 3 (ядро) и тест.
Read
Read
Read
Read
Read
Read
Прочитаю `client.go` (ядро) и группу 4 (генератор).
Read
Прочитаю группу 4 (генератор), группу 6 (modifiers.tf) и начну группу 5.
Read
Read
Read
Прочитаю группу 5 (API YAML). Сначала проверю размер.
No matches found. Your search pattern might be excluded completely by either the search.exclude settings or .*ignore files.
If you believe that it should have results, you can check into the .*ignore files and the exclude setting (here are some excluded patterns for reference:[**/node_modules,**/bower_components,**/*.code-search]).
Then if you want to include those files you can call the tool again by setting "includeIgnoredFiles" to true.
No matches found. Your search pattern might be excluded completely by either the search.exclude settings or .*ignore files.
If you believe that it should have results, you can check into the .*ignore files and the exclude setting (here are some excluded patterns for reference:[**/node_modules,**/bower_components,**/*.code-search]).
Then if you want to include those files you can call the tool again by setting "includeIgnoredFiles" to true.
Searched for files matching `**/generated/dev/resources_yaml/*vc_org*.yaml`, no matches
Searched for files matching `**/generated/dev/resources_yaml/*nsxt*.yaml`, no matches
Searched for files matching `**/generated/dev/**/19_*.yaml`, no matches
Read
Searched for files matching `**/generated/dev/resources_yaml/*.yaml`, no matches
Read
Файлы существуют; grep не сработал по паттерну. Прочитаю их напрямую.
No matches found. Your search pattern might be excluded completely by either the search.exclude settings or .*ignore files.
If you believe that it should have results, you can check into the .*ignore files and the exclude setting (here are some excluded patterns for reference:[**/node_modules,**/bower_components,**/*.code-search]).
Then if you want to include those files you can call the tool again by setting "includeIgnoredFiles" to true.
No matches found. Your search pattern might be excluded completely by either the search.exclude settings or .*ignore files.
If you believe that it should have results, you can check into the .*ignore files and the exclude setting (here are some excluded patterns for reference:[**/node_modules,**/bower_components,**/*.code-search]).
Then if you want to include those files you can call the tool again by setting "includeIgnoredFiles" to true.
Read
Read
Проанализировал все файлы из разрешённого списка (группы 1–6; группа 7 не понадобилась). Отчёт ниже.
```
### Отчёт Opus (дословно)
```markdown
## 1. Устройство архитектуры
Поток данных: **API → YAML → Go-код → API**.
- Генератор YAML (`main.go`) делает **чистую выгрузку** операций сервиса из API (`create/delete/modify/suspend/...`) без доменных знаний — один файл на сервис (`19_vc_org.yaml`, `22_vc_nsxt.yaml`).
- Генератор кода (`loader.go`) читает YAML, классифицирует операции на `instance/subresource/action/modifier`, строит `GenResource/...` и рендерит универсальные ресурсы.
- Ядро (`core`) — универсальный HTTP-клиент: запуск операций, ожидание, резолв ref-параметров, досылка дефолтов.
- **Модификаторы** — это НЕ генерируемый слой: два ручных Go-ресурса (`org_ip_allocation_resource.go`, `nsxt_snat_resource.go`), зарегистрированных вручную в `provider.go:199-204`. Каждый дёргает `modify` родителя через `RunInstanceOperationUniversalByCode`.
## 2. Спека ↔ код (расхождения)
**S1. Реестр `serviceSpecificModifiers` в спеке указан не там, где он есть (его нет).**
`ARCHITECTURE.md` (таблица «Exception Registry») утверждает, что `serviceSpecificModifiers` живёт в `main.go`. Фактически этот реестр **удалён** (рефакторинг 2026-09-23) — прямо задокументировано в `main.go:70-90`. Названного реестра нет; модификаторы стали ручными ресурсами. Таблица-спека устарела.
**S2. «Service-specific Go code is fully generated… No manual edits» / «Core is universal» — нарушено.**
`ARCHITECTURE.md` (Core Principles 2, 3; «Provider Model»). Но `org_ip_allocation_resource.go` и `nsxt_snat_resource.go` — ручной сервис-специфичный Go с зашитыми `service_id`, именами параметров и канон-значениями, лежащий в слое `resources_core`. Это доменная логика внутри «ядра».
**S3. Заявленные «ровно ДВА реестра исключений» — по факту есть третье место.**
`ARCHITECTURE.md` («The ONLY allowed deviations… MUST live in exactly two named registries»). Третье место доменных данных/логики — `provider.go:199-204` + два файла модификаторов. Оно не покрыто правилом реестра и не отражается в его диффе.
**S4. «modify всегда через WithDefaults» — модификаторы идут через ByCode.**
`ARCHITECTURE.md` («modify всегда через WithDefaults / `RunInstanceOperationUniversalWithDefaults`»). Модификаторы намеренно используют `RunInstanceOperationUniversalByCode` (`operation_run_bycode.go:10-13`) с причиной в комментарии `nsxt_snat_resource.go:240-250`. Досылка дефолтов там своя (live → paramValue формы → default, `operation_run_bycode.go:105-150`). Буквальное утверждение спеки не выполняется.
**S5. Полноценный генерируемый слой модификаторов существует, но не задействован.**
`loader.go:55-90` полностью поддерживает `kind: modifier` с `delete_strategy` (`noop_warn/inverse/error`), `idempotency` (`none/check_before_run`), `delete_params`, валидацией (validateModifierOperation). Ядро под это имеет `RunInstanceOperationUniversalByIdempotent` (`operation_run_bycode.go:15-19`) и `RunOperationByCodeIdempotent` (`crud.go`). Но оба реальных модификатора — ручные и это всё **не используют**, переизобретая delete-стратегию вручную (`keep_on_destroy` + inverse). Базовые YAML (19, 22) `kind: modifier` не содержат — оверлей `modifiers.yaml`, упомянутый в `main.go:84-90`, в разрешённом списке отсутствует и в базовых спеках не проявлен.
## 3. Дефекты и риски модификаторов (по убыванию критичности)
**R1. Двойное владение одним и тем же параметром API.**
Суть: `vIPConfigure` (id 662) есть в `modify` генерируемого `nubes_vc_org` (`19_vc_org.yaml`, op modify), а `ipSpaceName` (id 372) — в `modify` генерируемого `nubes_vc_nsxt` (`22_vc_nsxt.yaml`). Те же поля пишет и модификатор.
Место: `org_ip_allocation_resource.go:316-340` / `nsxt_snat_resource.go:240-253`.
Последствие: если пользователь заводит и инстанс-ресурс, и модификатор — «война дрейфов»: два ресурса по очереди перезаписывают поле каждым apply.
Направление: явно исключать пересекающиеся коды из схемы генерируемого ресурса, если поле отдано модификатору (или запретить одновременное использование).
**R2. Зашитые сервис-специфичные данные обходят страж `check_hardcoded_service_ids.sh`.**
Суть: id 19/22, имена `vIPConfigure`/`ipSpaceName`, значение `"no-needed"` зашиты как литералы-аргументы, а не как `svc.ID == N`.
Место: `org_ip_allocation_resource.go:296-314` (`ResolveRefSvcParamValue(ctx, 19, …)`), `nsxt_snat_resource.go:43`.
Последствие: правило «никаких hardcoded service id вне реестров» формально соблюдено, фактически — нет; страж это не ловит.
Направление: вынести id/коды/каноны в один явный реестр-данные, покрытый чекером, либо расширить паттерн чекера.
**R3. Порядок «edge → аллокация» не гарантируется провайдером.**
Суть: платформа требует существующий vDC+Edge до `modify` орги, иначе «Can't cast Complex Object Type Struct to String».
Место: описано в `modifiers.tf:6-16`; в коде порядок не выражен — держится только на пользовательском `depends_on`.
Последствие: забытый `depends_on` → непонятная ошибка платформы на apply.
Направление: либо документировать как жёсткое требование в схеме/описании ресурса, либо проверять готовность edge в `Create` до modify.
**R4. Нельзя снять аллокацию через атрибут — только `destroy`.**
Суть: пустой массив запрещён (`org_ip_allocation_resource.go:328-331` `len==0 → error`), а Delete шлёт `count=0`, но `[]` не отправляется (`org_ip_allocation_resource.go:246-266`).
Последствие: «выключено» выражается двумя разными способами (count=0 при destroy vs невозможность `[]` при update) — асимметрия семантики.
Направление: определить единый канон «ноль аллокаций» и разрешить его через атрибут, либо явно задокументировать ограничение как намеренное.
**R5. Модификаторы не идемпотентны на уровне API (modify выполняется всегда).**
Суть: `ByCode` без pre-check — `Create`/`Update` всегда POST-ят modify, даже если live уже совпадает. Причина осознанная (комментарий applyAllocation).
Последствие: лишние операции modify при повторных apply/после ручных правок state; на больших инстансах — время и нагрузка. Корректности не нарушает (дрейф гасится в `Read`).
Направление: если появится надёжное сравнение desired↔live (не с paramValue формы) — переключить на idempotent-путь, уже имеющийся в ядре.
**R6. Импорт оставляет Required-атрибуты неинициализированными.**
Суть: `ImportState` ставит только `id` и `organization`/`nsxt_uid` (`org_ip_allocation_resource.go:288-292`, `nsxt_snat_resource.go:222-226`). `vip_configure`/`ip_space_name` (Required) наполняются лишь в `Read`, и то по условию (nsxt — только если live непустой `nsxt_snat_resource.go:160-166`).
Последствие: при импорте эджа с невыставленным SNAT `ip_space_name` останется null → требуется совпадение с конфигом на следующем плане; для пользователя неочевидно.
Направление: в `ImportState`/`Read` заполнять атрибут каноном (`no-needed`) при отсутствии значения.
## 4. Открытые вопросы
1. Оверлей `modifiers.yaml` и генерируемый слой `kind: modifier` (см. R5/S5) — в разрешённый список не входил. Нужно ли оценивать его как «мёртвый/параллельный» код к удалению, или он планируется как целевая замена ручных модификаторов? (файл вне списка — не читал).
```
---
## Ход 6 — Указание пользователя (дословно)
```
сначала документируй ВЕСЬ диалог, без сокращений
```
---
## Открытый вопрос Opus (ожидает ответа пользователя)
1. Оверлей `modifiers.yaml` и генерируемый слой `kind: modifier` — «мёртвый/параллельный» код
к удалению, или планируемая целевая замена ручных модификаторов?
---
## Ход 7 — Запрос пользователя (дословно)
```
твоё критическое мнение ?
```
### Критическое мнение агента о отчёте Opus (дословно)
```
**Сильное**
- **S1** — верно. `ARCHITECTURE.md:225` ссылается на реестр `serviceSpecificModifiers`, а в `TOOLS/yaml-generator/main.go` (~стр. 77–90) написано, что он удалён при рефакторинге. Спека устарела — цена находки низкая, но факт точный.
- **S5** — верно и важно. `loader.go` умеет `kind: modifier` (+ `delete_strategy`, `idempotency`) и в ядре есть idempotent-путь, но реальные модификаторы ручные. Это не «мёртвый код», а **незавершённая миграция**.
- **R1** — самое ценное. `nubes_vc_nsxt` в `Update` шлёт `ip_space_name` (372), и тот же параметр пишет `nubes_vc_nsxt_snat` → два ресурса перезаписывают одно поле. Аналогично `nubes_vc_org` / `v_ip_configure`.
- R4, R6 — по делу, но второго порядка.
**Слабое / спорное**
- **S2, S4 — натянуто.** Правила спеки («no manual edits», «core is universal») писаны про **сгенерированный** код; ручной оверлей в них вообще не описан. Это пробел спеки, а не нарушение «по букве». S4 ещё и относится к `Update` инстанс-ресурсов, к модификатору прямого отношения не имеет.
- **R2 — нарушение собственного правила.** Opus утверждает, что `check_hardcoded_service_ids.sh` «это не ловит», **не читая сам скрипт** (он вне списка). Надо было оформить вопросом, а не утверждением.
- **R5 — завышен по приоритету**: сам же пишет «корректности не нарушает».
- **Номера строк не сверены.** У меня из чтения `provider.go` функция `Resources` начинается со строки 182, и регистрация модификаторов идёт сразу за ней, а не 199–204. Одно из двух неверно → «проверяемость» отчёта под вопросом (а она была главным требованием).
**Упущено**
- Файла `modifiers.yaml` в репозитории **нет вообще** — только упоминания-комментарии. Значит это не «параллельный слой», а **задокументированное, но не созданное** наложение; его вопрос №1 надо переформулировать.
- Не названа корневая причина: генератор умеет модификаторы, но доменное знание (какая операция, имя, inverse) некуда положить — оверлей не сделан, поэтому написали руками.
- Не отмечена рассинхронизация семантики: у генерируемых ресурсов `suspend_on_destroy`, у модификаторов `keep_on_destroy` — два имени для одного смысла.
- То, что для Terraform опаснее всего — устойчивость `Read` (вечный diff), — разобрано тоньше всего, хотя именно там был реальный баг с порядком ключей JSON.
**Итог:** ~60 % полезного (S1, S5, R1, R4 + вопрос про оверлей), но с натяжками в S2/S4/R2 и без разбора `Read`. Главный дефект — поверхностный lifecycle-анализ.
```
---
## Ход 8 — Указание пользователя (дословно)
```
так сделай ему замечание !!! пусть глубже копнёт ЕСЛИ ЭТО НАДО
```
### Замечания агента (раунд 2) — текст, отправляемый Opus
Сохранён отдельным файлом: `NOTES/20_prompts/prompt_for_opus_provider_architecture_modifiers_round2.md`.
Содержимое (дословно):
```markdown
# Промпт для Opus 4.8 — РАУНД 2: замечания к отчёту (2026-09-30)
> Продолжение диалога. Раунд 1 — `prompt_for_opus_provider_architecture_modifiers.md`.
> Формат ответа тот же: сжато, тезисы, `файл:строка`, без догадок. Границы доступа — как в раунде 1
> (плюс список из §4 ниже). `HISTORY/**`, `NOTES/**`, `docs/**`, `HAR/**`, `TMP/**`, git-история — по-прежнему ЗАПРЕЩЕНЫ.
---
## 1. Зачтено (переделывать НЕ надо)
`S1`, `S5`, `R1`, `R4` — приняты. Не повторяй их в ответе.
## 2. Замечания — обязательны к отработке
**M1. Номера строк не сходятся.**
Ты дал `provider.go:199-204` для регистрации модификаторов. По моему чтению файла (начиная со
строки 180) функция `Resources()` находится примерно на строке 182, и регистрация идёт сразу за ней —
твои 199–204 не сходятся. Требование задания — «каждое утверждение проверяемо».
Действие: перепроверь **каждую** ссылку `файл:строка` в отчёте и дай точные номера; где не сверял —
пометь «не сверено». Без этого отчёт не принимается.
**M2. `R2` — нарушено правило «без догадок».**
Ты утверждаешь, что `check_hardcoded_service_ids.sh` «это не ловит», но этот скрипт **не читал**
(его не было в разрешённом списке). Это догадка, а не факт.
Действие: скрипт теперь разрешён (см. §4). Либо приведи факт из его кода, либо переформулируй в вопрос.
**M3. `S2`/`S4` — проверь основание, иначе они натянуты.**
Правила спеки («No manual edits to **generated** Go code», «Service-specific Go code is fully
**generated** from YAML») писаны про генерируемый код. Ресурсы в `resources_core` — ручные, не
генерируемые. Плюс `S4` («modify всегда через WithDefaults») относится к `Update` инстанс-ресурсов,
а не к отдельному ресурсу-модификатору.
Действие: для каждого из S2/S4 дай **текстуальную опору из спеки** (`TOOLS/ARCHITECTURE.md:строка`)
и переформулируй: это **пробел спеки** (нет категории для ручных оверлеев) или **нарушение**? Если
опоры нет — пункт снять.
**M4. `R5` — обоснуй приоритет или понизь.**
Ты сам пишешь «корректности не нарушает», но ставишь R5 выше R6.
Действие: назови шкалу ранжирования (например: вероятность × последствие × обнаружимость) и
пересчитай порядок; либо понизь R5.
**M5. Главный пробел: устойчивость `Read` и вечный diff.**
Для Terraform это опаснее всего, а разобрано тоньше всего (только R6/импорт).
Действие: разбери построчно, как `Read` модификатора формирует `vip_configure` / `ip_space_name`
из live-состояния и совпадёт ли это с тем, что вернёт `jsonencode` из конфига:
- порядок ключей JSON;
- тип `count` (строка vs число);
- снятие `null` и пустых значений;
- что видит `plan` после `Read` для `keep_on_destroy`.
Отдельно: может ли `Update` инстанс-ресурса (`nubes_vc_org` / `nubes_vc_nsxt`) вклиниться между
apply-ами модификатора и вернуть поле назад (пересечение — это R1, но нужен сценарий во времени).
Если считаешь, что глубокий разбор здесь не нужен — обоснуй одной строкой, почему.
## 3. Что упущено — доработать
**U1. Файла `modifiers.yaml` в репозитории НЕТ.**
Есть только упоминания-комментарии (в `TOOLS/yaml-generator/main.go` и док-заметках). Это не
«параллельный слой», а **задокументированное, но не созданное** наложение.
Действие: переформулируй свой открытый вопрос №1 с учётом этого факта.
**U2. Рассинхронизация словаря жизненного цикла.**
У генерируемых ресурсов — `suspend_on_destroy`, у модификаторов — `keep_on_destroy`. Два имени
для одного смысла, живут в разных ветках кода.
Действие: оцени как архитектурный дефект (единый словарь/контракт жизненного цикла), с `файл:строка`.
**U3. Корневая причина «ручных» модификаторов.**
Генератор умеет `kind: modifier`, но доменное знание (какая операция, имя, inverse) некуда положить —
оверлей не создан.
Действие: сформулируй вывод по вариантам (оверлей-данные vs ручной код vs иное) — одной строкой на вариант.
## 4. Дополнительно РАЗРЕШЕНО читать (только это, сверх раунда 1)
- `TOOLS/scripts/check_hardcoded_service_ids.sh`
- `TOOLS/resource-generator/main.go`
- `TOOLS/resource-generator/internal/templates/instance.go`
- `TOOLS/resource-generator/internal/templates/modifier.go`
- `provider/internal/resources_core/state_refresh.go`
- `provider/internal/resources_core/helpers.go`
- `provider/internal/resources_core/params_compare.go`
Всё остальное — по-прежнему нельзя. Нужен файл вне списка → вопрос мне, не чтение.
## 5. Формат ответа
- **Только дельта**: что исправляешь, что снимаешь, чем обосновываешь; новые находки по M5/U2.
- Полный отчёт заново не переписывать.
- Как в раунде 1: тезисы, `файл:строка`, без «лирики» и без «а ещё могу».
## 6. Разрешение копать глубже
Если для честного ответа нужно копнуть глубже (в т.ч. по файлам вне §4) — **скажи прямо**, каких
файлов не хватает, и задай вопрос. Если глубина не нужна — так и напиши по конкретному пункту.
```
---
## Ход 9 — Ответ Opus на раунд 2 (дословно, как получен от пользователя)
### Сырой лог сессии
```
Read
Раунд 2. Перепроверю номера строк и прочту новые разрешённые файлы.
Read , lines 193 to 195
Read
Read , lines 178 to 195
Searched for regex `ResolveRefSvcParamValue\(ctx, 19|noNeededIpSpace|func \(r \*|len\(items\) == 0|RunInstanceOperationUniversalByCode|liveItems, parseErr|if raw, ok := live\["ipSpaceName"\]|func parseVipConfigure|func formatVipConfigure|func vipAllocationsEqual` (`**/provider/internal/resources_core/org_ip_allocation_resource.go`), 21 results
Searched for regex `noNeededIpSpace|func \(r \*NsxtSnat|if raw, ok := live\["ipSpaceName"\]|func \(r \*NsxtSnatResource\) setSnat|RunInstanceOperationUniversalByCode|ImportState` (`**/provider/internal/resources_core/nsxt_snat_resource.go`), 18 results
Прочитаю новые разрешённые файлы: шаблоны генератора, helpers, params_compare, state_refresh.
Read
Read
Прочитаю helpers.go, state_refresh.go, params_compare.go для проверки поведения `ParamFormat`/refresh.
Read
Проверю точные номера строк в `ARCHITECTURE.md` для опоры S2/S4.
Searched for regex `must not contain service-specific|fully generated from YAML|No manual edits|no service-specific logic inside the core|modify всегда через WithDefaults|serviceSpecificModifiers|exactly two named registries` (`**/TOOLS/ARCHITECTURE.md`), 7 results
Searched for regex `for _, param := range cfsParams|func \(c \*UniversalClient\) RunInstanceOperationUniversalByCode|RunInstanceOperationUniversalByIdempotent|val, hasLive := lookupLiveParam` (`**/provider/internal/core/operation_run_bycode.go`), 5 results
Read , lines 174 to 193
Дельта по раунду 2.
```
### Дельта (дословно)
```markdown
## M1 — перепроверенные `файл:строка`
| Ссылка раунда 1 | Было | Факт (сверено) |
|---|---|---|
| Регистрация модификаторов | `provider.go:199-204` | `provider.go:182` — `func Resources()`; регистрация — `provider.go:186` и `provider.go:187` |
| Удалённый реестр в генераторе | `yaml-generator/main.go:70-90` | `main.go:80-90` (комментарий про снятый `serviceSpecificModifiers`) |
| hardcoded id 19 | `~300` | `org_ip_allocation_resource.go:308` |
| `noNeededIpSpace` | `43` | const — `nsxt_snat_resource.go:44`; `setSnat` — `nsxt_snat_resource.go:234`; inverse-modify — `nsxt_snat_resource.go:199-200` |
| Досылка дефолтов ByCode | `105-150` | `operation_run_bycode.go:112-150`; `ByCode` — `operation_run_bycode.go:11`, `ByIdempotent` — `operation_run_bycode.go:18` |
| `len(items)==0` → error | `328-331` | `org_ip_allocation_resource.go:333` |
| GenModifier-конвейер | `55-90` | `loader.go:57-86` (`op.Kind=="modifier"`) |
Прочее сверено: org_ip `ImportState` `org_ip_allocation_resource.go:290`; nsxt `ImportState` `nsxt_snat_resource.go:224`; nsxt `Read` overwrite `nsxt_snat_resource.go:152`; org_ip `Read` semantic-compare `org_ip_allocation_resource.go:177-188`.
## M2 — R2 переформулирован как ФАКТ (скрипт прочитан)
`check_hardcoded_service_ids.sh:15`: `grep -rnE '\.(ServiceID|ID)[[:space:]]*==[[:space:]]*[1-9][0-9]*' "$ROOT_DIR/TOOLS"`.
Два проверяемых факта:
1. **Область — только `TOOLS`.** Дерево `provider` не сканируется вовсе → ручные модификаторы в `resources_core` вне охвата стража по определению.
2. **Паттерн ловит только `.ServiceID==N` / `.ID==N`.** Литерал-аргумент `org_ip_allocation_resource.go:308` (`ResolveRefSvcParamValue(ctx, 19, …)`) под паттерн не подпадает даже теоретически.
Итог R2: hardcoded 19/22/`"no-needed"` не покрыты стражем по **двум** причинам (область + паттерн). Догадки убраны.
## M3 — S2/S4 пересмотрены
**S4 — СНИМАЮ.** Опора `ARCHITECTURE.md:134` находится в разделе «Generated Code Resilience» и относится к `Update` генерируемых инстанс-ресурсов, а не к ручному модификатору. К модификаторам правило неприменимо → расхождения нет.
**S2 — переклассифицирую в ПРОБЕЛ СПЕКИ (не «нарушение No manual edits»).**
Опоры: `ARCHITECTURE.md:13` и `ARCHITECTURE.md:213` говорят про *generated* Go — к ручным файлам не относятся (замечание верное). Но `ARCHITECTURE.md:12` («core … must not contain service-specific logic») и `ARCHITECTURE.md:110` («no service-specific logic inside the core») сформулированы про «core» без оговорок. Ручные модификаторы с зашитыми id 19/22 лежат в `resources_core` (`org_ip_allocation_resource.go:308`, `nsxt_snat_resource.go:44`). Спека **не содержит категории «ручной сервисный оверлей»** и не говорит, является ли `resources_core` частью «core». Поэтому S2 — пробел контракта (нет разрешённого места для такого кода), а по букве принципа 2 — пограничное противоречие. Не «нарушение No manual edits».
## M4 — шкала ранжирования и пересчёт
Шкала: **severity = вероятность × тяжесть_последствия × (1/обнаружимость)**. «Тихие» дефекты (низкая обнаружимость) поднимаются; то, что сразу видно в плане/диффе — опускается.
- **R2** — вер. высокая (любое добавление сервис-логики), тяжесть средняя (эрозия инварианта «ядро универсально»), обнаружимость низкая (страж молчит) → **верх**.
- **R3** — вер. средняя (забытый `depends_on`), тяжесть средняя (непонятная ошибка платформы), обнаружимость средняя → **середина**.
- **R6** — вер. средняя (импорт редок, но реален), тяжесть низкая, обнаружимость средняя → **ниже R3**.
- **R5** — «корректности не нарушает» (моё же слово), тяжесть минимальная, обнаружимость высокая → **низ**.
Новый порядок: **R2 > R3 > R6 > R5**. R5 понижен ниже R6 (замечание принято).
## M5 — устойчивость `Read` и вечный diff (главный разбор)
**org_ip, путь значения `vip_configure`:**
- Конфиг: `jsonencode([{name,count}])` → ключи по алфавиту (`count`,`name`); `count` — тип из `var.ip_count` (в `modifiers.tf:24` не квотирован → зависит от типа переменной).
- Канон провайдера `org_ip_allocation_resource.go:385`: `name` первым, `count` — всегда строка.
- Эти две формы **байт-различны** (порядок ключей; возможно число vs строка).
- Защита от вечного diff — `org_ip_allocation_resource.go:177-188`: сравнение **смысловое** (`org_ip_allocation_resource.go:398`, игнорирует порядок и формат), и при совпадении state **не перезаписывается** → в state остаётся байт-в-байт конфиг-форма → `plan` чист. `count`-число нормализуется в строку в `org_ip_allocation_resource.go:351` до сравнения, поэтому «3» (число) и «"3"» считаются равными. `null`/пустые/`[{}]` отбрасываются там же → не порождают фантомную аллокацию.
- Перезапись (реальный дрейф) даёт канон-форму (name-first) — она никогда не совпадёт байтово с `jsonencode`, но diff тогда **легитимен**; ближайший `Update` ставит `state=plan` (`org_ip_allocation_resource.go:119`) и вечного diff не создаёт.
**nsxt_snat, путь `ip_space_name`:** скаляр-строка. `nsxt_snat_resource.go:152` перезаписывает state только если live-значение непустое; иначе оставляет план (защита Required-атрибута от null). Форматных расхождений нет (простое имя) → вечного diff нет.
**`keep_on_destroy` в плане:** Optional+Computed, `Default=false` (`org_ip_allocation_resource.go:84`, `nsxt_snat_resource.go:78`). `Read` читает его из state в начале и не трогает (наружу его в API нет) → после первого apply стабилен, `plan` чист. В `modifiers.tf:30` задан `true` явно — diff отсутствует.
**Вывод M5-основной:** сами модификаторы вечного diff **не дают** — смысловое сравнение в `Read` его гасит.
**Временной сценарий R1 (инстанс-ресурс возвращает поле назад):**
Шаблон instance.go Update строит `params` из **всех** `ModifyParams` безусловно (не пропускает null для не-nested), и вызывает `UpdateResourceWithTimeout` → `RunInstanceOperationUniversalWithDefaults`. Гейт — `hasServiceParamChanges` (тот же шаблон): modify запускается, если изменился **любой** modify-параметр.
- `vc_org`: modify содержит **только** `vIPConfigure` (`19_vc_org.yaml`, op modify) → `nubes_vc_org` тронет поле, лишь если пользователь сам задал `vIPConfigure` на инстанс-ресурсе. Риск ниже.
- `vc_nsxt`: modify содержит `needEnableAVI`, `virtualServicesCount`, `ipSpaceName`, `qosProfile`, `routedNetConfiguration` (`22_vc_nsxt.yaml`). Сценарий: (t1) модификатор включил SNAT → (t2) пользователь на `nubes_vc_nsxt` меняет `needEnableAVI` → `hasServiceParamChanges=true` → `params[372]=ParamFormat(plan.IpSpaceName)` (запись присутствует всегда) → (t3) следующий `Read` модификатора видит дрейф и на очередном apply восстанавливает. Две сущности «пинают» поле по очереди.
**Не сверено** (нужны файлы вне §4): пошлёт ли `RunInstanceOperationUniversalWithDefaults` пустой `ipSpaceName` как `""` (затрёт SNAT) или дособерёт из live (как ByCode). Это решает, «затирание» или «no-op». Файлы: `core/operation_*` с реализацией `WithDefaults` и генератор-хелпер `ParamFormat` (funcs.go) для поведения null→"". См. M6.
## U1 — открытый вопрос №1 переформулирован
`modifiers.yaml` в репозитории **нет** — только упоминания-комментарии (`main.go:84-90`). Генерируемый слой модификаторов при этом **реален и готов**: шаблон `modifier.go` (полный CRUD + `reconcile` + `delete_strategy` + `idempotency`) и конвейер `loader.go:57-86`. То есть это **задокументированное-но-несозданное наложение**: механизм есть, данных для него нет. Вопрос: создавать `modifiers.yaml`-оверлей (данные) и перевести org_ip/nsxt_snat на генерацию — или узаконить ручные ресурсы отдельной категорией в спеке?
## U2 — рассинхронизация словаря жизненного цикла (архитектурный дефект)
Три несогласованных словаря «что делать при destroy», в разных ветках кода:
1. Генерируемый инстанс: `suspend_on_destroy` + `keep_on_destroy` (runtime-булевы, приоритет keep) — instance.go schema и `Delete` (ветки `suspend`/`state_only`).
2. Генерируемый модификатор: `delete_strategy` = `noop_warn`/`inverse`/`error` — **compile-time из YAML**, без `keep_on_destroy` — modifier.go Delete.
3. Ручной модификатор: снова `keep_on_destroy` (runtime) + ручной inverse — `org_ip_allocation_resource.go:84`, `org_ip_allocation_resource.go:195`; `nsxt_snat_resource.go:78`.
Дефект: один смысл («не трогать эффект при destroy / обратный откат») выражен тремя разными контрактами (runtime-флаг vs compile-time-стратегия), причём генерируемый и ручной модификаторы, решая одну задачу, используют **противоположные** механизмы. Нужен единый словарь жизненного цикла для всех трёх видов ресурсов.
## U3 — корневая причина «ручных» модификаторов (по вариантам, одной строкой)
- **Оверлей-данные (`modifiers.yaml` + второй проход):** доменное знание (сервис, операция, имя, inverse, idempotency) — декларативно; org_ip/nsxt_snat становятся генерируемыми; убирает S2/U2/R2 разом; цена — доделать загрузчик оверлея и миграцию.
- **Ручной Go-код (статус-кво):** максимум гибкости под нестандартную семантику (replace-массив, `no-needed`); цена — сервис-логика в `resources_core`, обход стража, тройной словарь.
- **Иное (гибрид):** генерировать каркас из оверлея, а нетиповые куски (канон vIPConfigure) выносить в маленькие data-хуки; цена — усложнение шаблона ради 1-2 сервисов.
## M6 — где нужна глубина (прошу доступ)
Для однозначного вердикта по временному R1 (затирание vs досбор) не хватает **двух** файлов вне §4:
1. Реализация `RunInstanceOperationUniversalWithDefaults` (ядро, `core/operation_run*.go` / `operation_cfs.go`) — шлёт ли пустой `ipSpaceName` как `""`.
2. Генератор-хелпер `ParamFormat` (`TOOLS/resource-generator/internal/.../funcs.go`) — во что разворачивается null-строка в `params`.
Разрешить чтение этих двух файлов? Без них пункт помечен «не сверено».
```
---
## Ход 10 — Инструкция и пояснение пользователя (дословно)
```
#attachment:Pasted text #1
докуменитируй сразу
пояснияю - модификаторы - модет и слодная, новая, но НЕБОЛЬШАЯ часть всего нашего сервиса
не нужно делать на них чрезмерный упор
главное - САМ провайдер, его устойчивость и правильность и тд
```
Разбор указания (для дальнейших раундов):
- **Документировать немедленно** — исполнено этим разделом.
- **Приоритет — САМ провайдер**: его устойчивость и корректность. Модификаторы — новая и, возможно,
сложная, но **НЕБОЛЬШАЯ** часть сервиса; чрезмерный упор на них делать не нужно.
- Следствие: темы `M5`/`M6`/`U2` (глубина по модификаторам) — второстепенны по отношению к вопросам
устойчивости/корректности ядра, генерации и всего провайдера в целом.
---
## Ход 10a — Запрос пользователя (дословно)
```
твоё мнение ?
```
Мнение агента по раунду 2 приведено ниже, внутри Хода 11 (раздел «Критическое мнение агента
о раунде 2»).
---
## Ход 11 — Запрос пользователя (дословно)
```
так дополни задание ему, пусть ещё анализирует НО не нало много - токены могути закончиться
```
### Критическое мнение агента о раунде 2 (дословно, отправлено пользователю)
```
**Что стало лучше**
- **M1** — Opus честно перепроверил и исправился: `provider.go:182/186/187`. Совпадает с моим чтением → доверие к остальным номерам выросло.
- **M2** — догадка устранена: приведена конкретная строка скрипта, область (только `TOOLS/`) и паттерн (`.ServiceID==N`/`.ID==N`). Это уже факт, и факт полезный: дерево `provider/` стражем не покрыто **вообще**.
- **M3** — `S4` снят с текстуальной опорой (`ARCHITECTURE.md:134` в разделе про generated-код), `S2` переклассифицирован в пробел спеки с опорой (`:12`, `:110` vs `:13`, `:213`). Честно и корректно.
- **U2** — новая ценная находка, которой у меня не было: **третий** словарь жизненного цикла — `delete_strategy` (compile-time в YAML) против runtime-флагов. Настоящий архитектурный дефект.
- **M4** — шкала задана, `R5` понижен. Замечание принято по существу.
**Что остаётся слабым**
- **M5 — это чтение кода, а не замер.** Вся защита от вечного diff держится на утверждении «в `Read` смысловое сравнение, и при совпадении state не перезаписывается». Это правдоподобно и проверяемо, но **не проверено прогоном** `terraform apply` ×2 / `plan`. По нашему же правилу «рекомендация без замера — догадка». Opus не предложил верификацию.
- **Риск переоценён/недооценён.** Самое опасное (R1: `Update` инстанс-ресурса затирает `ipSpaceName`) осталось **нерешённым** — за M6. При этом Opus сам пишет, что для `vc_nsxt` modify содержит 5 параметров, и запись `params[372]` присутствует **всегда**. Если так — это не «война дрейфов», а **молчаливое затирание SNAT** при любой правке эджа. Это надо было ставить вопросом №1 прошлого раунда, а не откладывать.
- **Ничего про сам провайдер.** Раунд целиком про модификаторы — ровно то, что вы просили не переоценивать. Ядро, генератор, устойчивость, корректность — не тронуто.
**Моё мнение**
Раунд 2 — честный и в плюс (правки по номерам, снятие `S4`, находка `U2`). Но по вашему приоритету он **мимо цели**: модификаторы, а не провайдер; и при этом главный вопрос (затирание `ipSpaceName` при `Update` эджа + отсутствие замера) остался открытым. Логичный следующий ход — развернуть раунд на «провайдер целиком», а `M6` **отложить**, а не разворачивать.
```
---
## Ход 12 — Задание агента (раунд 3) — текст, отправляемый Opus
Сохранён отдельным файлом: `NOTES/20_prompts/prompt_for_opus_provider_core_round3.md`.
Содержимое (дословно):
```markdown
# Промпт для Opus 4.8 — РАУНД 3: сам провайдер (устойчивость и корректность)
> Продолжение диалога. Раунды 1–2 — `HISTORY/OPUS/2026-09-30_provider_architecture_modifiers_dialog.md`.
> Режим и формат ответа — как раньше: сжато, тезисы, `файл:строка`, без догадок.
---
## Вводная (смена приоритета)
Модификаторы (`nubes_vc_org_ip_allocation`, `nubes_vc_nsxt_snat`) — новая, возможно сложная, но
**НЕБОЛЬШАЯ** часть сервиса. Чрезмерный упор на них не нужен.
**Главное — САМ провайдер: его устойчивость и корректность.**
- `M6` (доступ к двум файлам для доразбора R1) — **снять**; углубление по модификаторам больше не требуется.
- Прошлые находки (`S1`, `S2`, `R2`–`R6`, `U1`–`U3`) **не повторять**.
## Бюджет (жёстко — экономим токены)
- Прочитать **не более 10 файлов** суммарно. Ранее прочитанные повторно не открывать.
- Ответ — **не более 5 находок**, каждая **не более 3 строк**.
- Никаких вступлений, повторения прошлых отчётов, «а ещё могу».
## Что анализировать (провайдер целиком)
1. **Жизненный цикл инстанс-ресурса:** `create` / adopt / `suspend` / resume / `modify` / redeploy /
`delete` и повторный `apply` — где теряется корректность состояния.
2. **Досылка и нормализация параметров** (`WithDefaults`, zero-value fallback, дефолты `map-fixed`,
регистр UUID): где риск затереть значение или получить ложный diff.
3. **`Read` / refresh:** устойчив ли state у генерируемых ресурсов; где возможен вечный diff.
4. **Устойчивость ядра:** ретраи и ошибки, `LockInstance`, идемпотентность, поведение при дубликатах
и параллельных `apply`.
5. **Генератор → код:** какие классы дефектов порождает шаблон.
## Границы доступа
Разрешено читать (только это):
- `provider/internal/core/**`
- `provider/internal/resources_core/**`
- `provider/internal/provider/provider.go`
- `TOOLS/resource-generator/internal/templates/**`
- `TOOLS/resource-generator/internal/params/params.go`
- `TOOLS/resource-generator/internal/helpers/helpers.go`
- `TOOLS/resource-generator/internal/loader/loader.go`
- `TOOLS/resource-generator/internal/writers/writers.go`
Запрещено: `HISTORY/**`, `NOTES/**`, `docs/**`, `HAR/**`, `TMP/**`, `DEV_STAND/**`, `TEST_STAND/**`,
`PROD_STAND/**`, git-история. Нужен файл вне списка → вопрос мне, не чтение.
## Формат ответа
- **ТОП-5 находок** по убыванию критичности: суть → место (`файл:строка`) → последствие (≤ 3 строк).
- Затем **одна строка**: что из этого проверяемо только замером (прогон `apply`/`plan`), а не чтением.
- Находок меньше пяти — столько, сколько есть. Водой не добивать.
## Стоп-правило
Не хватает файла или данных — один короткий вопрос. Не догадываться.
```
---
## Ход 12a — Сообщение пользователя и вопрос агента
Пользователь прислал **только путь к файлу**, без какой-либо инструкции:
```
/home/naeel/TF/tf_provider/NOTES/20_prompts/prompt_for_opus_provider_core_round3.md
```
Агент, не имея однозначной команды, задал уточняющий вопрос с вариантами (проверить пути из
списка доступа / поправить текст задания / пересмотреть состав и границы / ничего не делать).
**Пользователь вопрос отклонил (cancelled)** — никаких изменений не вносилось, уточнение осталось
без ответа.
---
## Ход 13 — Ответ Opus на раунд 3 (дословно, как получен от пользователя)
### Сырой лог сессии
```
Read
Ограничусь бюджетом. Посмотрю состав `core/` и выберу ключевые непрочитанные файлы.
Read
Read
Read
Прочитаю ещё 4 ключевых файла ядра: ожидание операций, поиск (дубликаты), нормализацию параметров, cfs.
Read
Read
Прочитаю ещё 2: состояние инстанса (проверка ретрая 401) и нормализацию параметров.
Read
Read
Бюджет: прочитано 6 файлов. ТОП-5 находок по устойчивости/корректности ядра.
```
### Отчёт (дословно)
```markdown
**1. Транзиентный 401 не ретраится нигде; `getInstanceStateWithRetry` — имя-обман (ретрая нет).**
`instance_state.go:39` просто зовёт `doRequest` один раз; `http.go:118` `isRetryable` = {429,502,503,504}, **без 401**.
Прямо противоречит ARCHITECTURE.md:105-108 («GetInstanceState retry 401», «doRequest treats 401 as retryable»). Кратковременный 401 Gateway роняет любой read → срыв plan/apply/поллинга.
**2. Ретраится только GET; любой POST падает намертво на транзиентном 502/503/сети.**
`http.go:88` — retry только при `method=="GET"`.
Сбой на `/instanceOperations`, `/instanceOperationCfsParams` или `run` рвёт операцию; при create остаётся осиротевший инстанс → следующий apply ловит страж дубликатов `instance_find.go:168`.
**3. Modify обрывается уже ПОСЛЕ создания операции, если чтение live-параметров сбойнуло.**
`operation_run.go:150` (и симметрично bycode `operation_run_bycode.go:108`): `instanceLiveParams` err → `return`, но POST `/instanceOperations` уже прошёл.
Любой транзиентный сбой чтения live → повисшая pending-операция, блокирующая инстанс до idle.
**4. Zero-value fallback угадывает тип по ПОДСТРОКЕ имени параметра.**
`params.go:47`: при пустом `dataType` тип берётся из `name/code/label`, содержащих `array/list/map/json`.
Строковый параметр с кодом вроде `whitelist`/`...json...` получит `"[]"`/`"{}"` вместо `""` → неверно-типизированное значение уходит в API на незаданном поле.
**5. Эквивалентность значения в `Read` захардкожена под `jsonEnv`/ref_svc — прочие строки пишутся как вернул API.**
`state_refresh.go:143` спец-ветка только для `jsonEnv`; остальное — `ParseString(value)` «как есть» `state_refresh.go:168`. Регистровая нормализация есть лишь в ref_svc-пути `refsvc_resolve.go:29`.
Для non-ref_svc строкового поля, где API канонизирует формат (регистр UUID и т.п.), plan≠state → вечный diff.
Проверяемо только замером (не чтением): №1–3 (поведение при флапающем Gateway, орфан-операции) — прогоном `apply`; №5 (вечный diff) — прогоном `plan`.
```
---
## Ход 14 — Запрос пользователя (дословно)
```
мнение ? более вопросов в том чате задават не получится - он уже большой и жрёт токены
```
### Мнение агента о раунде 3 (дословно)
```
**Сильное**
- **#1** — самое ценное: `ARCHITECTURE.md:105-108` буквально обещает ретрай 401 для GET, а код, по его словам, 401 не ретраит. Если верно — это не «мелочь», а срыв `plan`/`apply`.
- **#3** — правдоподобно и практично: обрыв modify уже **после** отправленного POST → висящая операция.
- **#5** — бьёт в самое больное место Terraform (вечный diff).
**Где не верю / нужна аккуратность**
- **#1 — слово «нигде» настораживает.** Спека говорит, что ретрай 401 живёт внутри `GetInstanceState`/`GetInstanceStateRaw`, а Opus цитирует `instance_state.go:39` — один вызов `doRequest`. Вполне возможно, что ретрай реализован в другой функции, и абсолютный вывод неверен. Проверять надо именно те функции, а не один call-site.
- **#2 — подано как дефект, а это может быть защита.** Слепой ретрай POST `/instanceOperations` **опаснее** обрыва: рискует создать дубликат операции. Настоящий дефект — не «нет ретрая», а «нет идемпотентности / безопасного продолжения». В нынешней формулировке вывод вводит в заблуждение.
- **#4 — нет оценки частоты.** Срабатывает только при **пустом** `dataType`. Без ответа «как часто он пуст» это теория, а не риск.
- **#5 — возможно, уже покрыто.** По §6.5 `ARCHITECTURE_NEW.md` нормализация UUID при **сравнении** есть в `JSONStringsEquivalent` / `JsonNormalize` / `normalizeCompareValue` / `ParamsMatchForResume`. Прежде чем считать это багом, надо убедиться, что расхождение не гасится там.
**Общее**
- Все пять — чтение кода; ни одна не измерена. Это **гипотезы**, и формально Opus это честно признал.
- Раз чат исчерпан по токенам — дальнейшая проверка возможна только **локально**, своими силами: `grep` по `isRetryable`/`GetInstanceState`, сверка `state_refresh.go` с `normalizeCompareValue`.
```
---
## Статус
- Раунды 1–3: все три отчёта получены. Раунд 3 уложился в бюджет (прочитано 6 файлов, 5 находок).
- **Итог раунда 3 (ядро, устойчивость/корректность):**
1. 401 не ретраится нигде; `isRetryable` без 401 — противоречие `ARCHITECTURE.md:105-108`.
2. Ретрай только для GET; POST обрывается на 502/503/сети → риск осиротевших операций/инстансов.
3. Modify может оборваться **после** создания операции (сбой чтения live) → повисшая pending-операция.
4. Zero-value fallback определяет тип по подстроке имени → возможна неверная типизация незаданного поля.
5. Нормализация значения в `Read` — только `jsonEnv`/ref_svc → вечный diff на прочих канонизируемых строках.
- Находки №1–3 и №5 **не подтверждены замером** (по словам самого Opus) — перенесены как гипотезы.
- **Чат с Opus исчерпан по токенам** (сообщено пользователем 2026-09-30): новые вопросы в него
задавать нельзя; проверка находок возможна только локально.
- **Мнения агента записаны по всем раундам:** раунд 1 — Ход 7; раунд 2 — Ход 11 (и Ход 10a);
раунд 3 — Ход 14.
- Артефакты: `752244f` — промпт раунда 1; `ea75507` — замечания раунда 2;
`e46bc35` — задание раунда 3;
`NOTES/20_prompts/prompt_for_opus_provider_architecture_modifiers_round2.md`,
`NOTES/20_prompts/prompt_for_opus_provider_core_round3.md`.
- Следующий шаг (2026-09-30): промпт для **DeepSeek Pro** —
`NOTES/20_prompts/prompt_for_deepseek_pro_remediation_plan.md` (план правок кода/документации/архитектуры
+ план проверки/тестов; гипотезы групп A/B переданы ему на верификацию; исполнять будет Copilot).
- Настоящий документ ведётся по прямому указанию пользователя «документируй ВЕСЬ диалог,
без сокращений».
+2 -2
View File
@@ -45,7 +45,7 @@ Provider naming defaults:
Script: `TOOLS/scripts/01_generate_yamls.sh --profile TOOLS/config/<стенд>` Script: `TOOLS/scripts/01_generate_yamls.sh --profile TOOLS/config/<стенд>`
Input list of services: Input list of services:
- `TOOLS/config/services_list.txt` (service_id only) - `TOOLS/config/<стенд>/services_list.txt` (service_id + name; у каждого стенда свой список)
Token options: Token options:
- `TOKEN_FILE=/home/naeel/terra/HH-MM-SS.token`, or - `TOKEN_FILE=/home/naeel/terra/HH-MM-SS.token`, or
@@ -106,7 +106,7 @@ export S3_SECRET_KEY=...
- The GPG private key must remain stable across releases. Do not regenerate per build. - 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. - 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. - 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` — источник правды по тому, какие сервисы генерируются. - `TOOLS/config/<стенд>/services_list.txt` — источник правды по тому, какие сервисы генерируются (у каждого стенда свой).
- Если меняется версия провайдера — обновить `provider/main.go` (ранее `universal_rebuild/main.go` — устаревший путь). - Если меняется версия провайдера — обновить `provider/main.go` (ранее `universal_rebuild/main.go` — устаревший путь).
## One-time GPG bootstrap (do this once, keep the key stable) ## One-time GPG bootstrap (do this once, keep the key stable)
+9 -8
View File
@@ -2,7 +2,7 @@
## 1. Где прописывать ## 1. Где прописывать
Единственная точка входа — `devops/config/services_list.txt` (или профильный `profiles/{stand}/services_list.txt`). Единственная точка входа — `TOOLS/config/<стенд>/services_list.txt` (у каждого стенда свой список).
Формат строки: Формат строки:
``` ```
@@ -158,21 +158,22 @@ operations:
## 8. Быстрый старт: добавляем новый сервис ## 8. Быстрый старт: добавляем новый сервис
```bash ```bash
# 1. Добавить строку в services_list.txt # 1. Добавить строку в список сервисов нужного стенда
echo "200 my_new_service # Моя новая услуга" >> devops/config/services_list.txt # (TOOLS/config/dev|test|prod/services_list.txt)
echo "200 my_new_service # Моя новая услуга" >> TOOLS/config/test/services_list.txt
# 2. Сгенерировать YAML (test-стенд) # 2. Сгенерировать YAML (test-стенд)
devops/01_generate_yamls.sh --profile devops/profiles/test ./TOOLS/scripts/01_generate_yamls.sh --profile TOOLS/config/test
# 3. Сгенерировать Go-код + доки # 3. Сгенерировать Go-код + доки
devops/02_generate_resources_and_docs_v2.sh --profile devops/profiles/test ./TOOLS/scripts/02_generate_resources_and_docs_v2.sh --profile TOOLS/config/test
# 4. Проверить что появился файл # 4. Проверить что появился файл
ls devops/profiles/test/generated/resources_yaml/200_my_new_service.yaml ls generated/test/resources_yaml/200_my_new_service.yaml
ls devops/profiles/test/generated/go/200_my_new_service_resource.go ls generated/test/go/200_my_new_service_resource.go
# 5. Собрать и задеплоить провайдер # 5. Собрать и задеплоить провайдер
devops/03_build_and_upload_provider.sh --profile devops/profiles/test ./TOOLS/scripts/03_build_and_upload_provider.sh --profile TOOLS/config/test
# 6. Написать тестовый манифест в TEST_STAND/my_new_service/main.tf # 6. Написать тестовый манифест в TEST_STAND/my_new_service/main.tf
# 7. terraform init && terraform plan && terraform apply # 7. terraform init && terraform plan && terraform apply
+1 -1
View File
@@ -50,7 +50,7 @@ cd /home/naeel/TF/tf_provider
| Пайплайн публикации документации | `../DOCS_PIPELINE/README.md`, `../DOCS_PIPELINE/publish-docs.sh` | | Пайплайн публикации документации | `../DOCS_PIPELINE/README.md`, `../DOCS_PIPELINE/publish-docs.sh` |
| Правила генерации кода провайдера (ОБЯЗАТЕЛЬНЫ для генератора) | `../TOOLS/ARCHITECTURE.md` | | Правила генерации кода провайдера (ОБЯЗАТЕЛЬНЫ для генератора) | `../TOOLS/ARCHITECTURE.md` |
| Скрипты пайплайна | `../TOOLS/scripts/` | | Скрипты пайплайна | `../TOOLS/scripts/` |
| Конфиги стендов и общий реестр | `../TOOLS/config/` (`registry.env`, `<стенд>/profile.env`, `services_list.txt`) | | Конфиги стендов и общий реестр | `../TOOLS/config/` (`registry.env`, `<стенд>/profile.env`, `<стенд>/services_list.txt`) |
| Секреты (не коммитить) | `../secrets/` | | Секреты (не коммитить) | `../secrets/` |
| Текущая задача по IaC/`modify` | `../NOTES/40_chat_summaries/CHAT_RESUME_IAC_2026-09-24.md` | | Текущая задача по IaC/`modify` | `../NOTES/40_chat_summaries/CHAT_RESUME_IAC_2026-09-24.md` |
@@ -0,0 +1,116 @@
# Промпт для DeepSeek Pro — план правок кода/документации/архитектуры + стратегия проверки
**Репо:** `/home/naeel/TF/tf_provider` — `terraform-provider-nubes` (Go, Terraform Plugin Framework).
**Дата:** 2026-09-30. **Стенд-источник фактов:** `dev`.
---
## Роль и исполнитель
- Ты — архитектор / ведущий инженер.
- Ты **НЕ пишешь код** и **НЕ меняешь файлы**. Только анализ + план.
- Результат твоей работы — **план правок** (код, документация, архитектура) и **план проверки/тестов**.
- **Исполнять будет другой агент (Copilot)** — строго по твоему плану. Поэтому план должен быть
исполнимым: точные пути файлов, функции, что именно менять, чем проверять.
## Вводная
- Главное — **сам провайдер**: устойчивость, корректность, отсутствие вечных diff.
- Модификаторы (`nubes_vc_org_ip_allocation`, `nubes_vc_nsxt_snat`) — новая, возможно сложная, но
**НЕБОЛЬШАЯ** часть сервиса. Не раздувай их.
- Ниже — **ГИПОТЕЗЫ** предыдущего анализа. Они **НЕ подтверждены замером**.
Твоя первая задача — проверить их по коду и разделить на «подтверждено / опровергнуто / нужен замер».
## Гипотезы к проверке
### Группа A — ядро (приоритет)
- **A1.** 401 не ретраится: `provider/internal/core/instance_state.go:39` зовёт `doRequest` один раз;
`isRetryable` в `provider/internal/core/http.go:118` = {429,502,503,504} без 401.
Противоречит `TOOLS/ARCHITECTURE.md:105-108` («GetInstanceState retry 401», «doRequest treats 401 as retryable»).
- **A2.** Ретрай только для GET (`http.go:88`); POST (`/instanceOperations`, `/instanceOperationCfsParams`,
`run`) обрывается на транзиентном 502/503/сети → возможны осиротевшая операция/инстанс; далее срабатывает
страж дубликатов (`instance_find.go:168`).
- **A3.** Modify может оборваться **после** создания операции, если чтение live-параметров сбойнуло:
`operation_run.go:150` и `operation_run_bycode.go:108` (`instanceLiveParams` err → `return`).
- **A4.** Zero-value fallback угадывает тип по **подстроке** имени параметра при пустом `dataType`:
`params.go:47` (ищет `array/list/map/json` в `name/code/label`).
- **A5.** В `Read` нормализация значения только для `jsonEnv`/ref_svc: `state_refresh.go:143` и `:168`;
регистровая нормализация только в ref_svc-пути (`refsvc_resolve.go:29`) → возможен вечный diff.
### Группа B — модификаторы (второстепенно)
- **B1.** `TOOLS/ARCHITECTURE.md:225` («Exception Registry») ссылается на реестр `serviceSpecificModifiers`
в `TOOLS/yaml-generator/main.go`, которого **нет** (удалён при рефакторинге; см. комментарий `main.go:80-90`).
- **B2.** Спека не описывает **ручные сервисные оверлеи** (`provider/internal/resources_core/org_ip_allocation_resource.go`,
`nsxt_snat_resource.go`) — нет категории «ручной сервисный ресурс», неясно, входит ли `resources_core` в «core».
- **B3.** Страж `TOOLS/scripts/check_hardcoded_service_ids.sh:15` сканирует **только** `TOOLS/`
и ловит **только** `.ServiceID==N`/`.ID==N` → hardcoded `19`/`22`/`"no-needed"` вне охвата.
- **B4.** Порядок «edge → аллокация» не гарантируется провайдером — держится на пользовательском `depends_on`
(`DEV_STAND/FullPipe/modifiers.tf`).
- **B5.** У `nubes_vc_org_ip_allocation` нет способа снять аллокацию через атрибут (пустой массив запрещён) — только `destroy`.
- **B6.** `modify` выполняется всегда (нет pre-check идемпотентности), хотя idempotent-путь в ядре есть.
- **B7.** `ImportState` модификаторов не заполняет Required-атрибуты.
- **B8.** `modifiers.yaml` (оверлей) **не существует**, при этом генерируемый слой модификаторов готов
(`TOOLS/resource-generator/internal/loader/loader.go:57-86`, `internal/templates/modifier.go`) и не используется.
- **B9.** Три несогласованных словаря жизненного цикла: у генерируемых ресурсов `suspend_on_destroy`/`keep_on_destroy`,
у генерируемых модификаторов `delete_strategy` (compile-time), у ручных модификаторов снова `keep_on_destroy`.
## Что нужно на выходе (строго в этом порядке)
**A. Верификация гипотез**
Таблица: `№ | подтверждено / опровергнуто / нужен замер | опора (файл:строка) | примечание`.
Опровергнутые — обосновать, почему вывод неверен.
**B. План правок кода**
Таблица: `№ | файл | функция/место | что изменить | зачем | риск (низк/сред/высок) | ломает ли совместимость`.
Только правки, вытекающие из подтверждённых пунктов. Никаких «заодно улучшим».
**C. План правок документации и архитектуры**
Что именно и в каком файле (`TOOLS/ARCHITECTURE.md`, `README.md`, `VERSIONS.md`, `HOW_TO/**`, `docs/**`).
Отдельно: какие **архитектурные решения** надо зафиксировать (напр. единый словарь жизненного цикла).
**D. План проверки и тестирования**
Для каждой правки — три уровня:
1. **Юнит/пакетный тест** — какой пакет, что проверяет, где лежит (есть примеры: `provider/internal/resources_core/*_test.go`,
`TOOLS/resource-generator/internal/loader/loader_modifier_test.go`).
2. **Интеграционная проверка** — `terraform plan` / `apply` на `DEV_STAND/FullPipe`: что запустить, что ожидать
в выводе, на что смотреть (с учётом: `apply`/`destroy` выполняет **владелец**, не агент).
3. **Регрессия** — что ещё может сломаться и как это поймать.
Плюс статические стражи: `TOOLS/scripts/check_generated_drift.sh`, `check_hardcoded_service_ids.sh`,
сборка `03_build_and_upload_provider.sh`, `dev-materialize.sh`.
**E. Порядок работ**
Шаги, сгруппированные в **отдельные коммиты**, от безопасных к рискованным. Для каждого шага — 1 строка:
что делаем и как проверяем, после чего фиксируем коммитом.
**F. Открытые вопросы**
Только то, что нельзя выяснить из кода (значения, решения владельца).
## Ограничения
- **Ничего не менять**: не править файлы, не коммитить, не запускать `terraform`/`go`.
- `terraform apply` и `destroy` — **только владелец**.
- Каждое утверждение — с `файл:строка`. **Факт и предположение разделяй явно.**
- Бюджет: читать **не более ~20 файлов**; ответ — компактный, таблицами, **без воды и без «а ещё могу»**.
- Не предлагать переписывание с нуля без доказанной необходимости.
## Границы доступа
Разрешено читать:
- `TOOLS/**` (архитектура, генераторы, скрипты, конфиги)
- `provider/**` (кроме `artifacts/`, `bin/`, `generated/`)
- `generated/dev/resources_yaml/**`
- `docs/**`, `HOW_TO/**`, `README.md`, `VERSIONS.md`
- `DEV_STAND/FullPipe/**` (пример использования)
Запрещено: `HISTORY/**`, `NOTES/**`, `TMP/**`, `HAR/**`, `secrets/**`, `! /`, `.git` (история коммитов).
Нужен файл вне списка — задай вопрос, не читай.
## Формат ответа
- Сжато, тезисами, таблицами. Каждое утверждение проверяемо (`файл:строка`).
- Никаких вступлений, повторения вводной, «лирики».
## Стоп-правило
Задание неоднозначно или данных не хватает — **остановиться и задать один короткий вопрос**.
Не достраивать смысл и не действовать по догадке.
@@ -0,0 +1,43 @@
# Opus — код-ревью правок (раунд 5, 2026-09-30)
**Репо:** `/home/naeel/TF/tf_provider` (Go, Terraform Plugin Framework). Правки уже влиты в `master`,
`go build ./...` и `go test ./internal/... -short` — зелёные. Нужно только ревью.
## Формат ответа (ЖЁСТКО)
- **Только дефекты.** По 1 строке: `файл:строка` → что не так → чем грозит.
- **Весь ответ ≤ 10 строк.** Дефектов нет — ответ «ок».
- Без похвал, пересказа, «а ещё можно», без предложений рефакторинга.
## Что ревьюить (ровно эти коммиты)
```
git --no-pager show 383f8ea c5a4499 ea75cac 8519ba0 9da9766
# или сразу:
git --no-pager diff 047d53a^..9da9766 -- provider/ TOOLS/
```
Файлы:
- `provider/internal/core/http.go` — 401 в `isRetryable`
- `provider/internal/core/modifier_compare.go` — `modifierDesiredEqualsLive`, `modifierValuesEqual`, `modifierCodeMap`
- `provider/internal/core/operation_run_bycode.go` — idempotency pre-check по live
- `provider/internal/core/instance_create.go` — `instanceUid` возвращается вместе с ошибкой
- `provider/internal/resources_core/nsxt_snat_resource.go`, `org_ip_allocation_resource.go` — `ImportState`, `ByIdempotent`
- `TOOLS/resource-generator/internal/templates/instance.go` — partial state в `Create`
- `TOOLS/scripts/check_hardcoded_service_ids.sh` — расширение области/паттернов
## Вопросы (ответ — по 1 строке на каждый, дефект или «ок»)
1. **Q5 partial state.** `Create`: при `err != nil && id != ""` вызывается `resp.State.Set` с планом,
затем `AddError`. Не нарушает ли это контракт framework (допустим ли partial state при ошибке)?
2. **Live pre-check.** `modifierDesiredEqualsLive` ищет live-значение через `lookupLiveParam`
(ключи `Code`/`SvcOperationCfsParam`/`Name`/`Label`). Достаточно ли этого, чтобы не пропустить
нужный `modify`?
3. **`isRetryable` + 401.** Не создаёт ли ретрай 401 ложных повторов там, где это опасно (GET-пути)?
4. **`ImportState`.** Запись Required-атрибутов в `ImportState` не конфликтует ли с последующим `Read`?
5. Что-то ещё критичное в этих диффах — 1 строка.
## Границы доступа
Разрешено: `provider/internal/**`, `TOOLS/**`, `git show`/`git diff` по перечисленным коммитам.
Запрещено: `HISTORY/**`, `NOTES/**`, `docs/**`, `HAR/**`, стенды. Нужен файл вне списка → вопрос.
@@ -0,0 +1,106 @@
# Промпт для Opus 4.8 — анализ архитектуры провайдера + ресурсов-модификаторов
**Репо:** `/home/naeel/TF/tf_provider` — `terraform-provider-nubes` (Go, Terraform Plugin Framework).
**Дата:** 2026-09-30. **Стенд-источник фактов:** `dev`.
---
## Роль и режим работы
- Ты — архитектор/ревьюер универсального Terraform-провайдера.
- Работаем **в диалоге**: я (агент) передаю твои вопросы пользователю и возвращаю его ответы.
- Вопрос задавай ТОЛЬКО если без него ответить нельзя. Максимум 1–2 вопроса за раз, предельно коротко.
- Не догадываться. Нет данных — вопрос, а не допущение.
- Область не расширять: отвечать ровно на поставленную задачу.
## Задача
Проанализировать архитектуру универсального провайдера Nubes и встроенный в неё слой
**ресурсов-модификаторов** — отдельных ресурсов, которые вызывают операцию `modify`
у родительского инстанса (когда нужного параметра нет в операции `create`).
Оценить:
1. Как устроена архитектура по слоям и как течёт поток данных (API → YAML → код → API).
2. Соответствие заявленной спеки (`TOOLS/ARCHITECTURE.md`) фактической реализации — все
расхождения, с указанием `файл:строка`.
3. Корректность жизненного цикла модификаторов: `Create` / `Read` / `Update` / `Delete`,
идемпотентность, дрейф (drift), поведение при `replace` / повторном `apply`, импорт.
4. Место модификаторов в универсальном ядре: где и как нарушается принцип
«ядро универсально, доменные знания — только данные». Насколько оправдано текущее
решение (ручные Go-ресурсы, зарегистрированные поверх генерируемых).
5. Границы ответственности: что модификатор делает сам, что отдаёт платформе; как
выражается обратная операция (откат при `destroy`, значение «выключено»).
6. Риски и топ-проблемы — по убыванию критичности, каждое с `файл:строка`.
## Границы доступа (ЖЁСТКО)
Читать РАЗРЕШЕНО **только** файлы из списка ниже. Всё остальное — ЗАПРЕЩЕНО, в частности:
- `HISTORY/**`, `NOTES/**`, `TMP/**`, `HAR/**`, `docs/**`, `site/**`, `site_test/**`,
`apps/**`, `charts/**`, `FIYR_MGU/**`, `gateway/**`, `scripts/**`, `secrets/**`,
`tfflaskcrud/**`, `tfluceecrud/**`, `tfnodejscrud/**`, `DEV_STAND/**` (кроме одного файла
из списка), `TEST_STAND/**`, `PROD_STAND/**`, `provider/artifacts/**`, `provider/bin/**`;
- история git (`git log`, `git show`, `git diff` с коммитами), коммиты, теги, ветки;
- любой файл репозитория, которого нет в списке ниже.
Нужен файл вне списка → НЕ читать, а задать мне вопрос.
## Файлы к изучению (исчерпывающий список)
### Группа 1. Архитектура (спека)
- `TOOLS/ARCHITECTURE.md`
### Группа 2. Ресурсы-модификаторы и их регистрация
- `provider/internal/provider/provider.go`
- `provider/internal/resources_core/org_ip_allocation_resource.go`
- `provider/internal/resources_core/nsxt_snat_resource.go`
- `provider/internal/resources_core/org_ip_allocation_test.go`
### Группа 3. Рантайм-зависимости модификаторов (ядро)
- `provider/internal/core/client.go`
- `provider/internal/core/operation_run_bycode.go`
- `provider/internal/core/instance_params.go`
- `provider/internal/core/refsvc_resolve.go`
- `provider/internal/resources_core/resource_diagnostics.go`
- `provider/internal/resources_core/crud.go`
### Группа 4. Генератор (как рождается «универсальная» часть)
- `TOOLS/yaml-generator/main.go`
- `TOOLS/resource-generator/internal/loader/loader.go`
### Группа 5. Факты API (спеки операций/параметров)
- `generated/dev/resources_yaml/19_vc_org.yaml`
- `generated/dev/resources_yaml/22_vc_nsxt.yaml`
### Группа 6. Применение модификаторов (композиция цепочки)
- `DEV_STAND/FullPipe/modifiers.tf`
### Группа 7. Только если без них нельзя ответить (иначе не открывать)
- `provider/internal/resources_core/state_refresh.go`
- `provider/internal/resources_core/params_compare.go`
- `provider/internal/resources_core/helpers.go`
- `TOOLS/resource-generator/main.go`
- `TOOLS/resource-generator/internal/templates/instance.go`
## Что нужно на выходе
Структурированный отчёт, разделы строго в этом порядке:
1. **Устройство архитектуры** — слои и поток данных, 5–10 строк.
2. **Спека ↔ код** — список расхождений `ARCHITECTURE.md` с реализацией (`файл:строка`).
3. **Дефекты и риски модификаторов** — по убыванию критичности. По каждому:
суть → место (`файл:строка`) → последствие → предлагаемое направление (одна строка).
4. **Открытые вопросы** — списком, если есть.
## Формат ответа
- Максимально сжато: тезисы, без вступлений, воды и «лирики».
- Каждое утверждение проверяемо: ссылка `файл:строка`.
- Код — только короткие фрагменты, и лишь если без них тезис не понятен.
- Никаких «а ещё могу», никаких предложений расширить работу.
## Правило «стоп»
Если задание неоднозначно или данных не хватает — **остановиться и задать один короткий
вопрос**. Не достраивать смысл и не действовать по догадке.
@@ -0,0 +1,91 @@
# Промпт для Opus 4.8 — РАУНД 2: замечания к отчёту (2026-09-30)
> Продолжение диалога. Раунд 1 — `prompt_for_opus_provider_architecture_modifiers.md`.
> Формат ответа тот же: сжато, тезисы, `файл:строка`, без догадок. Границы доступа — как в раунде 1
> (плюс список из §4 ниже). `HISTORY/**`, `NOTES/**`, `docs/**`, `HAR/**`, `TMP/**`, git-история — по-прежнему ЗАПРЕЩЕНЫ.
---
## 1. Зачтено (переделывать НЕ надо)
`S1`, `S5`, `R1`, `R4` — приняты. Не повторяй их в ответе.
## 2. Замечания — обязательны к отработке
**M1. Номера строк не сходятся.**
Ты дал `provider.go:199-204` для регистрации модификаторов. По моему чтению файла (начиная со
строки 180) функция `Resources()` находится примерно на строке 182, и регистрация идёт сразу за ней —
твои 199–204 не сходятся. Требование задания — «каждое утверждение проверяемо».
Действие: перепроверь **каждую** ссылку `файл:строка` в отчёте и дай точные номера; где не сверял —
пометь «не сверено». Без этого отчёт не принимается.
**M2. `R2` — нарушено правило «без догадок».**
Ты утверждаешь, что `check_hardcoded_service_ids.sh` «это не ловит», но этот скрипт **не читал**
(его не было в разрешённом списке). Это догадка, а не факт.
Действие: скрипт теперь разрешён (см. §4). Либо приведи факт из его кода, либо переформулируй в вопрос.
**M3. `S2`/`S4` — проверь основание, иначе они натянуты.**
Правила спеки («No manual edits to **generated** Go code», «Service-specific Go code is fully
**generated** from YAML») писаны про генерируемый код. Ресурсы в `resources_core` — ручные, не
генерируемые. Плюс `S4` («modify всегда через WithDefaults») относится к `Update` инстанс-ресурсов,
а не к отдельному ресурсу-модификатору.
Действие: для каждого из S2/S4 дай **текстуальную опору из спеки** (`TOOLS/ARCHITECTURE.md:строка`)
и переформулируй: это **пробел спеки** (нет категории для ручных оверлеев) или **нарушение**? Если
опоры нет — пункт снять.
**M4. `R5` — обоснуй приоритет или понизь.**
Ты сам пишешь «корректности не нарушает», но ставишь R5 выше R6.
Действие: назови шкалу ранжирования (например: вероятность × последствие × обнаружимость) и
пересчитай порядок; либо понизь R5.
**M5. Главный пробел: устойчивость `Read` и вечный diff.**
Для Terraform это опаснее всего, а разобрано тоньше всего (только R6/импорт).
Действие: разбери построчно, как `Read` модификатора формирует `vip_configure` / `ip_space_name`
из live-состояния и совпадёт ли это с тем, что вернёт `jsonencode` из конфига:
- порядок ключей JSON;
- тип `count` (строка vs число);
- снятие `null` и пустых значений;
- что видит `plan` после `Read` для `keep_on_destroy`.
Отдельно: может ли `Update` инстанс-ресурса (`nubes_vc_org` / `nubes_vc_nsxt`) вклиниться между
apply-ами модификатора и вернуть поле назад (пересечение — это R1, но нужен сценарий во времени).
Если считаешь, что глубокий разбор здесь не нужен — обоснуй одной строкой, почему.
## 3. Что упущено — доработать
**U1. Файла `modifiers.yaml` в репозитории НЕТ.**
Есть только упоминания-комментарии (в `TOOLS/yaml-generator/main.go` и док-заметках). Это не
«параллельный слой», а **задокументированное, но не созданное** наложение.
Действие: переформулируй свой открытый вопрос №1 с учётом этого факта.
**U2. Рассинхронизация словаря жизненного цикла.**
У генерируемых ресурсов — `suspend_on_destroy`, у модификаторов — `keep_on_destroy`. Два имени
для одного смысла, живут в разных ветках кода.
Действие: оцени как архитектурный дефект (единый словарь/контракт жизненного цикла), с `файл:строка`.
**U3. Корневая причина «ручных» модификаторов.**
Генератор умеет `kind: modifier`, но доменное знание (какая операция, имя, inverse) некуда положить —
оверлей не создан.
Действие: сформулируй вывод по вариантам (оверлей-данные vs ручной код vs иное) — одной строкой на вариант.
## 4. Дополнительно РАЗРЕШЕНО читать (только это, сверх раунда 1)
- `TOOLS/scripts/check_hardcoded_service_ids.sh`
- `TOOLS/resource-generator/main.go`
- `TOOLS/resource-generator/internal/templates/instance.go`
- `TOOLS/resource-generator/internal/templates/modifier.go`
- `provider/internal/resources_core/state_refresh.go`
- `provider/internal/resources_core/helpers.go`
- `provider/internal/resources_core/params_compare.go`
Всё остальное — по-прежнему нельзя. Нужен файл вне списка → вопрос мне, не чтение.
## 5. Формат ответа
- **Только дельта**: что исправляешь, что снимаешь, чем обосновываешь; новые находки по M5/U2.
- Полный отчёт заново не переписывать.
- Как в раунде 1: тезисы, `файл:строка`, без «лирики» и без «а ещё могу».
## 6. Разрешение копать глубже
Если для честного ответа нужно копнуть глубже (в т.ч. по файлам вне §4) — **скажи прямо**, каких
файлов не хватает, и задай вопрос. Если глубина не нужна — так и напиши по конкретному пункту.
@@ -0,0 +1,59 @@
# Промпт для Opus 4.8 — РАУНД 3: сам провайдер (устойчивость и корректность)
> Продолжение диалога. Раунды 1–2 — `HISTORY/OPUS/2026-09-30_provider_architecture_modifiers_dialog.md`.
> Режим и формат ответа — как раньше: сжато, тезисы, `файл:строка`, без догадок.
---
## Вводная (смена приоритета)
Модификаторы (`nubes_vc_org_ip_allocation`, `nubes_vc_nsxt_snat`) — новая, возможно сложная, но
**НЕБОЛЬШАЯ** часть сервиса. Чрезмерный упор на них не нужен.
**Главное — САМ провайдер: его устойчивость и корректность.**
- `M6` (доступ к двум файлам для доразбора R1) — **снять**; углубление по модификаторам больше не требуется.
- Прошлые находки (`S1`, `S2`, `R2`–`R6`, `U1`–`U3`) **не повторять**.
## Бюджет (жёстко — экономим токены)
- Прочитать **не более 10 файлов** суммарно. Ранее прочитанные повторно не открывать.
- Ответ — **не более 5 находок**, каждая **не более 3 строк**.
- Никаких вступлений, повторения прошлых отчётов, «а ещё могу».
## Что анализировать (провайдер целиком)
1. **Жизненный цикл инстанс-ресурса:** `create` / adopt / `suspend` / resume / `modify` / redeploy /
`delete` и повторный `apply` — где теряется корректность состояния.
2. **Досылка и нормализация параметров** (`WithDefaults`, zero-value fallback, дефолты `map-fixed`,
регистр UUID): где риск затереть значение или получить ложный diff.
3. **`Read` / refresh:** устойчив ли state у генерируемых ресурсов; где возможен вечный diff.
4. **Устойчивость ядра:** ретраи и ошибки, `LockInstance`, идемпотентность, поведение при дубликатах
и параллельных `apply`.
5. **Генератор → код:** какие классы дефектов порождает шаблон.
## Границы доступа
Разрешено читать (только это):
- `provider/internal/core/**`
- `provider/internal/resources_core/**`
- `provider/internal/provider/provider.go`
- `TOOLS/resource-generator/internal/templates/**`
- `TOOLS/resource-generator/internal/params/params.go`
- `TOOLS/resource-generator/internal/helpers/helpers.go`
- `TOOLS/resource-generator/internal/loader/loader.go`
- `TOOLS/resource-generator/internal/writers/writers.go`
Запрещено: `HISTORY/**`, `NOTES/**`, `docs/**`, `HAR/**`, `TMP/**`, `DEV_STAND/**`, `TEST_STAND/**`,
`PROD_STAND/**`, git-история. Нужен файл вне списка → вопрос мне, не чтение.
## Формат ответа
- **ТОП-5 находок** по убыванию критичности: суть → место (`файл:строка`) → последствие (≤ 3 строк).
- Затем **одна строка**: что из этого проверяемо только замером (прогон `apply`/`plan`), а не чтением.
- Находок меньше пяти — столько, сколько есть. Водой не добивать.
## Стоп-правило
Не хватает файла или данных — один короткий вопрос. Не догадываться.
@@ -0,0 +1,43 @@
# Opus — раунд 4: короткие решения (2026-09-30)
**Репо:** `/home/naeel/TF/tf_provider` — `terraform-provider-nubes` (Go, Terraform Plugin Framework).
## Формат ответа (ЖЁСТКО)
- На каждый вопрос — **1–3 строки**, с `файл:строка`.
- **Весь ответ ≤ 25 строк.** Без вступлений, пересказа, «а ещё могу», без повторения вопросов.
- Не хватает данных — одна короткая строка-вопрос, не догадка.
## Уже сделано — не обсуждать
401 в `isRetryable`; страж хардкодов покрыл `provider/`; idempotency pre-check переведён на live
(`core/modifier_compare.go`); `ImportState` модификаторов заполняет Required.
## Вопросы
**Q1. Retry POST.** `core/http.go:87-108` ретраит только GET. Слепой retry создающего
`POST /instanceOperations` рискует дубликатом операции. Как правильно: (а) не ретраить +
задокументировать; (б) ретраить только безопасный POST (`validate-cfs`); (в) иное?
**Q2. Осиротевшая операция.** `core/operation_run.go:150`, `core/operation_run_bycode.go:108`: при
сбое `instanceLiveParams` уже созданная операция остаётся невыполненной. Продолжение с fallback
вернёт reset-баг (защита поставлена сознательно). Есть ли безопасный третий путь (cancel/delete
операции через API) или оставить как есть? Нужен API-метод — назови его.
**Q3. Zero-value по подстроке имени.** `core/params.go:47-59`: при пустом `dataType` тип угадывается
по `name/code/label`. Пути применения: `instance_create.go:105-113` (required без значения), досылка
modify. Менять (убрать угадывание) или оставить?
**Q4. Словарь жизненного цикла.** Три несогласованных контракта: `suspend_on_destroy`/`keep_on_destroy`
(генерируемые ресурсы), `delete_strategy` (генерируемые модификаторы), `keep_on_destroy` (ручные).
Какой единый контракт зафиксировать в `TOOLS/ARCHITECTURE.md`?
**Q5. Что упущено.** Назови **один** самый критичный для устойчивости/корректности **ядра** дефект,
не упомянутый выше: 1 строка — суть → `файл:строка`.
## Границы доступа
Читать: `provider/internal/**`, `TOOLS/ARCHITECTURE.md`,
`TOOLS/resource-generator/internal/{templates,loader,params,helpers}/**`.
Не читать: `HISTORY/**`, `NOTES/**`, `docs/**`, `HAR/**`, стенды, git-историю. Нужен файл вне списка → вопрос.
+21
View File
@@ -145,6 +145,27 @@ go build -o terraform-provider-nubes
- Генератор может создавать «разбитые» snake_case для CamelCase (например `resource_c_p_u`). - Генератор может создавать «разбитые» snake_case для CamelCase (например `resource_c_p_u`).
- Это ожидаемо, но если критично — нужен отдельный маппинг (по согласованию). - Это ожидаемо, но если критично — нужен отдельный маппинг (по согласованию).
### 6.5 Регистр UUID в JSON-параметрах (map-fixed) — ОБЯЗАТЕЛЬНО ЗНАТЬ
- Платформа хранит UUID в lowercase, но **сравнивает регистр при create**. Ресурс
`nubes_vc_nsxt` отдаёт `id` в UPPERCASE → `startupConfiguration.vdcUid/nsxtUid`
в верхнем регистре → ошибка «Edge не развёрнут в указанном vDC».
- **Нормализация нужна в ДВУХ разных местах, и они не взаимозаменяемы:**
1. **Сравнение** (план vs state, adopt/suspend/resume, modifier-compare, диагностика) —
`jsonutil.LowercaseUUIDsInText` внутри `JSONStringsEquivalent`, `JsonNormalize()`,
`ParamsMatchForResume`, `normalizeCompareValue`.
2. **Отправка в API** — единственная точка: `resources_core.BuildJSON`
(`provider/internal/resources_core/helpers.go`), её вызывает генератор
(`NestedJSONExpr` в `templates/instance.go`: Create / Modify / Redeploy).
С 30.09 результат оборачивается в `LowercaseUUIDsInText(...)`.
- ⛔ **Не «лечить» это в HCL** (`lower(...)` в конфиге стенда) — это костыль, который
существовал только потому, что путь отправки не нормализовал UUID. Он ломался при
работе из-под Windows на провайдере `2.0.23`.
- Детали, аудит всех мест и границы применимости: `docs/60_strategy/terraform_case_sensitivity_fix.md`
(§10 — регистр при сравнении, §11 — регистр при отправке).
- Не покрыто: скалярные UUID в `normalizeUniversalValueV6` (дефолты create / досылка modify)
и валидация ref-параметров внутри JSON при adopt — см. §11 и HISTORY/2026-09-30.
--- ---
## 7) Проверенная цепочка (dummy) ## 7) Проверенная цепочка (dummy)
@@ -242,6 +242,51 @@ SNAT останется включённым (Delete при `keep=true` печа
--- ---
## 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. Мои ошибки в этой сессии (обязательно к фиксации) ## 6. Мои ошибки в этой сессии (обязательно к фиксации)
1. Сказал, что apply «либо даст ошибку, либо создаст дубль кластера» — **неверно**: будет hard error 1. Сказал, что apply «либо даст ошибку, либо создаст дубль кластера» — **неверно**: будет hard error
@@ -0,0 +1,142 @@
# CHAT RESUME — Штурвал + «freeze on destroy»: состояние на 2026-09-25
> Цель файла: начать новый чат **без уточняющих вопросов** — здесь всё, что сделано, где живёт
> документация, что в каком состоянии и что делать дальше.
## 0. Где что лежит (точки входа)
| Что | Путь |
|---|---|
| Полный разбор сессии (диагностика, пайплайн, аудит регистра UUID) | `NOTES/30_analysis/SHTURVAL_DEV00_DIAG_AND_FREEZE_DESIGN_2026-09-24.md` |
| Страница для пользователя/DevOps: поведение и отличия от канонического Terraform | `docs/30_registry/guides/provider-behavior.md` |
| Регистр UUID (все нерабочие подходы + кейс UUID внутри JSON, §10) | `docs/60_strategy/terraform_case_sensitivity_fix.md` |
| Запись дня с коммитами и результатами | `HISTORY/2026-09-24_shturval_dev00_adopt_and_freeze_design.md` |
| Память репозитория (факты, шпаргалки, уроки) | `/memories/repo/shturval-destroy-freeze.md` |
| Версии провайдеров по стендам | `VERSIONS.md` |
Репозиторий: `/home/naeel/TF/tf_provider`, ветка `master`, remote `origin`
(`https://gitea.services.ngcloud.ru/terraform/tf_provider.git`). Снапшот состояния — ветка
`snapshot/2026-09-25-shturval-freeze-state`.
## 1. Что уже сделано
1. **Диагностика стенда `shturval-dev-00`** (услуга 150, инстанс `shturval-dev`, uid `94627ff4-…`):
кластер здоров (2 ноды Ready, 41/41 Shturval-сервисов `ready`, NodeConfigItems 4/4, у всех сервисов
есть endpoints). Единственный мусор — 4 подвисших пода `kube-system/shturval-init-job`
(3 Error + 1 Unknown при `Complete 1/1`), причина — webhook-и Штурвала недоступны до готовности Cilium
(`connect: operation not permitted`); самоочистка по `ttlSecondsAfterFinished: 86400` (≈25.09 14:31 UTC).
2. **Расшифрованы счётчики ЛК/Штурвала**: `Pods X/Y` = готовые/всего (без `Completed`);
«Системные сервисы» = число сервисов в режиме `auto`; «Конфигурация узлов» = NodeConfigItems;
«Ingress» = домен-шаблон, не счётчик.
3. **Разобран провал `destroy`**: `nubes_vc_org_ip_allocation` отправлял `count=0`, платформа не даёт
опустить `count` ниже занятых адресов — их держит кластер (`.146` API и `.148` ingress), `suspend`
адреса не освобождает.
4. **Усыновление исправлено**: в `DEV_STAND/FullPipe/shturval.tf` добавлен `adopt_existing_on_create = true`
(коммит `57abb7b`); проверено вживую — apply усыновил существующий инстанс и сам сделал `resume`.
5. **Реализован третий режим destroy** (коммит `22c6c83`, генератор, универсально для всех instance-ресурсов):
`keep_on_destroy` → `state_only` (приоритет), иначе `suspend_on_destroy` → `suspend`, иначе `delete`;
в `Delete` добавлены предупреждения «Ресурс заморожен, а не удалён» / «оставлен как есть».
6. **Конфиг стенда переведён в режим «заморозки»** (коммит `40aef87`): `keep_on_destroy = true` у квоты IP,
SNAT и эджа; `adopt_existing_on_create = true` и `suspend_on_destroy = true` у кластера; у vDC оба флага
уже стояли.
7. **Проверен цикл `destroy` = заморозка** на живом стенде (`2.0.22`): `0 added, 0 changed, 5 destroyed`,
ошибок нет; кластер и vDC → `suspended`, эдж `running` с `ipSpaceName=internet-ipv4-v1`, квота IP `count=3`,
state пуст; в выводе — 5 предупреждений.
8. **Найден и исправлен баг регистра UUID внутри JSON** (коммит `621280a`, релиз `2.0.23`): первый `apply`
после заморозки падал на `required params mismatch … startupConfiguration` (`2c37fed1-…` в плане против
`2C37FED1-…` в живом инстансе). Проведён аудит 8 мест (см. §10 в `terraform_case_sensitivity_fix.md`),
добавлены `jsonutil.LowercaseUUIDsInText` и нормализация строк внутри JSON, `JsonNormalize()` приводит
UUID-подстроки к lowercase; покрыто тестами (`jsonutil_test.go`, `params_compare_test.go`), `go test ./...` зелёный.
9. **Документация**: страница `docs/30_registry/guides/provider-behavior.md`, обновлённый §10 в
`terraform_case_sensitivity_fix.md`, записи в `NOTES`/`HISTORY`, память репозитория.
## 2. Текущее состояние (на момент записи)
- `DEV_STAND/FullPipe`: **`terraform state list` пуст** (после проверочного `destroy`).
- В облаке: кластер `shturval-dev-00` — `suspended`; vDC — `suspended`; эдж — `running`
(`ipSpaceName = internet-ipv4-v1`, SNAT включён); квота IP организации — `count = 3`.
- Кластер «спит»: `.146:6443` TCP принимается эджем, но k8s не отвечает (`connection reset by peer`);
`.148:443` открыт (эдж/AVI живут).
- Версия провайдера: в реестре dev — **`2.0.23`**; пины `versions.tf` в `DEV_STAND/FullPipe` и
`DEV_STAND/FPipeGmail` — `2.0.23`.
- Новый (не проверенный вживую) стенд пользователя: `DEV_STAND/FPipeGmail/`.
## 3. Что делать дальше
1. **Проверить обратный ход** (запускает только пользователь): `terraform apply` в `DEV_STAND/FullPipe`.
Ожидание: `adopt` по имени + `resume` для кластера и vDC; эдж/SNAT/квота — no-op; затем `plan` = `No changes`.
Проверки: `terraform state list`, `terraform state show`, статусы инстансов в API ЛК, `kubectl get nodes`
(снова Ready), поды `44/48` (+ мусор `shturval-init-job`, уйдёт сам).
2. **Доку при необходимости**: добавить страницу `provider-behavior.md` в `nav` (`mkdocs.yml`) и запустить
`TOOLS/scripts/04_build_and_publish_docs.sh` — **не запускать без прямой команды**.
3. **Открытые техдолги:**
- ref-параметр внутри JSON **не валидируется** при adopt (`resources_core/ref_validation.go`);
- регистр ключей в `lookupLiveParam` (`core/operation_run.go`, `operation_run_bycode.go`) — требует живой проверки;
- тикет в платформу: TTL/очистка Failed-подов установщика + порядок установки компонентов до готовности Cilium;
- тикет в платформу: у Эджа нет операции `suspend` (в `availableOperations` только `delete/modify/reconcile`).
4. **Полный teardown** — осознанно: `keep_on_destroy = false` и `suspend_on_destroy = false`, порядок
кластер → `count=0` → SNAT → эдж → vDC (для vDC действует правило «14 дней после suspend»).
## 4. Шпаргалка: режимы destroy
| Ресурс | Флаг | Поведение при destroy | Предупреждение |
|---|---|---|---|
| `nubes_k8s_sthutrval_cluster` | `suspend_on_destroy = true` | `suspend` | «Ресурс заморожен, а не удалён» |
| `nubes_vc_vdc` | `suspend_on_destroy = true` | `suspend` | то же |
| `nubes_vc_nsxt` (эдж) | `keep_on_destroy = true` | не трогается | «Ресурс оставлен как есть, а не удалён» |
| `nubes_vc_nsxt_snat` | `keep_on_destroy = true` | не трогается | «SNAT не выключался» |
| `nubes_vc_org_ip_allocation` | `keep_on_destroy = true` | не трогается | «Аллокация IP не снималась» |
Приоритет: `keep_on_destroy` > `suspend_on_destroy` > обычное удаление. Дефолты провайдера — разрушающие;
«заморозка» включается в `.tf` стенда.
## 5. Факты платформы (проверено)
- 1 кластер Штурвала = 1 vDC; vDC удаляется только через 14 дней после `suspend`.
- Квоту IP нельзя опустить ниже занятых адресов; адреса кластера `suspend` не освобождает.
- У эджа нет `suspend`; удаление эджа при живых зависимых (vApp/VM/кластер) недопустимо.
- API ЛК: `GET /api/v1/svc/instances/{uid}`, состояние — `instance.state.params`, статус — `explainedStatus`,
операции — `availableOperations`; ошибки операции — в теле (`isSuccessful=false`, `errorLog`), HTTP 200/201.
- Обязательные заголовки API ЛК: браузерный `User-Agent`, `Referer: https://deck-dev.ngcloud.ru/`,
`Authorization: Bearer <secrets/narodDEV.token>` — иначе `403`.
- Регистр UUID: облако отдаёт один и тот же UUID в разных регистрах → сравнивать всегда без учёта регистра
(в т.ч. **внутри JSON**).
## 6. Релизы
- Схема: prod `1.*`, dev `2.*`, test `3.*`; актуальная dev — `2.0.23` (`VERSIONS.md`).
- Релиз: `TOOLS/scripts/03_build_and_upload_provider.sh --profile TOOLS/config/dev <версия>`
(сам прогоняет `01` + `02`, собирает, подписывает, заливает, обновить `VERSIONS.md` вручную).
- Локальная проверка без релиза: `go build -o TMP/devbin/terraform-provider-nubes .` в `provider/`
и `TF_CLI_CONFIG_FILE=TMP/terraformrc.dev terraform validate|plan` (dev_overrides).
- YAML-спеки (`generated/*/resources_yaml/`) **не редактировать руками** — `01_generate_yamls.sh` перезапишет их из API.
## 7. Правила работы (для нового чата)
- `terraform apply` / `destroy` — только пользователь. Мне доступны `init/plan/validate/show/state show`.
- Никаких правок, коммитов, релизов и публикаций без прямой команды; после каждой правки — коммит.
- Перед правками важных файлов — бэкап в `TMP/backup_<дата>/`.
- Не «улучшать» соседние стенды/сервисы без команды (scope creep запрещён).
## 8. Быстрые команды
```bash
# состояние стенда
cd DEV_STAND/FullPipe && terraform state list && terraform plan
terraform state show nubes_k8s_sthutrval_cluster.shturval | grep -E "id|adopt|suspend|keep"
# кластер
kubectl get nodes
kubectl get pods -A --no-headers | awk '{split($3,a,"/"); tot++; if($4=="Running"&&a[1]==a[2]) ok++} END{print tot, ok}'
kubectl get pods -A | grep -v -E "Running|Completed"
# API ЛК (dev)
TOK=$(tr -d '\n' < secrets/narodDEV.token)
curl -s -H "Authorization: Bearer $TOK" \
-H "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36" \
-H "Referer: https://deck-dev.ngcloud.ru/" \
'https://lk-api-gateway-dev.ngcloud.ru/api/v1/svc/instances/94627ff4-33a5-48f2-aca1-695741e0b6a2'
# реестр провайдера (dev)
curl -s https://tf-registry.containerk8s.services.ngcloud.ru/v1/providers/nubes-dev/nubes/versions
```
@@ -0,0 +1,257 @@
# CHAT RESUME — добавляем ВМ в пайплайн (состояние на 2026-09-27)
> Цель файла: новый чат стартует **без уточняющих вопросов** — здесь всё, что уже сделано,
> что где лежит, что проверено на живых стендах и что решать по ВМ.
> Репозиторий: `/home/naeel/TF/tf_provider`, ветка `master`.
> Remote: `https://gitea.services.ngcloud.ru/terraform/tf_provider.git`.
> Репозиторий примеров: `tf_examples/` (отдельный git) → `https://gitea.services.ngcloud.ru/Nail/tf_examples.git`.
---
## 0. Где что лежит (точки входа)
| Тема | Путь |
|---|---|
| Предыдущее резюме (freeze/adopt, состояние Штурвала) | `NOTES/40_chat_summaries/CHAT_RESUME_2026-09-25_shturval_freeze_state.md` |
| Разбор диагностики + дизайн «заморозки» | `NOTES/30_analysis/SHTURVAL_DEV00_DIAG_AND_FREEZE_DESIGN_2026-09-24.md` |
| Зависимости сервисов DEV + чек-листы Штурвал/ВМ | `docs/60_strategy/dev_stand_service_dependencies.md` |
| Диаграмма зависимостей + генератор | `docs/diagrams/README.md`, `docs/diagrams/infra_services_flow.mmd`, `render_infra_diagram.py` |
| Страница для пользователя: поведение провайдера | `docs/30_registry/guides/provider-behavior.md` |
| Страница-инструкция пайплайна | `docs/curated/pipeline/vdc_edge_ip_snat.md` |
| Пример в репе примеров | `tf_examples/fullpipe_chain/` (в т.ч. `shturval.tf`) |
| Регламент публикации доков | `DOCS_PIPELINE/README.md`, `TOOLS/scripts/04_build_and_publish_docs.sh`, `scripts/publish-docs.sh` |
| Инструкции «как сделать руками» | `HOW_TO/README.md` (индекс), `HOW_TO/howitwasdone.md`, `HOW_TO/DEVOPS_BUILD_PIPELINE.md` |
| Версии провайдеров по стендам (источник правды) | `VERSIONS.md` |
| Память (репозиторий) | `/memories/repo/shturval-destroy-freeze.md`, `/memories/repo/registry-versions.md` |
| **ВМ: старые материалы** | `docs/20_discovery/vm_service.md`, `vm-ui-verification.md`, `vm_provisioning_failures.md`, `vapp-service.md`, `docs/40_analysis/vm_creation_failure_analysis.md`, `docs/50_history/04_vm_hang_fix_and_500_error.md`, `docs/50_history/12_vm_resource_hardening.md`, `docs/50_history/23_vapp_uid_inconsistency_displayname_resolve_bug.md` |
| **ВМ: старое резюме (ЛЕГАСИ)** | `NOTES/40_chat_summaries/CHAT_RESUME_PLAN_VM.md` — другой репозиторий (`/home/naeel/terra`), ресурс `nubes_vm`, пути неактуальны; только как история |
| **ВМ: спеки услуг** | `generated/dev/resources_yaml/26_vapp.yaml` (услуга 26, vApp), `generated/dev/resources_yaml/28_vc_vm_v3.yaml` (услуга 28, ВМ) |
| Референс от платформы (Cloud Director) | каталог `!/` в корне: `vmware_org.tf`, `vdc.tf`, `network.tf.tmpl` (цепочка ipSpace → providerGateway → providerVdc) |
| Стенды | `DEV_STAND/FullPipe/` (проверенный, аккаунт `tazet@narod.ru`), `DEV_STAND/FPipeGmail/` (аккаунт `tazetdinovn@gmail.com`, организация `kontra`) |
---
## 1. Что уже сделано (только факты, по коммитам)
**21–24.09 — ресурсы-модификаторы и релизы 2.0.18 → 2.0.23**
- `nubes_vc_org_ip_allocation` (modify `vIPConfigure`) и `nubes_vc_nsxt_snat` (modify `ipSpaceName`, обратный `no-needed`) — генератор + код + тесты (`22cf259`, `80d82a1`, `73a7459`, `4b497e6`, `ba6c4f5`, `1236c59`).
- Ресурс аллокации принимает **имя** организации (резолв в UUID) — `5bd197f`.
- Убран plan-modifier, менявший пользовательское значение (Terraform: planned value must match config) — `807dfde`.
**24.09 — Штурвал: adopt + «заморозка»**
- Всё про Штурвал собрано в один файл `shturval.tf` (`b8adeb6`); правильный сервис — **150**, не 148 (`1a90c77`, `d98f603`).
- `worker_configuration` — JSON с **camelCase** (`groupName`, `sizingPolicy`, `sizingDisk`, `labelDeck`) — snake_case валит платформу (`72d5771`).
- `adopt_existing_on_create = true` для кластера (`57abb7b`).
- Реализован **третий режим destroy** `keep_on_destroy` → `state_only` для всех instance-ресурсов + предупреждения в Delete (`22c6c83`), релиз **2.0.22** (`c29df21`).
- Конфиг стенда переведён в «заморозку» (`40aef87`): квота IP/SNAT/эдж — `keep_on_destroy = true`, кластер — `suspend_on_destroy = true`, у vDC — `suspend` + adopt.
- Живая проверка `destroy` = заморозка: `5 destroyed`, кластер и vDC → `suspended`, эдж/SNAT/квота не тронуты (`c9d7345`).
**24.09 — фикс регистра UUID, релиз 2.0.23**
- Adopt suspended-инстанса падал на регистре UUID внутри JSON (`startupConfiguration`: план `2c37fed1-…` против живого `2C37FED1-…`). Добавлены `jsonutil.LowercaseUUIDsInText`, нормализация строк внутри JSON, `JsonNormalize()` приводит UUID-подстроки к lowercase; покрыто тестами (`621280a`), релиз **2.0.23** (`208d97e`, `03fff05`).
**24–25.09 — документация**
- Новая страница «Как работает провайдер Nubes: поведение и отличия от канонического Terraform» (`aaf87d9`, читаемость `c808a3b`): §1 суть, §2 ресурсы, §3 отличия от Terraform, §4 CRUD, §5 delete/suspend/state_only, **§6 конкретный пайплайн стенда vDC → Edge → IP → SNAT → Штурвал**, §7 регистр UUID, §8 чек-лист DevOps, §9 FAQ, §10 ссылки.
- Страница пайплайна переписана (`f8d6494`): требования, чек-лист услуги 150 (ALB + AVI ≥ 3, внешних IP ≥ 3), проверка результата (адреса API/Ingress), таблица «заморозки» при destroy, полное удаление, состав файлов.
- `provider-behavior.md` добавлена в навигацию `mkdocs.yml` («Руководства → Как работает провайдер (отличия от Terraform)») и связана ссылкой со страницей пайплайна (`8d25de9`).
**25.09 — стенд второго пользователя `FPipeGmail`**
- Организация `kontra` (аккаунт `tazetdinovn@gmail.com`, компания `naeel_test`, ClientID `WZ03709`), токен — `secrets/dev.token`; в `.tfvars` было закомментировано устаревшее `kontora` (`e20c22e`).
- Имена Штурвала сделаны свои: `shturval-dev1` / `shturval-dev-01` — занятое имя кластера (даже созданное другим пользователем) платформа не принимает (`e72eb75`).
- Проверено на 25.09: `terraform init` (провайдер 2.0.23), `validate` — ок, `plan` = `5 to add, 0 change, 0 destroy`. **apply не запускался** (запускает только пользователь).
**25.09 — репозиторий примеров `tf_examples`**
- `fullpipe_chain`: добавлен `shturval.tf` (переменные + locals + ресурс, самодостаточно), пин провайдера `2.0.21 → 2.0.23`, флаги «заморозки» как в рабочем стенде (эдж/SNAT/квота `keep_on_destroy = true`, vDC `suspend` + adopt), выводы Штурвала, блок параметров в `terraform.tfvars.example`, переписаны README примера и корневой (`21618a7`); добавлено предупреждение про уникальность имени кластера (`435a685`).
- Всё запушено: последний коммит примеров — `435a685`.
**26.09 (не в этой сессии, но уже в `master` — чтобы не переспрашивать)**
- Диаграмма зависимостей сервисов: `docs/diagrams/*` (`ea725bd`, `ff38352`, `6791e8f`), генератор `render_infra_diagram.py`, README с инструкцией перегенерации.
- Сверка цепочек зависимостей с YAML + чек-листы Штурвала и ВМ: `docs/60_strategy/dev_stand_service_dependencies.md` (`dc837e8`), перенос артефактов диаграмм + гайд по созданию vApp (`a366f47`).
- Закоммичены бэкапы от 25.09: `TMP/backup_2026-09-25/**` (`4800958`).
---
## 2. Текущее состояние (проверено 2026-09-27)
**Terraform**
- `DEV_STAND/FullPipe`: `terraform state list` — **пусто**.
- `DEV_STAND/FPipeGmail`: `terraform state list` — **пусто**; при этом конфиг валиден, `plan` = `5 to add` (по состоянию на 25.09).
- Terraform `1.9.5`; `go build ./...` и `go test ./...` — чисто (проверено 25.09).
**Облако, аккаунт `tazet@narod.ru` (токен `secrets/narodDEV.token`)**
| Услуга | Имя | UID | Статус |
|---|---|---|---|
| 150 Штурвал | `shturval-dev` | `94627ff4-33a5-48f2-aca1-695741e0b6a2` | `suspended` |
| 21 vDC | `fullpipe-vdc` | `d0937335-276b-475b-baa4-d8e6d16bad51` | `suspended` |
| 22 Edge | `fullpipe-edge` | `2c37fed1-e8f8-4a84-8434-7851c7c8b5d6` | `running`, SNAT `internet-ipv4-v1` |
| 19 Организация | `organ` | `57eeacd1-dc7f-4a52-b903-7e5f7d3c1164` | `running`, `vIPConfigure` = `[{internet-ipv4-v1, count: 3}]` |
Всего в аккаунте 13 инстансов, в том числе: `dummy-5`/`dummy-6` (1), `External ip` и `vIp` (25, `running`), `A-record` (111, `running`), инстансы mgmt-кластера (21/22, `deleted`). Из 3 инстансов `fullpipe-edge` — 2 `deleted` (история), 1 `running` — не пугаться.
**Облако, аккаунт `tazetdinovn@gmail.com` (токен `secrets/dev.token`, срок до 2026-12-27)**
- Организация `kontra` (`df5ec5f2-5f5d-4c82-952c-3dffa91c61d3`, услуга 19): `running`, `isTrial`, создана 2026-09-24 08:20, `vIPConfigure` = `count: 3`, realm `sandbox.nubes.ru`.
- В этом аккаунте **уже созданы объекты стенда FPipeGmail**: `fullpipe-vdc` (21) — `suspended`, `fullpipe-edge` (22) — `running`, `shturval-dev1` (150) — `suspended` (локальный `state` при этом пуст → следующий `apply` усыновит их по имени и разморозит). Всего 12 инстансов, плюс `kontra` (19), S3-бакеты (13), `s3naeeldev` (12), `mariamdb` (115), `vc_v`/`vc_vdc-1`/`tedj` — `deleted`.
- ⛔ **Исправление ранее ложного наблюдения** (в первой версии этого резюме было «список отдаёт 0 инстансов»): это была **ошибка парсинга ответа с моей стороны** — API отдаёт список в ключе `results`, а не `instances`. Оба аккаунта отдают данные корректно (narod — 13, gmail — 12). Никакой аномалии платформы нет.
**Провайдер / реестр / доки**
- DEV в реестре: **`2.0.23`** (последняя), пины `2.0.23` в `DEV_STAND/FullPipe/versions.tf`, `DEV_STAND/FPipeGmail/versions.tf`, `tf_examples/fullpipe_chain/versions.tf`, `.terraform.lock.hcl` FullPipe.
- PROD `1.0.0`, TEST `3.0.0` (новая схема: prod `1.*`, dev `2.*`, test `3.*`).
- Доки dev опубликованы (`2.0.23`) и зеркало на ВМ обновлено; живые URL:
- https://tf-docs.nodejsk8s.dev.nubes.ru/nubes-dev/curated/pipeline/vdc_edge_ip_snat/ (в меню: «Проверенные примеры»)
- https://tf-docs.nodejsk8s.dev.nubes.ru/nubes-dev/30_registry/guides/provider-behavior/ (в меню: «Руководства»)
**Git**
- Основной репо: `master` = `origin/master` = `e72eb75`; примеры: `435a685` = `origin/master`.
- В истории уже лежат коммиты от 26.09 (диаграммы, чек-листы, бэкапы) — см. §1.
---
## 3. Что делать дальше: **добавляем ВМ в пайплайн** (главная цель нового чата)
**Целевая цепочка (текущая + ВМ):**
организация (руками в ЛК) → vDC (21) → Edge (22) → внешние IP (modify) → SNAT (modify) → **[vApp (26) → ВМ (28)]** → Штурвал (150)
**Подтверждённые факты (сверено с YAML, коммит `dc837e8`)**
- D-06: vApp (26) → Edge (22), параметр `nsxtUid` — vApp использует Edge для сети.
- D-07: vApp (26) → vDC (21), параметр `vdcUid`.
- D-08: ВМ (28) → vApp (26), параметр `vappUid` — ВМ размещается в vApp.
- ВМ можно создавать в **уже существующих** vApp / vDC / Edge — ссылками `vappUid` / `vdcUid` / `nsxtUid`.
- Ресурсы (CPU/RAM/Storage) ВМ: `vmCpu`, `vmRam`, `vmDisk` (доп. диск); квоты выделяются на уровне vDC (21) и услуги ВМ (28).
- Чек-лист создания ВМ (28) по create-параметрам, обязательные: `vappUid`, `vmName`, `vmCpu`, `vmRam`, `accessPortList`, `imageVm`, `userLogin`, `userPublicKey`, `ipSpaceName`. Полный текст — `docs/60_strategy/dev_stand_service_dependencies.md` (§«Чек-лист создания ВМ (28)»).
- Спеки ресурсов: `generated/dev/resources_yaml/26_vapp.yaml`, `28_vc_vm_v3.yaml` (**руками не править** — перезапишет `01_generate_yamls.sh`).
- Имена ресурсов в провайдере: `nubes_vc_vapp`, `nubes_vc_vm_v3` (услуга 148 «Менеджмент Kubernetes Штурвал» — **не** то, что нужно; для кластера только 150).
**Известные проблемы ВМ (читать до работ)**
- `docs/20_discovery/vm_provisioning_failures.md` — падения на создании, разбор payload Terraform vs ручного запроса.
- `docs/40_analysis/vm_creation_failure_analysis.md`, `docs/50_history/04_vm_hang_fix_and_500_error.md` — зависание на FW и 500 («String[] → GUID»).
- `docs/50_history/12_vm_resource_hardening.md` — правки по надёжности ресурса ВМ.
- `docs/50_history/23_vapp_uid_inconsistency_displayname_resolve_bug.md` — резолв по имени мог вернуть **удалённый** инстанс (лечится фильтром `isDeleted=false`); проверить, что фикс жив в 2.0.23.
**Что уже выяснено фактами (проверено 27.09 — чтобы не гадать)**
- vApp (26), обязательные параметры create: `vappName`, `vdcUid`, `nsxtUid`. Операции: `create`, `delete`, `suspend`, `resume`; в YAML — `suspend_on_destroy_default: true`. Из инструкции услуги: `delete` требует **предварительного `suspend`**, полное удаление — через 14 дней. Отдельных параметров сети/подсети/gateway у vApp нет — сеть даёт Edge (`nsxtUid`).
- ВМ (28), обязательные параметры create: `vappUid`, `vmName`, `vmCpu`, `vmRam`, `ipSpaceName`, `imageVm`, `userLogin`, `userPublicKey`, `accessPortList` (`vmDisk` — дополнительный диск, не обязателен). Операции: `create`, `delete`, `modify`, `reconcile`, `redeploy`, `resume`, `suspend`; в YAML — `suspend_on_destroy_default: true`. В modify-наборе есть параметр `sameSnat` (разобраться, что даёт).
- `ipSpaceName` у ВМ — «Public IP setting», допускает значение `no-needed` (без публичного адреса). Если ВМ нужен внешний IP, квоту организации придётся увеличивать: сейчас `count = 3` и все три держит кластер Штурвала, а `suspend` адреса не освобождает.
- Соответствие кодов и ID параметров ВМ (`docs/20_discovery/vm_service.md`): `vappUid` 407, `vmName` 408, `vmCpu` 409/493, `vmRam` 410/494, `vmDisk` 411/495, `ipSpaceName` 412/496, `accessIpList` 413/497, `imageVm` 414, `cloudInit` 415, `userLogin` 416, `userPublicKey` 417, `accessPortList` 448/498, `needAddZabbixTemplate` 449/499.
- **Список образов (`imageVm`) в репозитории отсутствует**: в документации зафиксирован только пример `Ubuntu_22-20G`; HAR с созданием ВМ (`vmOK.har`, `vmsuspendresumemodify.har`) были в старом репо `/home/naeel/terra` — в этом репо в `HAR/` их нет (там `org*`, `vdc`, `edge_`, `ipSpace0`, `pgmodify`, `dummy`, `globak`). Значит: смотреть список в ЛК либо снять свежий HAR.
- Живых инстансов vApp (26) и ВМ (28) сейчас **нет ни в одном аккаунте** → `availableOperations` по ним снять не с чего, проверять после первого создания.
- Наличие `suspend` у обоих сервисов означает, что режим «заморозка» (`suspend_on_destroy` / `keep_on_destroy`) у них применим — как у кластера и vDC.
**Что предстоит решить (НЕ выбрано, требует решения пользователя)**
1. Где создавать vApp: в `DEV_STAND/FullPipe` (проверенный стенд, аккаунт `narod`) или отдельным файлом-аналогом `shturval.tf` (например `vm.tf`) — по сложившейся конвенции «всё про сервис в одном файле».
2. Очерёдность: ВМ до Штурвала или после (влияет и на внешние адреса, и на время apply).
3. `imageVm` — откуда брать список образов (в текущих заметках не зафиксировано) → проверять в ЛК/HAR.
4. `ipSpaceName` для ВМ — тот же `internet-ipv4-v1` или другой; хватит ли квоты (сейчас `count = 3`, занято кластером).
5. `userPublicKey` — какой ключ использовать (в репо есть `secrets/id_ed25519.pub`).
6. Режим destroy для vApp и ВМ (есть ли `suspend` у 26/28 — проверить `availableOperations` на живом инстансе перед выбором флагов).
7. Обновить после работ: `docs/curated/pipeline/vdc_edge_ip_snat.md` (или новую страницу), `tf_examples/fullpipe_chain` (файл ВМ + README + выводы), `docs/60_strategy/dev_stand_service_dependencies.md`, диаграмму `docs/diagrams/*` (перегенерировать `render_infra_diagram.py`).
**Правило порядка (из инструкций ЛК):** платформа строит список ipSpace из состояния `job.vcd.networkProvider` / `providerGateway`, то есть аллокация IP и SNAT идут **после** готовых vDC и Edge; иначе modify падает с «Can't cast Complex Object Type Struct to String».
---
## 4. Факты платформы (проверено, не перепроверять)
- 1 кластер Штурвала = 1 vDC; vDC удаляется только через **14 дней** после `suspend`.
- Квоту IP нельзя опустить ниже занятых адресов; `suspend` кластера адреса **не** освобождает.
- У Edge операции `suspend` нет вообще: доступны `delete`, `modify`, `reconcile`.
- Доступные операции: организация (19) — `delete`, `suspend`, `resume`, `modify`, `reconcile`, `create_user`, `delete_user`; vDC (21) — `delete`, `modify`, `suspend`, `resume`, `reconcile`; Штурвал (150) — то же + `create_user`, `delete_user`.
- API ЛК (dev): `GET /api/v1/svc/instances/{uid}`; поля: `explainedStatus`, `state.isSuspended`, `state.isDeleted`, `state.params`, `availableOperations`; ошибки операций приходят **в теле** (`isSuccessful=false`, `errorLog`) при HTTP 200/201.
- Обязательные заголовки: браузерный `User-Agent`, `Referer: https://deck-dev.ngcloud.ru/`, `Authorization: Bearer <токен>`; иначе 403.
- Регистр UUID: облако отдаёт один и тот же UUID в разных регистрах → сравнивать **без учёта регистра, в том числе внутри JSON** (учтено в 2.0.23).
- Имя кластера Штурвала должно быть **уникальным**: занятое (даже другим пользователем) не подойдёт; имя услуги и имя кластера — свои у каждого пользователя.
- `plan` не видит существующие объекты: adopt/конфликт проверяются в `Create` (в плане всегда «will be created»).
---
## 5. Шпаргалка: режимы destroy («заморозка»)
| Ресурс | Флаг в стенде | Поведение при destroy | Предупреждение |
|---|---|---|---|
| `nubes_k8s_sthutrval_cluster` | `suspend_on_destroy = true` | `suspend` | «Ресурс заморожен, а не удалён» |
| `nubes_vc_vdc` | `suspend_on_destroy = true` | `suspend` | то же |
| `nubes_vc_nsxt` (эдж) | `keep_on_destroy = true` | не трогается | «Ресурс оставлен как есть, а не удалён» |
| `nubes_vc_nsxt_snat` | `keep_on_destroy = true` | не трогается | «SNAT не выключался» |
| `nubes_vc_org_ip_allocation` | `keep_on_destroy = true` | не трогается | «Аллокация IP не снималась» |
Приоритет: `keep_on_destroy` > `suspend_on_destroy` > обычное удаление. Дефолты генератора: `suspend_on_destroy = true` (для сервисов с операцией `suspend`), `keep_on_destroy = false`, `adopt_existing_on_create = false`. Обратный ход: `apply` усыновляет объект по имени и делает `resume`.
---
## 6. Релизы и публикация (команды)
```bash
# Релиз провайдера (dev)
TOOLS/scripts/03_build_and_upload_provider.sh --profile TOOLS/config/dev <версия>
# → сам прогоняет 01 (YAML) + 02 (Go/доки), собирает, подписывает, заливает в реестр
# → VERSIONS.md обновить вручную
# Локальная проверка без релиза
cd provider && go build -o ../TMP/devbin/terraform-provider-nubes .
TF_CLI_CONFIG_FILE=TMP/terraformrc.dev terraform validate|plan # dev_overrides
# Документация: собрать и залить в S3
TOOLS/scripts/04_build_and_publish_docs.sh --profile TOOLS/config/dev 2.0.23
# ⚠️ версию передавать АРГУМЕНТОМ: в TOOLS/config/dev/profile.env всё ещё VERSION="2.0.17"
# docker недоступен → сборка идёт локальным mkdocs 1.6.1 + mkdocs-material
# Довезти до домена (сейчас обязательно руками: с ВМ нет маршрута до S3)
tar -C site -cf - . | ssh vps 'rm -rf ~/tmp-docs-site && mkdir -p ~/tmp-docs-site && tar -C ~/tmp-docs-site -xf -'
ssh vps 'rsync -a --delete ~/tmp-docs-site/ /var/www/tf-docs/nubes-dev/'
# vps = naeel@5.172.178.213 (алиас в ~/.ssh/config, ключ ~/.ssh/naeel_vm_id_ed25519)
# путь из secrets/HOW_TO_SSH.md устарел (файла /home/naeel/remote_dev/... нет)
# Примеры
cd tf_examples && git add -A && git commit -m "..." && git push origin master
```
Проверка доков: `curl -s -o /dev/null -w "%{http_code}" <URL>` — должно быть 200; содержимое локальной сборки лежит в `site/` (каталог в `.gitignore`).
---
## 7. Правила работы (не нарушать)
- `terraform apply` и `destroy` — **только пользователь**. Мне доступны `init/plan/validate/show/state list/state show`.
- Никаких правок, коммитов, релизов и публикаций без прямой команды; команда непонятна — **спросить, не гадать**.
- После каждой правки — коммит с осмысленным сообщением; перед правкой важных файлов — бэкап в `TMP/backup_<дата>/`.
- Секреты (`terraform.tfvars`, токены) **не печатать** в терминал и не коммитить (`*.tfvars` в `.gitignore`); сравнивать по хешу/маскировать.
- Не расширять область задач (scope creep), не «улучшать» соседние стенды и репозитории.
- Не зацикливаться: два вызова инструмента с близкими аргументами без результата — стоп и доклад.
- Проверка примера — на копии в `/tmp`, не в `tf_examples/` (там нет gitignore для `.terraform/`, служебные файлы уехали бы в публичный репозиторий).
---
## 8. Быстрые команды
```bash
# состояние стендов
cd DEV_STAND/FullPipe && terraform state list && terraform plan
cd DEV_STAND/FPipeGmail && terraform state list && terraform plan
# кластер (когда running)
kubectl config current-context # tazet@narod.ru@shturval-dev-00
kubectl get nodes
kubectl get pods -A | grep -v -E "Running|Completed"
# API ЛК (dev), народ-аккаунт
TOK=$(tr -d '\n' < secrets/narodDEV.token)
curl -s -H "Authorization: Bearer $TOK" \
-H "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36" \
-H 'Referer: https://deck-dev.ngcloud.ru/' \
'https://lk-api-gateway-dev.ngcloud.ru/api/v1/svc/instances?page=1&size=200'
# реестр провайдера (dev)
curl -s https://tf-registry.containerk8s.services.ngcloud.ru/v1/providers/nubes-dev/nubes/versions
# услуги каталога: 19 орга, 21 vDC, 22 Edge/SNAT, 26 vApp, 28 ВМ, 150 Штурвал, 148 — не то
```
---
## 9. Открытые вопросы и техдолги
1. Ref-параметр **внутри JSON** не валидируется при adopt — `provider/internal/resources_core/ref_validation.go`.
2. Регистр ключей в `lookupLiveParam` — `provider/internal/core/operation_cfs.go:38` (в прежних заметках был указан неверный файл); нужна живая проверка.
3. Тикеты в платформу: TTL/очистка Failed-подов установщика Штурвала (порядок установки компонентов до готовности Cilium); у Edge нет операции `suspend`.
4. `TOOLS/config/dev/profile.env`: `VERSION="2.0.17"` — конфиг отстаёт от реестра (`2.0.23`); правка конфига без команды не делалась.
5. `DEV_STAND/FPipeGmail`: локальный `state` пуст, но объекты в облаке **уже есть** (`fullpipe-vdc` suspended, `fullpipe-edge` running, `shturval-dev1` suspended) — при применении Terraform их усыновит.
6. `DEV_STAND/FPipeGmail` — применять или нет (сейчас `plan` = 5 ресурсов, `state` пуст) — решение за пользователем.
+6 -3
View File
@@ -7,7 +7,10 @@
| Файл | Тема | Статус | | Файл | Тема | Статус |
|---|---|---| |---|---|---|
| `CHAT_RESUME_IAC_2026-09-24.md` | **Текущая линия: IaC + `modify` (Штурвал).** Задача, позиции участников (Георгий/Дмитрий/Виталий), 10 подтверждённых фактов, почему не работает простое, каноничное решение, развилка A–E, **исправленные ошибки**, карта файлов, открытые вопросы | ✅ **Актуально — начинать отсюда** | | `CHAT_RESUME_2026-09-27_vm_in_pipeline.md` | **Актуальная линия: добавляем ВМ (услуги 26 vApp + 28 ВМ) в пайплайн.** Всё сделанное (freeze/adopt, 2.0.19→2.0.23, пример, доки), состояние стендов и облака на 27.09, точки входа по ВМ, известные проблемы, развилки | ✅ **Актуально — начинать отсюда** |
| `CHAT_RESUME_2026-09-25_shturval_freeze_state.md` | Состояние Штурвал/freeze на 25.09: диагностика стенда, режимы destroy, релиз 2.0.23, схема публикаций | ⚠️ История (всё вошло в резюме от 27.09) |
| `CHAT_RESUME_2026-09-24_shturval_freeze.md` | Adopt кластера + дизайн freeze-on-destroy | ⚠️ История |
| `CHAT_RESUME_IAC_2026-09-24.md` | Линия IaC + `modify` (Штурвал): факты, развилка A–E, карта файлов | ⚠️ История |
| `CHAT_RESUME_2026-09-22.md` | Предыдущая сессия (дата в имени) | ⚠️ История | | `CHAT_RESUME_2026-09-22.md` | Предыдущая сессия (дата в имени) | ⚠️ История |
| `CHAT_RESUME_2026-09-21.md` | Предыдущая сессия | ⚠️ История | | `CHAT_RESUME_2026-09-21.md` | Предыдущая сессия | ⚠️ История |
| `CHAT_RESUME_2026-09-20.md` | Предыдущая сессия | ⚠️ История | | `CHAT_RESUME_2026-09-20.md` | Предыдущая сессия | ⚠️ История |
@@ -17,8 +20,8 @@
## Как пользоваться ## Как пользоваться
1. Открыть `CHAT_RESUME_IAC_2026-09-24.md` — это сводка всей линии по IaC. 1. Открыть `CHAT_RESUME_2026-09-27_vm_in_pipeline.md` — это сводка всей текущей линии (и всё, что уже сделано ранее).
2. Внутри него есть ссылки на детальные документы (`../30_analysis/*`, `../20_prompts/*`). 2. Внутри него есть ссылки на детальные документы (`../30_analysis/*`, `../10_plans/*`, `../20_prompts/*`).
3. Устаревшие рестюме **не удалять** — они фиксируют состояние на свою дату (полезно для хронологии). 3. Устаревшие рестюме **не удалять** — они фиксируют состояние на свою дату (полезно для хронологии).
## Важно про даты ## Важно про даты
+2 -1
View File
@@ -116,7 +116,8 @@ API стенда ──01──▶ generated/<стенд>/resources_yaml/*.yaml
- Реестр: `tf-registry.containerk8s.services.ngcloud.ru`; бакет бинарников `nubes-terraform-registry`. - Реестр: `tf-registry.containerk8s.services.ngcloud.ru`; бакет бинарников `nubes-terraform-registry`.
- Общий конфиг реестра: `TOOLS/config/registry.env`; стенд-специфика: `TOOLS/config/<стенд>/profile.env` - Общий конфиг реестра: `TOOLS/config/registry.env`; стенд-специфика: `TOOLS/config/<стенд>/profile.env`
(`NUBES_API_ENDPOINT`, `TOKEN_FILE`, `NAMESPACE`, `VERSION`). (`NUBES_API_ENDPOINT`, `TOKEN_FILE`, `NAMESPACE`, `VERSION`).
- Список сервисов для генерации: `TOOLS/config/services_list.txt`. - Список сервисов для генерации: `TOOLS/config/<стенд>/services_list.txt` (у каждого стенда свой;
общего списка нет — см. `HISTORY/2026-09-30_yaml_pipeline_hardening.md`).
--- ---
+1 -1
View File
@@ -5,7 +5,7 @@
## Как скачать ## Как скачать
```bash ```bash
git clone https://gitea.services.ngcloud.ru/Nail/tf_examples.git git clone https://gitea.services.ngcloud.ru/terraform/tf_examples.git
cd tf_examples/CRUD cd tf_examples/CRUD
``` ```
+36 -36
View File
@@ -9,52 +9,52 @@ locals {
# PostgreSQL — общая БД для всех трёх приложений # PostgreSQL — общая БД для всех трёх приложений
# ═══════════════════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════════════════
pg_resource_name = "pg4crud2" # имя ресурса в Nubes pg_resource_name = "pg4crud2" # имя ресурса в Nubes
pg_cpu = 500 # CPU в millicores (500 = 0.5 ядра) pg_cpu = 500 # CPU в millicores (500 = 0.5 ядра)
pg_memory = 512 # память в MB pg_memory = 512 # память в MB
pg_replicas = 1 # количество реплик pg_replicas = 1 # количество реплик
pg_disk = 10 # диск в GB pg_disk = 10 # диск в GB
pg_version = "17" # версия PostgreSQL pg_version = "17" # версия PostgreSQL
pg_retain = 14 # дней хранения бэкапов pg_retain = 14 # дней хранения бэкапов
pg_schedule = "0 0 * * *" # cron расписание бэкапов (ежедневно в полночь) pg_schedule = "0 0 * * *" # cron расписание бэкапов (ежедневно в полночь)
pg_timeout = "11m" # таймаут операций create/modify pg_timeout = "11m" # таймаут операций create/modify
# ═══════════════════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════════════════
# PostgreSQL — пользователь и база данных # PostgreSQL — пользователь и база данных
# ═══════════════════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════════════════
pg_username = "user4crudpg" # имя пользователя БД pg_username = "user4crudpg" # имя пользователя БД
pg_role = "ddl_user" # роль (ddl_user = может создавать таблицы) pg_role = "ddl_user" # роль (ddl_user = может создавать таблицы)
pg_db_name = "db4crudpg" # имя базы данных pg_db_name = "db4crudpg" # имя базы данных
# ═══════════════════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════════════════
# Lucee — CFML-приложение (сервис 94) # Lucee — CFML-приложение (сервис 94)
# ═══════════════════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════════════════
lucee_resource_name = "crud-lucee" # имя ресурса в Nubes lucee_resource_name = "crud-lucee" # имя ресурса в Nubes
lucee_domain = "tflucee" # домен (станет tflucee.luceek8s.dev.nubes.ru). Имя должно быть уникальным — заменяйте на своё! lucee_domain = "tflucee" # домен (станет tflucee.luceek8s.dev.nubes.ru). Имя должно быть уникальным — заменяйте на своё!
lucee_version = "5.4" # версия Lucee (CFML engine) lucee_version = "5.4" # версия Lucee (CFML engine)
lucee_git_path = "https://gitea.services.ngcloud.ru/Nail/tfluceecrud.git" lucee_git_path = "https://gitea.services.ngcloud.ru/terraform/tfluceecrud.git"
lucee_cpu = 300 # CPU в millicores lucee_cpu = 300 # CPU в millicores
lucee_memory = 512 # память в MB lucee_memory = 512 # память в MB
lucee_replicas = 1 # количество реплик lucee_replicas = 1 # количество реплик
# ═══════════════════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════════════════
# Таблица CRUD — общая для всех трёх приложений # Таблица CRUD — общая для всех трёх приложений
# ═══════════════════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════════════════
crud_table_name = "crud_items" # имя таблицы (TABLE_NAME в env) crud_table_name = "crud_items" # имя таблицы (TABLE_NAME в env)
# ═══════════════════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════════════════
# Flask — Python-приложение (сервис 89) # Flask — Python-приложение (сервис 89)
# ═══════════════════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════════════════
flask_resource_name = "crud-flask" # имя ресурса в Nubes flask_resource_name = "crud-flask" # имя ресурса в Nubes
flask_domain = "tfflask" # домен (станет tfflask.pythonk8s.dev.nubes.ru). Имя должно быть уникальным — заменяйте на своё! flask_domain = "tfflask" # домен (станет tfflask.pythonk8s.dev.nubes.ru). Имя должно быть уникальным — заменяйте на своё!
flask_git_path = "https://gitea.services.ngcloud.ru/Nail/tfflaskcrud.git" flask_git_path = "https://gitea.services.ngcloud.ru/terraform/tfflaskcrud.git"
flask_cpu = 300 # CPU в millicores flask_cpu = 300 # CPU в millicores
flask_memory = 512 # память в MB flask_memory = 512 # память в MB
flask_replicas = 1 # количество реплик flask_replicas = 1 # количество реплик
# ═══════════════════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════════════════
# Node.js — Express-приложение (сервис 95) # Node.js — Express-приложение (сервис 95)
@@ -62,11 +62,11 @@ locals {
nodejs_resource_name = "crud-nodejs" # имя ресурса в Nubes nodejs_resource_name = "crud-nodejs" # имя ресурса в Nubes
nodejs_domain = "tfnodejs" # домен (станет tfnodejs.<суффикс>.dev.nubes.ru). Имя должно быть уникальным — заменяйте на своё! nodejs_domain = "tfnodejs" # домен (станет tfnodejs.<суффикс>.dev.nubes.ru). Имя должно быть уникальным — заменяйте на своё!
nodejs_git_path = "https://gitea.services.ngcloud.ru/Nail/tfnodejscrud.git" nodejs_git_path = "https://gitea.services.ngcloud.ru/terraform/tfnodejscrud.git"
nodejs_cpu = 300 # CPU в millicores nodejs_cpu = 300 # CPU в millicores
nodejs_memory = 512 # память в MB nodejs_memory = 512 # память в MB
nodejs_replicas = 1 # количество реплик nodejs_replicas = 1 # количество реплик
nodejs_timeout = "15m" # таймаут операций create/modify nodejs_timeout = "15m" # таймаут операций create/modify
# ═══════════════════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════════════════
# JDBC — параметры подключения Lucee к PostgreSQL # JDBC — параметры подключения Lucee к PostgreSQL
@@ -75,14 +75,14 @@ locals {
jdbc_class = "org.postgresql.Driver" jdbc_class = "org.postgresql.Driver"
jdbc_bundle_name = "org.postgresql.jdbc" jdbc_bundle_name = "org.postgresql.jdbc"
jdbc_bundle_version = "42.6.0" jdbc_bundle_version = "42.6.0"
jdbc_conn_limit = "5" # макс. количество соединений jdbc_conn_limit = "5" # макс. количество соединений
jdbc_live_timeout = "15" # таймаут неактивного соединения (минут) jdbc_live_timeout = "15" # таймаут неактивного соединения (минут)
jdbc_validate = "false" # валидация соединения при выдаче из пула jdbc_validate = "false" # валидация соединения при выдаче из пула
# ═══════════════════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════════════════
# PostgreSQL — общие параметры подключения # PostgreSQL — общие параметры подключения
# ═══════════════════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════════════════
pg_port = "5432" # порт PostgreSQL pg_port = "5432" # порт PostgreSQL
pg_ssl_mode = "require" # SSL-режим (require = обязательно TLS) pg_ssl_mode = "require" # SSL-режим (require = обязательно TLS)
} }
+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
}
@@ -0,0 +1,60 @@
# =============================================================================
# Ресурсы-модификаторы (операции modify, которых нет в create-схеме ресурсов)
#
# Порядок строго такой:
# орга (создана вручную в ЛК)
# -> nubes_vc_vdc.vdc
# -> nubes_vc_nsxt.edge
# -> nubes_vc_org_ip_allocation (выделение внешних IP на орге)
# -> nubes_vc_nsxt_snat (SNAT на эдже этим ipSpace)
#
# Почему аллокация ПОСЛЕ эджа: платформа строит список ipSpace из состояния
# `job.vcd.networkProvider` / `job.vcd.providerGateway`, то есть требует уже
# созданный vDC и Edge. Иначе modify на орге падает
# («Can't cast Complex Object Type Struct to String»).
# =============================================================================
# 1. Внешние IP на организации (modify: vIPConfigure, массив перезаписывается целиком)
resource "nubes_vc_org_ip_allocation" "org_ip" {
organization = var.organization
vip_configure = jsonencode([
{
name = var.ip_space_name
count = var.ip_count
}
])
# true = «заморозка»: destroy не трогает квоту внешних IP (кластер Штурвала держит
# адреса, опустить count ниже занятых платформа не даёт). Для полного удаления — false
# (и только после удаления кластера).
keep_on_destroy = true
depends_on = [nubes_vc_nsxt.edge]
}
# 2. SNAT на эдже (modify: ipSpaceName)
resource "nubes_vc_nsxt_snat" "snat" {
nsxt_uid = nubes_vc_nsxt.edge.id
ip_space_name = var.ip_space_name
# true = «заморозка»: destroy не выключает SNAT на эдже. Для полного удаления — false.
keep_on_destroy = true
# ipSpace должен быть уже выделен на организации
depends_on = [nubes_vc_org_ip_allocation.org_ip]
}
output "allocated_org_ip" {
description = "Выделено внешних IP на организации"
value = {
organization = var.organization
ip_space_name = var.ip_space_name
ip_count = var.ip_count
}
}
output "snat_ip_space" {
description = "ipSpace, включённый как SNAT на эдже"
value = nubes_vc_nsxt_snat.snat.ip_space_name
}
@@ -0,0 +1,29 @@
output "vdc_id" {
description = "UID созданного VDC"
value = nubes_vc_vdc.vdc.id
}
output "vdc_name" {
description = "Имя VDC"
value = nubes_vc_vdc.vdc.resource_name
}
output "vdc_state_params" {
description = "Параметры состояния VDC из API"
value = nubes_vc_vdc.vdc.state_params
}
output "nsxt_id" {
description = "UID созданного Edge (vc_nsxt)"
value = nubes_vc_nsxt.edge.id
}
output "nsxt_name" {
description = "Имя Edge (vc_nsxt)"
value = nubes_vc_nsxt.edge.resource_name
}
output "nsxt_state_params" {
description = "Параметры состояния Edge (vc_nsxt) из API"
value = nubes_vc_nsxt.edge.state_params
}
@@ -0,0 +1,4 @@
provider "nubes" {
api_token = var.api_token
api_endpoint = var.api_endpoint
}
@@ -0,0 +1,161 @@
# =============================================================================
# Kubernetes кластер Штурвал — сервис 150, ресурс nubes_k8s_sthutrval_cluster
# (НЕ 148 «Менеджмент Kubernetes кластер Штурвал» — это другой сервис)
#
# Всё, что относится к Штурвалу, лежит ТОЛЬКО в этом файле: переменные, их
# значения по умолчанию и сам ресурс. Чтобы выключить Штурвал — удалить файл
# или закомментировать ресурс.
#
# Порядок (чек-лист из инструкции на услугу в ЛК):
# 1) Организация в Cloud Director — создана вручную в ЛК
# 2) nubes_vc_vdc.vdc — есть
# 3) nubes_vc_nsxt.edge — есть, обязательно ALB + AVI VS >= 3
# 4) внешние адреса в организации — суммарно >= 3 (nubes_vc_org_ip_allocation)
# 5) SNAT на Edge — nubes_vc_nsxt_snat
# 6) Kubernetes кластер Штурвал — этот ресурс
#
# Минимальные требования к кластеру: мастер-нод >= 1, воркер-нод >= 1,
# 4 vCPU / 8 GB RAM / 50 GB диска на ноду.
# =============================================================================
# --- Переменные Штурвала ---
variable "shturval_resource_name" {
type = string
default = "shturval-dev"
description = "Имя услуги «Kubernetes кластер Штурвал» в ЛК"
}
variable "shturval_cluster_name" {
type = string
default = "shturval-dev-00"
description = "Имя кластера внутри Штурвала"
}
variable "shturval_app_version" {
type = string
default = "2.14.0"
description = "Версия Штурвала (значение по умолчанию платформы — 2.14.0)"
}
variable "shturval_cp_sizing_policy" {
type = string
default = "TKG 4CPU 8RAM"
description = "Политика размера control plane: 4 vCPU / 8 GB (минимум по инструкции). Должна существовать в ресурсной платформе vDC — список политик берётся из услуги «Виртуальный датацентр»"
}
variable "shturval_cp_sizing_disk" {
type = number
default = 50
description = "Диск control plane, ГБ (минимум 50)"
}
variable "shturval_cp_count" {
type = number
default = 1
description = "Количество мастер-нод: 1, 3 или 5"
}
variable "shturval_worker_group_name" {
type = string
default = "workers-shturval-dev"
description = "Имя группы воркеров (уникальное в кластере; допустимы строчные латинские буквы, цифры и дефис)"
}
variable "shturval_worker_sizing_policy" {
type = string
default = "TKG 4CPU 8RAM"
description = "Политика размера воркеров: 4 vCPU / 8 GB (минимум по инструкции)"
}
variable "shturval_worker_sizing_disk" {
type = number
default = 50
description = "Диск воркеров, ГБ (минимум 50)"
}
variable "shturval_worker_count" {
type = number
default = 1
description = "Количество воркер-нод (минимум 1)"
}
# --- Значения, которые собираются из переменных ---
locals {
# Группы воркеров передаются JSON-строкой ВНУТРЬ услуги как есть, поэтому ключи
# должны быть ровно такими, как в манифесте услуги 150: groupName, sizingPolicy,
# sizingDisk, count, autoscale, labelDeck.
# ВНИМАНИЕ: в сгенерированном примере провайдера (docs → Example) ключи показаны
# в snake_case — это ошибка генератора, платформа на них падает с
# «Cannot invoke method split() on null object» (не находит groupName → null).
shturval_worker_config = jsonencode([
{
groupName = var.shturval_worker_group_name
sizingPolicy = var.shturval_worker_sizing_policy
sizingDisk = var.shturval_worker_sizing_disk
count = var.shturval_worker_count
autoscale = false # автоскейл выключен
labelDeck = true # разрешить разворачивать услуги из ЛК на этих нодах
}
])
}
# --- Ресурс Штурвала ---
resource "nubes_k8s_sthutrval_cluster" "shturval" {
resource_name = var.shturval_resource_name
# Кластер Штурвала уже существует (инстанс «shturval-dev») и в проде не
# удаляется неделями, поэтому ресурс должен УСЫНОВИТЬ существующий инстанс,
# а не падать с «РЕСУРС С ТАКИМ ИМЕНЕМ УЖЕ СУЩЕСТВУЕТ (SUSPEND)».
# Проверка/adopt выполняются в Create на apply (в plan будет «will be created»).
adopt_existing_on_create = true
# «Заморозка»: destroy приостанавливает кластер (suspend), а не удаляет.
# Следующий apply усыновит его и разморозит (resume).
suspend_on_destroy = true
# Штурвал создаётся долго (десятки минут) — поднимаем таймаут ожидания,
# иначе провайдер сдаётся на дефолтных 600 с.
operation_timeout = "60m"
startup_configuration = {
# vDC и Edge из этого же конфига (обязательные поля)
vdc_uid = nubes_vc_vdc.vdc.id
nsxt_uid = nubes_vc_nsxt.edge.id
cluster_name = var.shturval_cluster_name
# Дополнительные возможности кластера (в ЛК — галочки при создании)
ex_logging = true # логи в Loki (без него логи услуг не видны в ЛК)
ex_monitoring = true # метрики в VictoriaMetrics (без него метрик в ЛК нет)
ex_local_csi = true
ex_vip = true
ex_update = true
ex_ingress = true
ex_named_csi = true
}
cluster_configuration = {
app_version = var.shturval_app_version
}
control_plane_configuration = {
sizing_policy = var.shturval_cp_sizing_policy
sizing_disk = var.shturval_cp_sizing_disk
count = var.shturval_cp_count
}
worker_configuration = local.shturval_worker_config
access_configuration = {
need_external_address_api = true # внешний адрес для Kubernetes API (false недопустим)
access_ip_list_api = jsonencode([]) # пусто = доступ всем
need_external_address_ingress = true # внешний адрес для Ingress
access_ip_list_ingress = jsonencode([]) # пусто = доступ всем
}
# Кластер поднимается только после готовой сети: vDC -> Edge -> внешние IP -> SNAT
depends_on = [nubes_vc_nsxt_snat.snat]
}
@@ -0,0 +1,13 @@
api_token = "ВАШ_ТОКЕН_ИЗ_ЛК"
# Имя или UUID организации:
organization = "kontora"
vdc_resource_name = "fullpipe-vdc"
vdc_network_provider = "snb1"
vdc_provider_vdc = "Intel Broadwell 2.4"
vdc_cpu_allocated = 8
vdc_cpu_guaranteed = 0
vdc_mem_allocated = 32
vdc_storage_config = "[{\"name\":\"SATA\",\"size\":\"200\"}]"
@@ -0,0 +1,116 @@
variable "api_token" {
type = string
sensitive = true
description = "API-токен Nubes"
}
variable "api_endpoint" {
type = string
default = "https://lk-api-gateway-dev.ngcloud.ru/api/v1/svc"
description = "API Gateway URL"
}
# Имя (display_name, напр. "kontora") ИЛИ UUID организации из ЛК
variable "organization" {
type = string
description = "Имя или UUID организации (vc_org)"
}
# --- Модификаторы (IP на орге + SNAT на эдже) ---
variable "ip_space_name" {
type = string
description = "Имя ipSpace, доступное организации (смотреть в ЛК, напр. internet-ipv4-v1)"
}
variable "ip_count" {
type = string
default = "3"
description = "Сколько внешних IP выделить на организации (count — строка)"
}
variable "vdc_resource_name" {
type = string
default = "fullpipe-vdc"
description = "Имя VDC"
}
variable "vdc_network_provider" {
type = string
default = null
description = "Сетевой провайдер. Заполнить значением из текущей страницы ЛК"
}
variable "vdc_provider_vdc" {
type = string
default = null
description = "Provider VDC. Заполнить значением из текущей страницы ЛК"
}
variable "vdc_cpu_allocated" {
type = number
default = 8
description = "vCPU (шт.)"
}
variable "vdc_cpu_guaranteed" {
type = number
default = 0
description = "Резервирование vCPU (%, допустимо: 0, 50, 80)"
}
variable "vdc_mem_allocated" {
type = number
default = 32
description = "RAM (GB)"
}
variable "vdc_storage_config" {
type = string
default = "[{\"name\":\"SATA\",\"size\":\"200\"}]"
description = "Дисковое хранилище (JSON-массив, size в GB). Имя политики должно существовать в ресурсном пуле (например, SATA, SSD)"
}
# --- vc_nsxt (Сетевой шлюз периметра / Edge) ---
variable "nsxt_resource_name" {
type = string
default = "fullpipe-edge"
description = "Имя Edge (vc_nsxt)"
}
variable "nsxt_vdc_type" {
type = string
default = "vdc"
description = "Тип родительской услуги: vdc или vdcGroup"
}
variable "nsxt_need_enable_avi" {
type = bool
default = true
description = "Включить AVI Load Balancer (ALB)"
}
variable "nsxt_virtual_services_count" {
type = number
default = 3
description = "Кол-во виртуальных сервисов на AVI (1..4; Штурвал: ≥ 3)"
}
variable "nsxt_ip_addr_pool" {
type = string
default = "10.10.102.0/24"
description = "Адресный пул routed-сети (маска /24 обязательна)"
}
variable "nsxt_main_dns" {
type = string
default = "81.22.46.22"
description = "Основной DNS"
}
variable "nsxt_second_dns" {
type = string
default = "185.247.187.77"
description = "Второй DNS"
}
+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
}
@@ -0,0 +1,10 @@
terraform {
required_version = ">= 1.5.0"
required_providers {
nubes = {
source = "tf-registry.containerk8s.services.ngcloud.ru/nubes-dev/nubes"
version = "2.0.23"
}
}
}
@@ -0,0 +1,85 @@
# vDC → Edge → внешние IP → SNAT
Пример поднимает сетевую основу в Nubes:
- виртуальный датацентр (vDC);
- сетевой шлюз периметра (Edge);
- внешние IP на организации;
- SNAT на шлюзе.
Кластер Штурвал сюда **не входит** — он разворачивается долго, отдельным шагом.
Организацию создайте заранее в ЛК: Terraform её не создаёт и не удаляет.
**Перед началом:** нужен установленный Terraform 1.5 или новее. Провайдер скачается сам
при `terraform init` — отдельно ставить ничего не нужно.
## 1. Скачайте пример
```bash
git clone https://gitea.services.ngcloud.ru/Nail/tf_examples.git
cd tf_examples/fullpipe_chain
```
## 2. Возьмите значения в ЛК
| Значение | Где взять | Пример |
|---|---|---|
| `api_token` | ЛК → Профиль → Токены → «Технический» | `eyJhbGciOi...` |
| `organization` | ЛК → услуга «Организация в Cloud Director» → название услуги | `organ` |
| `vdc_network_provider` | ЛК → создание vDC → «Сетевой провайдер» | `snb1` |
| `vdc_provider_vdc` | ЛК → создание vDC → «Provider VDC» | `Intel Broadwell 2.4` |
| `vdc_storage_config` | ЛК → создание vDC → доступные дисковые политики | `SATA` |
| `ip_space_name` | ЛК → карточка организации → внешние IP | `internet-ipv4-v1` |
Остальные значения уже подставлены в `terraform.tfvars.example` — меняйте, если нужно.
## 3. Заполните terraform.tfvars
```bash
cp terraform.tfvars.example terraform.tfvars
nano terraform.tfvars # подставьте значения из шага 2
```
Полный список параметров — в `variables.tf`, у каждой переменной есть описание.
## 4. Выполните команды
```bash
terraform init # один раз — скачает провайдер
terraform plan # покажет, что будет создано (4 ресурса), ничего не меняет
terraform apply # создаст (подтвердить: yes)
```
## 5. Проверьте результат
```bash
terraform output # UUID и имена созданных услуг
```
И в ЛК: появились vDC и Edge, на организации выделены внешние IP, на шлюзе включён SNAT.
Повторный `terraform plan` должен показать `No changes`.
## 6. Удаление
```bash
terraform destroy
```
Порядок обратный: SNAT выключается, квота внешних IP обнуляется, затем удаляется шлюз,
а vDC приостанавливается (данные сохраняются).
**Организация не удаляется.**
## Состав файлов
| Файл | Что делает |
|---|---|
| `versions.tf` | версия Terraform и провайдера Nubes |
| `provider.tf` | подключение к API (токен, адрес) |
| `variables.tf` | все параметры с описанием |
| `vdc.tf` | виртуальный датацентр |
| `edge.tf` | сетевой шлюз периметра (Edge) |
| `modifiers.tf` | внешние IP на организации + SNAT на шлюзе |
| `outputs.tf` | UUID и имена созданных услуг |
| `terraform.tfvars.example` | шаблон значений (копируется в `terraform.tfvars`) |
`terraform.tfvars` с токеном никому не передавайте и не коммитьте в git.
@@ -0,0 +1,23 @@
# Сетевой шлюз периметра (Edge) — создаётся внутри vDC, объявленного в vdc.tf.
resource "nubes_vc_nsxt" "edge" {
# имя услуги в ЛК (любое, удобное вам)
resource_name = var.nsxt_resource_name
# родительская услуга: vdc (нужен vdc_uid) или vdcGroup
vdc_type = var.nsxt_vdc_type
# ссылка на vDC: сначала создаётся vDC, потом шлюз
vdc_uid = nubes_vc_vdc.vdc.id
# балансировщик (ALB): включён; виртуальных сервисов 1..4,
# для кластера Штурвал нужно не меньше 3
need_enable_avi = var.nsxt_need_enable_avi
virtual_services_count = var.nsxt_virtual_services_count
# сеть шлюза: пул адресов (маска /24 обязательна) и DNS для машин
routed_net_configuration = {
ip_addr_pool = var.nsxt_ip_addr_pool
main_dns = var.nsxt_main_dns
second_dns = var.nsxt_second_dns
}
}
@@ -0,0 +1,61 @@
# =============================================================================
# Ресурсы, которые выполняют операции modify над уже созданными услугами.
#
# Порядок строго такой:
# организация (создаётся заранее в ЛК)
# -> nubes_vc_vdc.vdc
# -> nubes_vc_nsxt.edge
# -> nubes_vc_org_ip_allocation (внешние IP на организации)
# -> nubes_vc_nsxt_snat (SNAT на шлюзе этим ipSpace)
#
# Аллокация IP идёт ПОСЛЕ шлюза: платформа строит список доступных ipSpace
# только когда vDC и Edge уже созданы. Если поменять порядок — modify упадёт
# с ошибкой «Can't cast Complex Object Type Struct to String».
# =============================================================================
# 1. Внешние IP на организации.
resource "nubes_vc_org_ip_allocation" "org_ip" {
# та же организация, что у vDC: название услуги из ЛК или её UUID
organization = var.organization
# сколько IP выделить: name — имя ipSpace, count — количество (строкой).
# массив передаётся целиком, поэтому старые значения перезаписываются
vip_configure = jsonencode([
{
name = var.ip_space_name
count = var.ip_count
}
])
# при destroy: false — квота обнуляется (count=0); true — оставить как есть
keep_on_destroy = false
depends_on = [nubes_vc_nsxt.edge]
}
# 2. SNAT на шлюзе: внешние адреса для машин.
resource "nubes_vc_nsxt_snat" "snat" {
# UUID шлюза, созданного выше
nsxt_uid = nubes_vc_nsxt.edge.id
ip_space_name = var.ip_space_name
# при destroy: false — SNAT выключается (no-needed); true — оставить как есть
keep_on_destroy = false
# ipSpace должен быть уже выделен на организации
depends_on = [nubes_vc_org_ip_allocation.org_ip]
}
output "allocated_org_ip" {
description = "Внешние IP, выделенные на организации"
value = {
organization = var.organization
ip_space_name = var.ip_space_name
ip_count = var.ip_count
}
}
output "snat_ip_space" {
description = "ipSpace, включённый как SNAT на шлюзе"
value = nubes_vc_nsxt_snat.snat.ip_space_name
}
@@ -0,0 +1,32 @@
# Значения, нужные для дальнейшей работы (например, чтобы привязать к шлюзу
# кластер Штурвал или другие услуги).
output "vdc_id" {
description = "UUID созданного vDC"
value = nubes_vc_vdc.vdc.id
}
output "vdc_name" {
description = "Имя vDC"
value = nubes_vc_vdc.vdc.resource_name
}
output "nsxt_id" {
description = "UUID созданного Edge (сетевого шлюза периметра)"
value = nubes_vc_nsxt.edge.id
}
output "nsxt_name" {
description = "Имя Edge"
value = nubes_vc_nsxt.edge.resource_name
}
output "vdc_state_params" {
description = "Параметры vDC из API (что реально создалось)"
value = nubes_vc_vdc.vdc.state_params
}
output "nsxt_state_params" {
description = "Параметры Edge из API (что реально создалось)"
value = nubes_vc_nsxt.edge.state_params
}
@@ -0,0 +1,6 @@
# Подключение к API Nubes.
# Токен и адрес API берутся из terraform.tfvars (переменные api_token, api_endpoint).
provider "nubes" {
api_token = var.api_token
api_endpoint = var.api_endpoint
}
@@ -0,0 +1,53 @@
# =============================================================================
# Шаблон значений для fullpipe_chain.
#
# 1. Скопируйте файл: cp terraform.tfvars.example terraform.tfvars
# 2. Заполните значения ниже (откуда брать — в комментариях и в README.md).
# 3. terraform.tfvars НЕ публикуйте и не коммитьте: в нём токен доступа.
# =============================================================================
# ЛК → Профиль → Токены → «Технический»
api_token = "eyJhbGciOi..."
# Название услуги «Организация в Cloud Director» в ЛК (например: organ).
# Организацию создайте заранее в ЛК — Terraform её не создаёт и не удаляет.
organization = "organ"
# Имя ipSpace для внешнего IP. Смотреть в ЛК в карточке организации.
ip_space_name = "internet-ipv4-v1"
# Сколько внешних IP выделить (строкой).
ip_count = "3"
# --- vDC (виртуальный датацентр) ---
# Имя vDC в ЛК — любое.
vdc_resource_name = "fullpipe-vdc"
# ЛК → создание vDC → «Сетевой провайдер»
vdc_network_provider = "snb1"
# ЛК → создание vDC → «Provider VDC»
vdc_provider_vdc = "Intel Broadwell 2.4"
# vCPU, шт.
vdc_cpu_allocated = 8
# Резервирование vCPU, %: допустимы 0, 50 или 80
vdc_cpu_guaranteed = 0
# RAM, ГБ
vdc_mem_allocated = 32
# Дисковая политика: JSON-массив, name — имя политики из ЛК, size — размер в ГБ.
# Пример: политика SATA, 200 ГБ.
vdc_storage_config = "[{\"name\":\"SATA\",\"size\":\"200\"}]"
# --- Edge (сетевой шлюз периметра) ---
# Имя Edge в ЛК — любое.
nsxt_resource_name = "fullpipe-edge"
# Родительская услуга: vdc или vdcGroup.
nsxt_vdc_type = "vdc"
# Балансировщик AVI (ALB).
nsxt_need_enable_avi = true
# Виртуальных сервисов AVI: 1..4. Для кластера Штурвал нужно не меньше 3.
nsxt_virtual_services_count = 3
# Адресный пул routed-сети шлюза. Маска /24 обязательна.
nsxt_ip_addr_pool = "10.10.102.0/24"
# DNS для машин за шлюзом.
nsxt_main_dns = "81.22.46.22"
nsxt_second_dns = "185.247.187.77"
@@ -0,0 +1,123 @@
# =============================================================================
# Переменные. Все значения задаются в terraform.tfvars.
# Шаблон файла: terraform.tfvars.example (скопируйте в terraform.tfvars).
# Где взять каждое значение — в описании переменной и в README.md.
# =============================================================================
variable "api_token" {
type = string
sensitive = true
description = "Токен API. ЛК → Профиль → Токены → «Технический». Никому не передавайте: токен даёт полный доступ к услугам."
}
variable "api_endpoint" {
type = string
default = "https://lk-api-gateway-dev.ngcloud.ru/api/v1/svc"
description = "Адрес API. Менять не нужно."
}
# --- Организация ---
variable "organization" {
type = string
description = "Название услуги «Организация в Cloud Director» в ЛК (например: organ). Можно указать и её UUID. Организация создаётся заранее в ЛК — Terraform её не создаёт."
}
variable "ip_space_name" {
type = string
description = "Имя ipSpace для внешнего IP. Смотреть в ЛК в карточке организации (например: internet-ipv4-v1)."
}
variable "ip_count" {
type = string
default = "3"
description = "Сколько внешних IP выделить на организации. Строкой — так его отдаёт платформа."
}
# --- vDC (виртуальный датацентр) ---
variable "vdc_resource_name" {
type = string
default = "fullpipe-vdc"
description = "Имя vDC в ЛК. Любое, удобное вам."
}
variable "vdc_network_provider" {
type = string
default = null
description = "Сетевой провайдер vDC. Взять в ЛК на странице создания vDC (например: snb1)."
}
variable "vdc_provider_vdc" {
type = string
default = null
description = "Provider VDC. Взять в ЛК на странице создания vDC (например: Intel Broadwell 2.4)."
}
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, ГБ."
}
variable "vdc_storage_config" {
type = string
default = "[{\"name\":\"SATA\",\"size\":\"200\"}]"
description = "Дисковая политика: JSON-массив, name — имя политики из ЛК, size — размер в ГБ (строкой). Имя политики должно существовать в ресурсном пуле (например: SATA, SSD)."
}
# --- Edge (сетевой шлюз периметра, vc_nsxt) ---
variable "nsxt_resource_name" {
type = string
default = "fullpipe-edge"
description = "Имя Edge в ЛК. Любое, удобное вам."
}
variable "nsxt_vdc_type" {
type = string
default = "vdc"
description = "Тип родительской услуги: vdc (нужен vdc_uid) или vdcGroup."
}
variable "nsxt_need_enable_avi" {
type = bool
default = true
description = "Включить балансировщик AVI (ALB)."
}
variable "nsxt_virtual_services_count" {
type = number
default = 3
description = "Количество виртуальных сервисов на AVI: 1..4. Для кластера Штурвал нужно не меньше 3."
}
variable "nsxt_ip_addr_pool" {
type = string
default = "10.10.102.0/24"
description = "Адресный пул routed-сети шлюза. Маска /24 обязательна."
}
variable "nsxt_main_dns" {
type = string
default = "81.22.46.22"
description = "Основной DNS для машин за шлюзом."
}
variable "nsxt_second_dns" {
type = string
default = "185.247.187.77"
description = "Второй DNS для машин за шлюзом."
}
@@ -0,0 +1,27 @@
# Виртуальный датацентр (vDC) — создаётся внутри организации из terraform.tfvars.
resource "nubes_vc_vdc" "vdc" {
# имя услуги в ЛК (любое, удобное вам)
resource_name = var.vdc_resource_name
# организация, к которой привязан vDC: название услуги из ЛК или её UUID
organization_uid = var.organization
# значения из ЛК со страницы создания vDC
network_provider = var.vdc_network_provider
provider_vdc = var.vdc_provider_vdc
# квоты: vCPU (шт.), резервирование vCPU (%), RAM (ГБ)
# для cpu_guaranteed допустимы только 0, 50 или 80
cpu_allocated = var.vdc_cpu_allocated
cpu_guaranteed = var.vdc_cpu_guaranteed
mem_allocated = var.vdc_mem_allocated
# дисковые политики: JSON-массив, name — имя политики из ЛК, size — размер в ГБ
storage_config = var.vdc_storage_config
# при destroy не удалять vDC, а приостановить (данные сохраняются)
suspend_on_destroy = true
# если услуга с таким именем уже есть — подключиться к ней, а не падать с ошибкой
adopt_existing_on_create = true
}
@@ -0,0 +1,12 @@
# Версия Terraform и провайдера Nubes.
# Провайдер скачается сам при `terraform init` — вручную ставить ничего не нужно.
terraform {
required_version = ">= 1.5.0"
required_providers {
nubes = {
source = "tf-registry.containerk8s.services.ngcloud.ru/nubes-dev/nubes"
version = "2.0.21"
}
}
}
@@ -0,0 +1,118 @@
# Как развернуть vDC, Edge, внешние IP и SNAT
Пошаговая инструкция. Готовые файлы примера — в репозитории `tf_examples`, папка `fullpipe_chain`.
Что получится в итоге:
- виртуальный датацентр (vDC);
- сетевой шлюз периметра (Edge);
- внешние IP на организации;
- SNAT на шлюзе.
Кластер Штурвал в эту инструкцию не входит — он разворачивается долго, отдельным шагом.
**Организацию создайте заранее в ЛК** — Terraform её не создаёт и не удаляет.
## 1. Скачайте пример
```bash
git clone https://gitea.services.ngcloud.ru/Nail/tf_examples.git
cd tf_examples/fullpipe_chain
```
Понадобится Terraform 1.5 или новее. Провайдер скачается сам при `terraform init`.
## 2. Возьмите значения в ЛК
| Значение | Где взять | Пример |
|---|---|---|
| `api_token` | ЛК → Профиль → Токены → «Технический» | `eyJhbGciOi...` |
| `organization` | ЛК → услуга «Организация в Cloud Director» → название услуги | `organ` |
| `vdc_network_provider` | ЛК → создание vDC → «Сетевой провайдер» | `snb1` |
| `vdc_provider_vdc` | ЛК → создание vDC → «Provider VDC» | `Intel Broadwell 2.4` |
| `vdc_storage_config` | ЛК → создание vDC → доступные дисковые политики | `SATA` |
| `ip_space_name` | ЛК → карточка организации → внешние IP | `internet-ipv4-v1` |
## 3. Заполните значения
```bash
cp terraform.tfvars.example terraform.tfvars
nano terraform.tfvars
```
Файл `terraform.tfvars` выглядит так:
```hcl
# Токен из ЛК
api_token = "eyJhbGciOi..."
# Организация из ЛК (создана заранее)
organization = "organ"
# Внешние IP
ip_space_name = "internet-ipv4-v1"
ip_count = "3"
# vDC
vdc_resource_name = "fullpipe-vdc" # имя услуги в ЛК, любое
vdc_network_provider = "snb1" # ЛК → создание vDC → «Сетевой провайдер»
vdc_provider_vdc = "Intel Broadwell 2.4" # ЛК → создание vDC → «Provider VDC»
vdc_cpu_allocated = 8 # vCPU, шт.
vdc_cpu_guaranteed = 0 # резервирование vCPU, %: 0, 50 или 80
vdc_mem_allocated = 32 # RAM, ГБ
vdc_storage_config = "[{\"name\":\"SATA\",\"size\":\"200\"}]" # политика и размер, ГБ
# Edge (сетевой шлюз периметра)
nsxt_resource_name = "fullpipe-edge" # имя услуги в ЛК, любое
nsxt_vdc_type = "vdc" # родитель: vdc или vdcGroup
nsxt_need_enable_avi = true # балансировщик AVI (ALB)
nsxt_virtual_services_count = 3 # виртуальных сервисов AVI: 1..4 (Штурвал: не меньше 3)
nsxt_ip_addr_pool = "10.10.102.0/24" # пул адресов routed-сети, маска /24
nsxt_main_dns = "81.22.46.22" # основной DNS
nsxt_second_dns = "185.247.187.77" # второй DNS
```
Описание всех параметров — в `variables.tf`. `terraform.tfvars` с токеном никому не передавайте
и не коммитьте в git.
## 4. Выполните команды
```bash
terraform init # один раз — скачает провайдер
terraform plan # покажет, что будет создано: 4 ресурса
terraform apply # создаст (подтвердить: yes)
```
## 5. Проверьте результат
```bash
terraform output # UUID и имена созданных услуг
```
И в ЛК: появились vDC и Edge, на организации выделены внешние IP, на шлюзе включён SNAT.
Повторный `terraform plan` должен показать `No changes`.
## 6. Удаление
```bash
terraform destroy
```
Порядок обратный: SNAT выключается, квота внешних IP обнуляется, затем удаляется шлюз,
а vDC приостанавливается (данные сохраняются). **Организация не удаляется.**
## Файлы примера
| Файл | Что делает |
|---|---|
| `versions.tf` | версия Terraform и провайдера Nubes |
| `provider.tf` | подключение к API (токен, адрес) |
| `variables.tf` | все параметры с описанием |
| `vdc.tf` | виртуальный датацентр |
| `edge.tf` | сетевой шлюз периметра (Edge) |
| `modifiers.tf` | внешние IP на организации + SNAT на шлюзе |
| `outputs.tf` | UUID и имена созданных услуг |
| `terraform.tfvars.example` | шаблон значений (копируется в `terraform.tfvars`) |
Подробнее про два последних ресурса — на странице
[«Ресурсы-модификаторы (IP организации, SNAT)»](../modifiers/org_ip_and_snat.md).
@@ -0,0 +1,32 @@
# Примеры Terraform для Nubes
> ## Внимание: рабочая папка одна
>
> Для работы используйте **только** [`fullpipe_chain`](fullpipe_chain/) — это пример
> «vDC → Edge → внешние IP → SNAT». Он прогнан на живом стенде (создание и удаление)
> и снабжён пошаговой инструкцией.
>
> Остальные папки репозитория — черновики: могут быть сырыми, содержать неактуальные
> версии провайдера и работать неправильно. **Не используйте их.**
Пошаговая инструкция: <https://tf-docs.nodejsk8s.dev.nubes.ru/nubes-dev/curated/pipeline/vdc_edge_ip_snat/>
## fullpipe_chain — что использовать
```bash
git clone https://gitea.services.ngcloud.ru/Nail/tf_examples.git
cd tf_examples/fullpipe_chain
cp terraform.tfvars.example terraform.tfvars # заполнить значениями из ЛК
terraform init
terraform plan # покажет 4 ресурса, ничего не меняет
terraform apply # подтвердить: yes
```
Подробности и список значений — в [README примера](fullpipe_chain/README.md).
Организацию создайте заранее в ЛК: Terraform её не создаёт и не удаляет.
## Остальные папки — не использовать
`CRUD`, `modify_resources`, `SHTURVAL_MGMT`, `iot-rmq-demo` — черновики от прошлых работ.
Могут быть сырыми и не поддерживаются. Берите только `fullpipe_chain`.
+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
}
@@ -0,0 +1,60 @@
# =============================================================================
# Ресурсы-модификаторы (операции modify, которых нет в create-схеме ресурсов)
#
# Порядок строго такой:
# орга (создана вручную в ЛК)
# -> nubes_vc_vdc.vdc
# -> nubes_vc_nsxt.edge
# -> nubes_vc_org_ip_allocation (выделение внешних IP на орге)
# -> nubes_vc_nsxt_snat (SNAT на эдже этим ipSpace)
#
# Почему аллокация ПОСЛЕ эджа: платформа строит список ipSpace из состояния
# `job.vcd.networkProvider` / `job.vcd.providerGateway`, то есть требует уже
# созданный vDC и Edge. Иначе modify на орге падает
# («Can't cast Complex Object Type Struct to String»).
# =============================================================================
# 1. Внешние IP на организации (modify: vIPConfigure, массив перезаписывается целиком)
resource "nubes_vc_org_ip_allocation" "org_ip" {
organization = var.organization
vip_configure = jsonencode([
{
name = var.ip_space_name
count = var.ip_count
}
])
# true = «заморозка»: destroy не трогает квоту внешних IP (кластер Штурвала держит
# адреса, опустить count ниже занятых платформа не даёт). Для полного удаления — false
# (и только после удаления кластера).
keep_on_destroy = true
depends_on = [nubes_vc_nsxt.edge]
}
# 2. SNAT на эдже (modify: ipSpaceName)
resource "nubes_vc_nsxt_snat" "snat" {
nsxt_uid = nubes_vc_nsxt.edge.id
ip_space_name = var.ip_space_name
# true = «заморозка»: destroy не выключает SNAT на эдже. Для полного удаления — false.
keep_on_destroy = true
# ipSpace должен быть уже выделен на организации
depends_on = [nubes_vc_org_ip_allocation.org_ip]
}
output "allocated_org_ip" {
description = "Выделено внешних IP на организации"
value = {
organization = var.organization
ip_space_name = var.ip_space_name
ip_count = var.ip_count
}
}
output "snat_ip_space" {
description = "ipSpace, включённый как SNAT на эдже"
value = nubes_vc_nsxt_snat.snat.ip_space_name
}
@@ -0,0 +1,29 @@
output "vdc_id" {
description = "UID созданного VDC"
value = nubes_vc_vdc.vdc.id
}
output "vdc_name" {
description = "Имя VDC"
value = nubes_vc_vdc.vdc.resource_name
}
output "vdc_state_params" {
description = "Параметры состояния VDC из API"
value = nubes_vc_vdc.vdc.state_params
}
output "nsxt_id" {
description = "UID созданного Edge (vc_nsxt)"
value = nubes_vc_nsxt.edge.id
}
output "nsxt_name" {
description = "Имя Edge (vc_nsxt)"
value = nubes_vc_nsxt.edge.resource_name
}
output "nsxt_state_params" {
description = "Параметры состояния Edge (vc_nsxt) из API"
value = nubes_vc_nsxt.edge.state_params
}
@@ -0,0 +1,4 @@
provider "nubes" {
api_token = var.api_token
api_endpoint = var.api_endpoint
}
@@ -0,0 +1,161 @@
# =============================================================================
# Kubernetes кластер Штурвал — сервис 150, ресурс nubes_k8s_sthutrval_cluster
# (НЕ 148 «Менеджмент Kubernetes кластер Штурвал» — это другой сервис)
#
# Всё, что относится к Штурвалу, лежит ТОЛЬКО в этом файле: переменные, их
# значения по умолчанию и сам ресурс. Чтобы выключить Штурвал — удалить файл
# или закомментировать ресурс.
#
# Порядок (чек-лист из инструкции на услугу в ЛК):
# 1) Организация в Cloud Director — создана вручную в ЛК
# 2) nubes_vc_vdc.vdc — есть
# 3) nubes_vc_nsxt.edge — есть, обязательно ALB + AVI VS >= 3
# 4) внешние адреса в организации — суммарно >= 3 (nubes_vc_org_ip_allocation)
# 5) SNAT на Edge — nubes_vc_nsxt_snat
# 6) Kubernetes кластер Штурвал — этот ресурс
#
# Минимальные требования к кластеру: мастер-нод >= 1, воркер-нод >= 1,
# 4 vCPU / 8 GB RAM / 50 GB диска на ноду.
# =============================================================================
# --- Переменные Штурвала ---
variable "shturval_resource_name" {
type = string
default = "shturval-dev1"
description = "Имя услуги «Kubernetes кластер Штурвал» в ЛК"
}
variable "shturval_cluster_name" {
type = string
default = "shturval-dev-01"
description = "Имя кластера внутри Штурвала"
}
variable "shturval_app_version" {
type = string
default = "2.14.0"
description = "Версия Штурвала (значение по умолчанию платформы — 2.14.0)"
}
variable "shturval_cp_sizing_policy" {
type = string
default = "TKG 4CPU 8RAM"
description = "Политика размера control plane: 4 vCPU / 8 GB (минимум по инструкции). Должна существовать в ресурсной платформе vDC — список политик берётся из услуги «Виртуальный датацентр»"
}
variable "shturval_cp_sizing_disk" {
type = number
default = 50
description = "Диск control plane, ГБ (минимум 50)"
}
variable "shturval_cp_count" {
type = number
default = 1
description = "Количество мастер-нод: 1, 3 или 5"
}
variable "shturval_worker_group_name" {
type = string
default = "workers-shturval-dev"
description = "Имя группы воркеров (уникальное в кластере; допустимы строчные латинские буквы, цифры и дефис)"
}
variable "shturval_worker_sizing_policy" {
type = string
default = "TKG 4CPU 8RAM"
description = "Политика размера воркеров: 4 vCPU / 8 GB (минимум по инструкции)"
}
variable "shturval_worker_sizing_disk" {
type = number
default = 50
description = "Диск воркеров, ГБ (минимум 50)"
}
variable "shturval_worker_count" {
type = number
default = 1
description = "Количество воркер-нод (минимум 1)"
}
# --- Значения, которые собираются из переменных ---
locals {
# Группы воркеров передаются JSON-строкой ВНУТРЬ услуги как есть, поэтому ключи
# должны быть ровно такими, как в манифесте услуги 150: groupName, sizingPolicy,
# sizingDisk, count, autoscale, labelDeck.
# ВНИМАНИЕ: в сгенерированном примере провайдера (docs → Example) ключи показаны
# в snake_case — это ошибка генератора, платформа на них падает с
# «Cannot invoke method split() on null object» (не находит groupName → null).
shturval_worker_config = jsonencode([
{
groupName = var.shturval_worker_group_name
sizingPolicy = var.shturval_worker_sizing_policy
sizingDisk = var.shturval_worker_sizing_disk
count = var.shturval_worker_count
autoscale = false # автоскейл выключен
labelDeck = true # разрешить разворачивать услуги из ЛК на этих нодах
}
])
}
# --- Ресурс Штурвала ---
resource "nubes_k8s_sthutrval_cluster" "shturval" {
resource_name = var.shturval_resource_name
# Кластер Штурвала уже существует (инстанс «shturval-dev») и в проде не
# удаляется неделями, поэтому ресурс должен УСЫНОВИТЬ существующий инстанс,
# а не падать с «РЕСУРС С ТАКИМ ИМЕНЕМ УЖЕ СУЩЕСТВУЕТ (SUSPEND)».
# Проверка/adopt выполняются в Create на apply (в plan будет «will be created»).
adopt_existing_on_create = true
# «Заморозка»: destroy приостанавливает кластер (suspend), а не удаляет.
# Следующий apply усыновит его и разморозит (resume).
suspend_on_destroy = true
# Штурвал создаётся долго (десятки минут) — поднимаем таймаут ожидания,
# иначе провайдер сдаётся на дефолтных 600 с.
operation_timeout = "60m"
startup_configuration = {
# vDC и Edge из этого же конфига (обязательные поля)
vdc_uid = nubes_vc_vdc.vdc.id
nsxt_uid = nubes_vc_nsxt.edge.id
cluster_name = var.shturval_cluster_name
# Дополнительные возможности кластера (в ЛК — галочки при создании)
ex_logging = true # логи в Loki (без него логи услуг не видны в ЛК)
ex_monitoring = true # метрики в VictoriaMetrics (без него метрик в ЛК нет)
ex_local_csi = true
ex_vip = true
ex_update = true
ex_ingress = true
ex_named_csi = true
}
cluster_configuration = {
app_version = var.shturval_app_version
}
control_plane_configuration = {
sizing_policy = var.shturval_cp_sizing_policy
sizing_disk = var.shturval_cp_sizing_disk
count = var.shturval_cp_count
}
worker_configuration = local.shturval_worker_config
access_configuration = {
need_external_address_api = true # внешний адрес для Kubernetes API (false недопустим)
access_ip_list_api = jsonencode([]) # пусто = доступ всем
need_external_address_ingress = true # внешний адрес для Ingress
access_ip_list_ingress = jsonencode([]) # пусто = доступ всем
}
# Кластер поднимается только после готовой сети: vDC -> Edge -> внешние IP -> SNAT
depends_on = [nubes_vc_nsxt_snat.snat]
}
@@ -0,0 +1,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
}
@@ -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"
}
}
}
+183
View File
@@ -0,0 +1,183 @@
# =============================================================================
# Виртуальная машина внутри 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
description = "Публичная часть SSH-ключа в формате OpenSSH. Не изменяется после создания. Значение — в terraform.tfvars"
}
variable "vm_access_port_list" {
type = list(object({
port = string
type = string
}))
default = [
{ port = "22", type = "tcp" }
]
description = "Белый список портов для доступа извне; type: tcp | udp | all"
}
variable "vm_access_ip_list" {
type = list(string)
default = ["0.0.0.0/0"]
description = "Белый список адресов, которым разрешён доступ к ВМ. Требует выделенного внешнего IP"
}
variable "vm_same_snat" {
type = bool
default = false
description = "false — публикация за общим SNAT эджа; true — за выделенным внешним IP услуги"
}
# --- vApp (услуга 26) ---
resource "nubes_vapp" "vapp" {
resource_name = var.vapp_resource_name
vapp_name = var.vapp_name
vdc_uid = nubes_vc_vdc.vdc.id # ref 21 — вычислительная инфраструктура
nsxt_uid = nubes_vc_nsxt.edge.id # ref 22 — сеть/маршрутизация
# «Заморозка»: destroy переводит vApp в suspend (delete требует suspend).
suspend_on_destroy = true
# Повторный apply усыновляет существующий vApp, а не падает с
# «РЕСУРС С ТАКИМ ИМЕНЕМ УЖЕ СУЩЕСТВУЕТ».
adopt_existing_on_create = true
# vApp требует готовую сеть (ipSpace на организации + SNAT на эдже).
depends_on = [nubes_vc_nsxt_snat.snat]
}
# --- ВМ (услуга 28) ---
resource "nubes_vc_vm_v3" "vm" {
resource_name = var.vm_resource_name
vm_name = var.vm_name
vapp_uid = nubes_vapp.vapp.id # ref 26 — ВМ размещается в vApp
image_vm = var.vm_image
vm_cpu = var.vm_cpu
vm_ram = var.vm_ram
vm_disk = var.vm_disk
user_login = var.vm_user_login
user_public_key = var.vm_user_public_key
# Внешний доступ
ip_space_name = var.ip_space_name # тот же ipSpace, что у SNAT эджа
same_snat = var.vm_same_snat
access_port_list = jsonencode(var.vm_access_port_list)
access_ip_list = jsonencode(var.vm_access_ip_list)
# «Заморозка»: destroy переводит ВМ в suspend.
suspend_on_destroy = true
adopt_existing_on_create = true
# ВМ создаётся платформой долго — поднимаем таймаут ожидания.
operation_timeout = "15m"
}
# --- Выводы ---
output "vapp_id" {
description = "UID созданного vApp (услуга 26)"
value = nubes_vapp.vapp.id
}
output "vapp_name" {
description = "Имя vApp"
value = nubes_vapp.vapp.vapp_name
}
output "vm_id" {
description = "UID созданной ВМ (услуга 28)"
value = nubes_vc_vm_v3.vm.id
}
output "vm_state_flat" {
description = "Плоский state ВМ — IP-адреса, статус и т.д."
value = nubes_vc_vm_v3.vm.state_out_flat
}
@@ -0,0 +1,18 @@
НИКАКОЙ САМОДЕЙТЕЛЬНОСТИ !!! делать ТОЛЬКО ТО НА ЧТО ПОЛУЧЕНО РАЗРЕШЕНИЕ !!!!
НИКАКИХ ДОГАДОК !!! ЕСТь сомнения - СПРОСИ !!!
НИКОГДА НЕ ДЕЛАЙ ПРЕДПОЛОЖЕНИЙ !!!
ВСЕГДА СПРАШИВАЙ, ЕСЛИ НЕ УВЕРЕН !!!
НИКОГДА НЕ ИГНОРИРУЙ ПОЛУЧЕННЫЕ ИНСТРУКЦИИ !!!
ВСЕГДА ПОДТВЕРЖДАЙ ПОЛУЧЕННЫЕ ИНСТРУКЦИИ !!!
НИКОГДА НЕ ИЗМЕНЯЙ ИНСТРУКЦИИ БЕЗ РАЗРЕШЕНИЯ !!!
ВСЕГДА СОБЛЮДАЙ ПОРЯДОК И ПОСЛЕДОВАТЕЛЬНОСТЬ В ИНСТРУКЦИЯХ !!!
НИКОГДА НЕ ПРЕВЫШАЙ СВОИ ПОЛНОМОЧИЯ !!!
ВСЕГДА СОБЛЮДАЙ БЕЗОПАСНОСТЬ И КОНФИДЕНЦИАЛЬНОСТЬ !!!
НИКОГДА НЕ ПЕРЕДАВАЙ СЕКРЕТЫ ИНТЕРНЕТУ БЕЗ РАЗРЕШЕНИЯ !!!
НЕ ВЫЗЫВАТЬ ДРУГИЕ АГЕНТЫ БЕЗ РАЗРЕШЕНИЯ !!!
коммитить после каждой правки, чтобы зафиксировать текущее состояние и избежать потери изменений. Использовать осмысленные сообщения коммитов, отражающие суть изменений.
ВСЕГДА СОХРАНЯТЬ РЕЗЕРВНЫЕ КОПИИ ВАЖНЫХ ФАЙЛОВ ПЕРЕД ВНЕСЕНИЕМ ИЗМЕНЕНИЙ.
НИКОГДА НЕ ПОЛАГАЙСЯ НА ПАМЯТЬ — ВСЕГДА ПРОВЕРЯЙ АКТУАЛЬНОСТЬ ИНСТРУКЦИЙ.
ВСЕГДА СОБЛЮДАЙ ИНСТРУКЦИИ, ДАВАЙТЕ ПОДТВЕРЖДЕНИЯ И НЕ ДЕЛАЙТЕ САМОСТОЯТЕЛЬНЫХ ИЗМЕНЕНИЙ.
Если не на 100% уверен в распоряжениях - СПРОСИ СНОВА И ПОДТВЕРДИ. НЕ ГАДАЙ ЧТО Я ИМЛ ВВИДУ !!!!
ЕСЛИ В ЧЁМ совменваешься - ОСТАНОВИСЬ И СПРОСИ !!!! НИКОГДА не действуй по догадкам и преположенгиям !!! ПЕРЕПРОВЕРЯЙ ВСЁ НЕСКОЛЬКО РАЗ !!!
+277
View File
@@ -0,0 +1,277 @@
# Provider Architecture (PRIMARY SOURCE OF TRUTH)
**⛔ THIS FILE IS THE FOUNDATION. ALL CODE AND SCRIPTS ARE DERIVED FROM IT.**
This document defines the project-wide architecture and rules for generation,
provider behavior, and documentation. Any change to provider logic MUST be
reflected here FIRST, then implemented in `gen_v2` and other tools.
## Core Principles
1) YAML per service is generated ONLY from API data.
2) The provider core (`provider/internal/core`) is universal and must not contain
service-specific logic.
3) Service-specific Go code (`provider/internal/resources_gen`) is fully generated
from YAML. No manual edits to generated code.
4) Documentation is generated from the same YAML.
5) Build artifacts for 3 OS targets are published to the registry, and docs are
published to the website.
6) `TOOLS/ARCHITECTURE.md` (this file) is the primary spec. Code follows.
## Service Selection
- The inclusion list is defined by: `TOOLS/config/<dev|test|prod>/services_list.txt`
(по одному списку на стенд; repo-relative path)
- Each line starts with service_id, followed by service name/alias.
- Operation timeouts source is defined by: `TOOLS/config/<dev|test|prod>/operation_timeouts.json`.
## API Endpoint
The provider supports two API styles, auto-detected by `NUBES_API_ENDPOINT`:
- **Legacy proxy**: contains `index.cfm` → `?endpoint=/path`
- **REST Gateway**: no `index.cfm` → direct path concatenation
## Unified YAML (Per Service)
One YAML file per service. This is the only input for:
- Provider code generation
- Documentation generation
- Validation rules
Required top-level fields:
- name
- service_id
- service_display_name
- service_short_name
- lifecycle
- outputs
- operations
- service_man
### Operations: Kinds and Rules
Each operation has a kind:
**instance** — CRUD for the service instance, plus suspend/resume:
| API action | Terraform behavior |
|---|---|
| create | `terraform apply` (new resource) |
| delete | `terraform destroy` |
| modify | `terraform apply` (params changed) |
| suspend | `terraform destroy` when `suspend_on_destroy=true` |
| resume | `terraform apply` when `adopt_existing_on_create=true` |
**subresource** — CRUD for objects inside the service (users, databases, topics):
- Exposed as separate resources: `nubes_{service}_{subresource}`
- Identity = `{parent_instance_uid, subresource_key}` (e.g. `{postgres_id, username}`)
- Supports `adopt_existing_on_create` — if subresource already exists, adopt it instead of failing
**action** — one-shot operations. **Only `redeploy` is included**:
- `redeploy` → **inline**: field `git_revision` in the main resource. When it changes, call redeploy instead of (or after) modify
- `restart`, `recovery`, `reconcile` → **excluded**. These are manual operational tasks, performed via UI only. Reason:
- `restart` — modify handles pod restart when needed
- `recovery` — creates a new instance from backup, not a modification of existing
- `reconcile` — sync after manual changes; Terraform owns its own state
### Idempotency Rules
- instance CRUD is idempotent via standard Terraform behavior.
- subresource CRUD is idempotent by resource identity.
- `redeploy`: idempotent via `git_revision` field — if unchanged, no redeploy
### Lifecycle Behavior
- For `instance` resources with `suspend`/`resume`, use explicit flags:
`adopt_existing_on_create` (default `false`) and `suspend_on_destroy` (default `true`).
- Apply decision matrix for suspend-capable services:
- cloud status `missing` or `deleted` -> `create`
- `suspend` + `adopt_existing_on_create=true` + key params match -> `resume` + `adopt`
- `suspend` + `adopt_existing_on_create=false` -> error (explicitly require flag for resume/adopt)
- `suspend` + key params mismatch -> error
- `running` + `adopt_existing_on_create=true` -> adopt/import behavior
- `running` + `adopt_existing_on_create=false` -> error
- `not created` -> error, no auto-adopt/create
- `creating`/`pending`/`failed` -> error
- Conflict diagnostics requirement:
- When `resource_name` already exists and `adopt_existing_on_create=false`, diagnostics must explicitly offer two choices:
1) change `resource_name` to create a new resource;
2) import/adopt existing one by setting `adopt_existing_on_create=true` and re-running `apply`.
- Destroy behavior for suspend-capable services:
- `suspend_on_destroy=true` -> call `suspend`
- `suspend_on_destroy=false` -> remove from Terraform state only (no API call)
### Diagnostics Format
- Lifecycle diagnostics for plan/apply must be multiline and human-readable.
- Include decision reason and controlling flag in message body.
- Print details as separate lines: `resource_name`, `service_id`, `instance_uid`,
`status`, `status_raw`, `operation_pending`, `operation_in_progress`.
## Provider Model
- Core (`provider/internal/core`) is universal: no service-specific logic inside it.
- Generated service resources (`provider/internal/resources_gen`) contain only
schema/params and references.
- Hand-written service resources live in `provider/internal/resources_core` and are
registered in `provider/internal/provider/provider.go` `Resources()`. They are
NOT generated; they must not contain arbitrary service logic, only:
- a schema, and
- wiring between schema fields and the universal core API
(`RunInstanceOperationUniversalByCode`, `ResolveRefSvcParamValue`, etc.).
### API Resilience
- Core MUST retry transient 401 errors from Gateway (3 attempts, exponential backoff).
Gateway may temporarily reject valid JWT tokens.
- **CURRENTLY NOT IMPLEMENTED** — `isRetryable` (`core/http.go`) retries only
{429, 502, 503, 504}, NOT 401, and only for GET. This is a known gap versus the
intent above; fix in `core/http.go` `isRetryable`.
### Generated Code Resilience
- **Zero-value fallback** (`normalizeUniversalValueV6`): если параметр отсутствует
в пользовательском `.tf`, подставлять zero-value по `dataType`:
- `integer` → `"0"`, `boolean` → `"false"`, `map-fixed` → `"{}"`, `array` → `"[]"`, `string` → `""`
- Это предотвращает NullPointerException на стороне API при добавлении новых полей.
- **map-fixed default из DataDescriptor** (`buildMapFixedDefault`): если API возвращает
`dataDescriptor` для `map-fixed`-параметра, а значение отсутствует или равно `"{}"`,
строится JSON из дефолтов sub-параметров (`{"type":"off","durationCA":"175200",...}`).
Это гарантирует что API получит все обязательные sub-поля с их значениями по умолчанию.
- **s3Uid-резолв в map-fixed** (`resolveS3UidInMapFixed`): для map-fixed-параметров
парсится JSON, ищутся ключи по паттерну `s3.*uid` (case-insensitive), значения-не-UUID
резолвятся в UUID через S3 (сервис 12). Не зависит от DataDescriptor API.
Соглашение об именах: любой sub-param с `s3`+`uid` в имени → S3 (12).
- **modify всегда через WithDefaults** (`RunInstanceOperationUniversalWithDefaults`):
modify-операции запрашивают `cfsParams` у API и отправляют все параметры,
включая новые, с дефолтами из API.
- **Nil-guard для nested-параметров** (шаблон `instance.go`): `map-fixed`-параметры
(указатели на вложенные структуры) проверяются на nil перед доступом к sub-полям.
Если состояние создано до добавления нового `map-fixed`-параметра — он будет nil,
и код не должен падать с nil pointer dereference. Вместо этого параметр пропускается,
и zero-value fallback подставит `{}`.
- **Nested-атрибуты с default — Optional без Computed** (`NestedSchemaBlock` в `helpers.go`):
`map-fixed`-параметры с default генерируются как `Optional: true` (без `Computed`),
потому что провайдер не вычисляет nested-значения из API. `Computed` приводил бы к
`unknown`-значению, которое нельзя декодировать в конкретный тип `*Struct`.
- **Merge SubParams union** (`params.go`): при совпадении `Code` параметра в разных
операциях (create/modify) с разными наборами sub-params, `Merge()` объединяет их
union'ом, а не отбрасывает второй. Это гарантирует что структура содержит все поля.
### Subresource Resources
Subresource operations are exposed as standard resources.
Example mapping:
- create_user/delete_user/modify_user => nubes_<service>_user
- create_database/delete_database => nubes_<service>_database
Example HCL:
resource "nubes_postgres_user" "user1" {
postgres_id = nubes_postgres.db.id
username = "app_user"
role = "app_user"
}
### Redeploy (inline action)
Services with `redeploy` operation get a `git_revision` field in the main resource.
Changing `git_revision` triggers `redeploy` instead of `modify`.
Example HCL:
resource "nubes_flask" "app" {
resource_name = "my-flask"
git_revision = "abc123" # ← change this to trigger redeploy
app_configuration = jsonencode({...})
}
## Concurrency and State Locking
**Provider-level guarantees:**
- The provider does NOT implement distributed locking for instance operations.
- Two concurrent `terraform apply` with the same `resource_name` may create duplicate
instances, leading to a «multiple instances found» error on subsequent applies.
**User responsibility:**
- Use Terraform backend with state locking (S3+DynamoDB, etc.).
- Do NOT run `terraform apply` from two workspaces against the same state simultaneously.
- If duplicates occur: delete extras via Cloud Console and re-apply.
**API-side limitations:**
- Nubes API does not enforce unique `displayName` per serviceId.
- The provider cannot atomically guarantee «create-or-adopt» without API support
for conditional creation or name uniqueness constraints.
## Documentation Model
From the unified YAML, generate:
- Resource page: CRUD params, outputs, lifecycle defaults, operations summary
- MAN page: service_man + parameter man blocks
- Resources index: each resource links to its page and its MAN page
## Pipeline Overview (DevOps)
1) Generate unified YAML from API for services_list.txt.
2) Generate provider code from YAML.
3) Generate documentation from YAML.
4) Build provider for linux/windows/darwin.
5) Upload provider artifacts to registry.
6) Build and publish docs to site.
## Lifecycle Vocabulary (single contract)
⚠️ The destroy-behaviour vocabulary is currently INCONSISTENT across resource kinds:
1. generated instance resources: runtime flags `suspend_on_destroy` / `keep_on_destroy`;
2. generated modifiers: compile-time `delete_strategy` (no runtime flag);
3. hand-written modifiers: runtime `keep_on_destroy` only.
All three express the same intent ("what happens to the platform effect on destroy").
Canonical direction: one unified vocabulary/contract for all resource kinds.
## Non-Negotiable Rules
- No manual edits to generated YAML or generated Go code.
- Any change to generated code must come from API or generator logic updates.
- The generator must enforce these rules and fail fast on drift.
## Exception Registry (service-specific DATA, never logic)
Principle: provider core and generator logic are universal for all stands and
services. Service-specific deviations are of two kinds:
1. **Generated modifiers** — a `modify` op that should become a dedicated modifier
resource. The generator (`TOOLS/resource-generator/internal/loader/loader.go`)
supports `kind: modifier` with `delete_strategy` (`noop_warn`/`inverse`/`error`)
and `idempotency` (`none`/`check_before_run`). The overlay data file
`modifiers.yaml` that would drive this is **documented but NOT yet created**;
until then, modifiers are hand-written in `provider/internal/resources_core/`.
(The legacy registry `serviceSpecificModifiers` in `TOOLS/yaml-generator/main.go`
was REMOVED during the 2026-09-23 refactoring.)
2. **Doc examples** — named registry:
| Registry | File | Declares |
|---|---|---|
| `serviceSpecificDocExamples` | `TOOLS/docs-generator/internal/writers/writers.go` | per-service doc examples, gated on service name + required state/vault keys |
Rules:
- Key by stable service NAME (slug), never by raw numeric ID.
- Each entry answers WHAT / WHAT IT DOES / WHY / WHERE (see code comments).
- Doc-example exception = editing the named registry above → visible in diff.
- Modifier exception (until `modifiers.yaml` exists) = hand-written resource in
`provider/internal/resources_core/` + registration in `provider.go`.
- Never annotate API-YAML: it is machine-regenerated and edits would be lost.
Enforced by scripts (run before build/commit):
- `TOOLS/scripts/check_generated_drift.sh <stand>` — generated Go vs provider copy.
- `TOOLS/scripts/check_hardcoded_service_ids.sh` — forbids `svc.ID == N` /
`ServiceID == N` outside the registries.
Build rule: `provider/internal/resources_gen` and `provider/resources_yaml` are
ephemeral by design and never a build source. Canonical build is
`03_build_and_upload_provider.sh` (temp copy from `generated/<stand>/go`);
`build-provider.sh` refuses direct build from `provider/`. For local IDE,
`go build` and `go test`, materialize one stand first:
`TOOLS/scripts/dev-materialize.sh <stand>` (output is git-ignored).
@@ -0,0 +1,29 @@
#!/usr/bin/env bash
set -euo pipefail
# check_hardcoded_service_ids.sh — запрет сервис-специфичных хардкодов по числовому ID.
#
# Ищет сравнения вида svc.ID == N / ServiceID == N / spec.ServiceID == N (N > 0)
# в Go-коде TOOLS/. Исключения должны жить ТОЛЬКО в именованных реестрах (данные):
# - TOOLS/yaml-generator/main.go (serviceSpecificModifiers)
# - TOOLS/docs-generator/internal/writers/writers.go (serviceSpecificDocExamples)
#
# Выход: 0 — хардкодов нет; 1 — найдены.
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="${ROOT_DIR:-$(cd "${SCRIPT_DIR}/../.." && pwd)}"
matches="$(grep -rnE '\.(ServiceID|ID)[[:space:]]*==[[:space:]]*[1-9][0-9]*' "$ROOT_DIR/TOOLS" --include='*.go' || true)"
if [[ -n "$matches" ]]; then
echo "HARDCODED SERVICE ID FOUND (service-specific logic must live in a registry):" >&2
echo "$matches" >&2
echo "" >&2
echo "Вынеси исключение в один из реестров:" >&2
echo " - TOOLS/yaml-generator/main.go (serviceSpecificModifiers)" >&2
echo " - TOOLS/docs-generator/internal/writers/writers.go (serviceSpecificDocExamples)" >&2
echo "См. TOOLS/ARCHITECTURE.md, раздел «Реестр исключений»." >&2
exit 1
fi
echo "OK: no hardcoded service IDs in TOOLS/."
@@ -0,0 +1,169 @@
package core
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httputil"
"os"
"strings"
"time"
)
// ===== Internal HTTP helpers =====
func (c *UniversalClient) doRequest(ctx context.Context, method, path string, payload interface{}) ([]byte, http.Header, error) {
// User-Agent: браузерный, чтобы пройти DDoS-Guard (см. docs/ops/API_TOKENS.md).
// Go-http-client по умолчанию блокируется фильтром ddos-guard.
const maxRetries = 3
baseDelay := c.RetryBaseDelay
if baseDelay <= 0 {
baseDelay = 2 * time.Second
}
var lastErr error
for attempt := 0; attempt <= maxRetries; attempt++ {
if attempt > 0 {
delay := baseDelay * time.Duration(1<<(attempt-1)) // 2s, 4s, 8s
select {
case <-time.After(delay):
case <-ctx.Done():
return nil, nil, ctx.Err()
}
}
var body io.Reader
if payload != nil {
b, err := json.Marshal(payload)
if err != nil {
return nil, nil, err
}
body = bytes.NewBuffer(b)
}
reqURL := c.buildURL(path)
req, err := http.NewRequestWithContext(ctx, method, reqURL, body)
if err != nil {
return nil, nil, err
}
// Форсируем новое TCP-соединение для POST/PUT/PATCH: DDoS-Guard может блочить их на keep-alive.
// GET-запросы (поиск, чтение) оставляем на keep-alive — req.Close на них ломает DDoS-Guard.
if method != "GET" {
req.Close = true
}
req.Header.Set("User-Agent", userAgent)
req.Header.Set("Accept", "*/*")
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
if c.ApiToken != "" {
req.Header.Set("Authorization", "Bearer "+c.ApiToken)
}
// DEBUG
if isHTTPDebugEnabled() {
reqForDump := req.Clone(req.Context())
reqForDump.Header = sanitizeAuthHeader(req.Header)
dump, _ := httputil.DumpRequestOut(reqForDump, body != nil)
fmt.Fprintf(os.Stderr, "\n>>> REQ %s %s\n%s\n", method, path, dump)
}
// DEBUG в файл
if isHTTPDebugEnabled() {
f, _ := os.OpenFile(debugLogPath("nubes_debug.log"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
if f != nil {
fmt.Fprintf(f, ">>> %s %s\n", method, req.URL.String())
f.Close()
}
}
resp, err := c.HttpClient.Do(req)
if err != nil {
lastErr = err
// Сетевые ошибки: retry только для идемпотентного GET.
if attempt < maxRetries && method == "GET" {
continue
}
return nil, nil, err
}
respBody, err := io.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
lastErr = err
if attempt < maxRetries {
continue
}
return nil, nil, err
}
// Retry только для transient-ошибок и только для GET
if resp.StatusCode >= 400 {
if attempt < maxRetries && method == "GET" && isRetryable(resp.StatusCode) {
lastErr = formatAPIError(resp.StatusCode, respBody)
continue
}
return nil, nil, formatAPIError(resp.StatusCode, respBody)
}
return respBody, resp.Header, nil
}
return nil, nil, fmt.Errorf("doRequest failed after %d retries: %w", maxRetries, lastErr)
}
// isRetryable returns true for transient HTTP errors that can be retried.
func isRetryable(statusCode int) bool {
return statusCode == http.StatusTooManyRequests || // 429
statusCode == http.StatusServiceUnavailable || // 503
statusCode == http.StatusBadGateway || // 502
statusCode == http.StatusGatewayTimeout // 504
}
func (c *UniversalClient) postIgnoreResponse(ctx context.Context, path string, payload interface{}, returnLocation bool) (string, error) {
respBody, headers, err := c.doRequest(ctx, "POST", path, payload)
if err != nil {
return "", err
}
if returnLocation {
if loc := headers.Get("Location"); loc != "" {
return extractUIDFromLocation(loc), nil
}
}
var justId string
if err := json.Unmarshal(respBody, &justId); err == nil && justId != "" {
return justId, nil
}
return "", nil
}
func extractUIDFromLocation(loc string) string {
if loc == "" {
return ""
}
return strings.TrimPrefix(loc, "./")
}
// formatAPIError парсит JSON-ответ API и возвращает читаемое сообщение.
// Если тело не является JSON с полем ERROR — возвращает сырой текст.
func formatAPIError(statusCode int, body []byte) error {
var parsed struct {
Error string `json:"ERROR"`
Detail string `json:"DETAIL"`
}
if json.Unmarshal(body, &parsed) == nil && parsed.Error != "" {
msg := parsed.Error
if d := strings.TrimSpace(parsed.Detail); d != "" {
msg += ": " + d
}
return fmt.Errorf("ошибка API %d: %s", statusCode, msg)
}
return fmt.Errorf("ошибка API %d: %s", statusCode, strings.TrimSpace(string(body)))
}
@@ -0,0 +1,249 @@
package core
import (
"context"
"fmt"
"strings"
"time"
)
// ===== Запуск операций инстанса (params по ID) =====
//
// - RunInstanceOperationUniversal — операция с params по ID (без досылки дефолтов)
// - RunInstanceOperationUniversalWithDefaults — то же + досылка required-дефолтов
// - RunRedeployOperation — redeploy
//
// Операции с params по code — см. operation_run_bycode.go.
// Схема cfsParams (с fallback) — см. operation_cfs.go.
// RunInstanceOperationUniversal runs an available operation (modify/suspend/delete/resume) if possible.
func (c *UniversalClient) RunInstanceOperationUniversal(ctx context.Context, instanceUid string, action string, params map[int]string) error {
state, err := c.GetInstanceState(ctx, instanceUid)
if err != nil {
return err
}
if state.OperationIsPending || state.OperationIsInProgress {
if err := c.waitForInstanceIdle(ctx, instanceUid, c.idleTimeoutFor(state.ServiceId)); err != nil {
return err
}
}
var opId int
for _, op := range state.AvailableOperations {
if strings.EqualFold(op.Operation, action) {
opId = op.SvcOperationId
break
}
}
if opId == 0 {
return fmt.Errorf("операция %s недоступна для экземпляра %s", action, instanceUid)
}
payload := map[string]interface{}{
"instanceUid": instanceUid,
"svcOperationId": opId,
"operation": action,
}
opUid, err := c.postIgnoreResponse(ctx, "/instanceOperations", payload, true)
if err != nil {
return fmt.Errorf("не удалось создать операцию %s: %w", action, err)
}
if opUid == "" {
return fmt.Errorf("не удалось получить UID операции для %s", action)
}
for paramId, value := range params {
pPayload := genericParamReq{
InstanceOperationUid: opUid,
SvcOperationCfsParamId: paramId,
ParamValue: value,
}
_, _, err := c.doRequest(ctx, "POST", "/instanceOperationCfsParams", pPayload)
if err != nil {
return fmt.Errorf("не удалось установить параметр %d: %w", paramId, err)
}
}
_, _, err = c.doRequest(ctx, "POST", fmt.Sprintf("/instanceOperations/%s/run", opUid), map[string]interface{}{})
if err != nil {
return err
}
// НЕ МЕНЯТЬ: завершение операции определяется по dtFinish
return c.waitForOperationFinish(ctx, opUid, c.operationTimeoutForContext(ctx, state.ServiceId, action))
}
// RunInstanceOperationUniversalWithDefaults runs an operation and submits required params (including defaults).
func (c *UniversalClient) RunInstanceOperationUniversalWithDefaults(ctx context.Context, instanceUid string, action string, params map[int]string) error {
state, err := c.GetInstanceState(ctx, instanceUid)
if err != nil {
return err
}
if state.OperationIsPending || state.OperationIsInProgress {
if err := c.waitForInstanceIdle(ctx, instanceUid, c.idleTimeoutFor(state.ServiceId)); err != nil {
return err
}
}
var opId int
for _, op := range state.AvailableOperations {
if strings.EqualFold(op.Operation, action) {
opId = op.SvcOperationId
break
}
}
if opId == 0 {
return fmt.Errorf("операция %s недоступна для экземпляра %s", action, instanceUid)
}
payload := map[string]interface{}{
"instanceUid": instanceUid,
"svcOperationId": opId,
"operation": action,
}
opUid, err := c.postIgnoreResponse(ctx, "/instanceOperations", payload, true)
if err != nil {
return fmt.Errorf("не удалось создать операцию %s: %w", action, err)
}
if opUid == "" {
return fmt.Errorf("не удалось получить UID операции для %s", action)
}
cfsParams, err := c.fetchOperationCfsParams(ctx, opUid, opId)
if err != nil {
return err
}
params, err = c.resolveRefSvcParamValues(ctx, cfsParams, params)
if err != nil {
return err
}
sent := make(map[int]bool)
for paramId, value := range params {
pPayload := genericParamReq{
InstanceOperationUid: opUid,
SvcOperationCfsParamId: paramId,
ParamValue: value,
}
_, _, err := c.doRequest(ctx, "POST", "/instanceOperationCfsParams", pPayload)
if err != nil {
return fmt.Errorf("не удалось установить параметр %d: %w", paramId, err)
}
sent[paramId] = true
}
live, liveErr := c.instanceLiveParams(ctx, instanceUid)
if liveErr != nil {
return fmt.Errorf("не удалось прочитать live-значения инстанса для досылки modify: %w", liveErr)
}
for _, param := range cfsParams {
if sent[param.SvcOperationCfsParamId] {
continue
}
// Приоритет: live state.params инстанса → paramValue операции → defaultValue.
// paramValue из cfsParams — дефолт формы, не состояние инстанса (см. operation_cfs.go).
// Симметрично runInstanceOperationByCode: если ни одного источника нет — пропускаем
// (иначе уйдёт синтетический "0"/"false"/"[]" и нарушит constraint).
val, hasLive := lookupLiveParam(live, param)
if !hasLive {
if (param.ParamValue == nil || strings.TrimSpace(*param.ParamValue) == "") &&
(param.DefaultValue == nil || strings.TrimSpace(*param.DefaultValue) == "") {
continue
}
if param.ParamValue != nil && strings.TrimSpace(*param.ParamValue) != "" {
val = *param.ParamValue
} else if param.DefaultValue != nil {
val = *param.DefaultValue
}
}
val = normalizeUniversalValueV6(val, param)
pPayload := genericParamReq{
InstanceOperationUid: opUid,
SvcOperationCfsParamId: param.SvcOperationCfsParamId,
ParamValue: val,
}
_, _, err := c.doRequest(ctx, "POST", "/instanceOperationCfsParams", pPayload)
if err != nil {
return fmt.Errorf("не удалось отправить параметр по умолчанию %d: %w", param.SvcOperationCfsParamId, err)
}
}
_, _, err = c.doRequest(ctx, "GET", fmt.Sprintf("/instanceOperations/%s/validate-cfs", opUid), nil)
if err != nil {
return fmt.Errorf("валидация не пройдена: %w", err)
}
_, _, err = c.doRequest(ctx, "POST", fmt.Sprintf("/instanceOperations/%s/run", opUid), map[string]interface{}{})
if err != nil {
return err
}
// НЕ МЕНЯТЬ: завершение операции определяется по dtFinish
return c.waitForOperationFinish(ctx, opUid, c.operationTimeoutForContext(ctx, state.ServiceId, action))
}
// RunRedeployOperation запускает redeploy для сервисов, поддерживающих пересборку из git.
// Если params не nil — отправляет CFS-параметры перед запуском.
func (c *UniversalClient) RunRedeployOperation(ctx context.Context, instanceUid string, timeoutOverride string, params map[int]string) error {
state, err := c.GetInstanceState(ctx, instanceUid)
if err != nil {
return err
}
if state.OperationIsPending || state.OperationIsInProgress {
if err := c.waitForInstanceIdle(ctx, instanceUid, c.idleTimeoutFor(state.ServiceId)); err != nil {
return err
}
}
opId := 0
for _, op := range state.AvailableOperations {
if strings.EqualFold(op.Operation, "redeploy") {
opId = op.SvcOperationId
break
}
}
if opId == 0 {
return fmt.Errorf("операция redeploy недоступна для экземпляра %s", instanceUid)
}
payload := map[string]interface{}{
"instanceUid": instanceUid,
"svcOperationId": opId,
"operation": "redeploy",
}
opUid, err := c.postIgnoreResponse(ctx, "/instanceOperations", payload, true)
if err != nil {
return fmt.Errorf("не удалось создать операцию redeploy: %w", err)
}
if opUid == "" {
return fmt.Errorf("не удалось получить UID операции redeploy")
}
for paramId, value := range params {
pPayload := genericParamReq{
InstanceOperationUid: opUid,
SvcOperationCfsParamId: paramId,
ParamValue: value,
}
if _, _, err := c.doRequest(ctx, "POST", "/instanceOperationCfsParams", pPayload); err != nil {
return fmt.Errorf("не удалось установить параметр %d для redeploy: %w", paramId, err)
}
}
if _, _, err := c.doRequest(ctx, "POST", fmt.Sprintf("/instanceOperations/%s/run", opUid), map[string]interface{}{}); err != nil {
return err
}
timeout := c.operationTimeoutForContext(ctx, state.ServiceId, "redeploy")
if timeoutOverride != "" {
if d, parseErr := time.ParseDuration(timeoutOverride); parseErr == nil {
timeout = d
}
}
return c.waitForOperationFinish(ctx, opUid, timeout)
}
@@ -0,0 +1,162 @@
package core
import (
"context"
"fmt"
"strings"
)
// RunInstanceOperationUniversalByCode runs an operation using params keyed by code.
// It resolves param codes to IDs via operation manifest, applies defaults, validates, and runs.
func (c *UniversalClient) RunInstanceOperationUniversalByCode(ctx context.Context, instanceUid string, action string, params map[string]string) error {
return c.runInstanceOperationByCode(ctx, instanceUid, action, params, false)
}
// RunInstanceOperationUniversalByIdempotent — то же, но с idempotency pre-check:
// перед run сверяет desired==current и, если ВСЕ поля совпали, пропускает run.
// Применяется для модификаторов с idempotency: check_before_run.
func (c *UniversalClient) RunInstanceOperationUniversalByIdempotent(ctx context.Context, instanceUid string, action string, params map[string]string) error {
return c.runInstanceOperationByCode(ctx, instanceUid, action, params, true)
}
func (c *UniversalClient) runInstanceOperationByCode(ctx context.Context, instanceUid string, action string, params map[string]string, idempotent bool) error {
state, err := c.GetInstanceState(ctx, instanceUid)
if err != nil {
return err
}
if state.OperationIsPending || state.OperationIsInProgress {
if err := c.waitForInstanceIdle(ctx, instanceUid, c.idleTimeoutFor(state.ServiceId)); err != nil {
return err
}
}
var opId int
for _, op := range state.AvailableOperations {
if strings.EqualFold(op.Operation, action) {
opId = op.SvcOperationId
break
}
}
if opId == 0 {
return fmt.Errorf("операция %s недоступна для экземпляра %s", action, instanceUid)
}
payload := map[string]interface{}{
"instanceUid": instanceUid,
"svcOperationId": opId,
"operation": action,
}
opUid, err := c.postIgnoreResponse(ctx, "/instanceOperations", payload, true)
if err != nil {
return fmt.Errorf("не удалось создать операцию %s: %w", action, err)
}
if opUid == "" {
return fmt.Errorf("не удалось получить UID операции для %s", action)
}
cfsParams, err := c.fetchOperationCfsParams(ctx, opUid, opId)
if err != nil {
return err
}
// Idempotency pre-check: если все desired уже равны live-значениям — пропускаем run.
// desired = явно заданные пользователем коды (params, keyed by code), БЕЗ досылки.
if idempotent && c.modifierDesiredEqualsCurrent(params, cfsParams) {
return nil
}
codeToParam := make(map[string]universalCfsParam)
for _, p := range cfsParams {
if key := strings.ToLower(strings.TrimSpace(p.Code)); key != "" {
codeToParam[key] = p
}
if key := strings.ToLower(strings.TrimSpace(p.SvcOperationCfsParam)); key != "" {
codeToParam[key] = p
}
}
paramsByID := map[int]string{}
for code, value := range params {
key := strings.ToLower(strings.TrimSpace(code))
p, ok := codeToParam[key]
if !ok {
return fmt.Errorf("код параметра %s не найден для операции %s", code, action)
}
paramsByID[p.SvcOperationCfsParamId] = value
}
paramsByID, err = c.resolveRefSvcParamValues(ctx, cfsParams, paramsByID)
if err != nil {
return err
}
sent := make(map[int]bool)
for paramId, value := range paramsByID {
pPayload := genericParamReq{
InstanceOperationUid: opUid,
SvcOperationCfsParamId: paramId,
ParamValue: value,
}
_, _, err := c.doRequest(ctx, "POST", "/instanceOperationCfsParams", pPayload)
if err != nil {
return fmt.Errorf("не удалось установить параметр %d: %w", paramId, err)
}
sent[paramId] = true
}
live, liveErr := c.instanceLiveParams(ctx, instanceUid)
if liveErr != nil {
return fmt.Errorf("не удалось прочитать live-значения инстанса для досылки modify: %w", liveErr)
}
for _, param := range cfsParams {
if sent[param.SvcOperationCfsParamId] {
continue
}
// Дозаполняем ВСЕ незаданные параметры, чтобы бэкенд modify не трактовал
// пропущенный/null как reset-to-default (иначе частичный payload затирает
// create-поля, см. prompt_for_opus_modifier_null_bug.md).
//
// ПРИОРИТЕТ ИСТОЧНИКА: live state.params инстанса → paramValue операции → defaultValue.
// КРИТИЧНО: paramValue из ?fields=cfsParams — дефолт ФОРМЫ операции, не состояние
// инстанса (HAR/edge_.har: needEnableAVI paramValue="false" при live=true → стирало ALB).
// Если ни live, ни paramValue, ни defaultValue НЕТ — пропускаем (не шлём синтетический
// "0"/"false"/"[]", который может нарушить constraint "integer > 0").
val, hasLive := lookupLiveParam(live, param)
if !hasLive {
if (param.ParamValue == nil || strings.TrimSpace(*param.ParamValue) == "") &&
(param.DefaultValue == nil || strings.TrimSpace(*param.DefaultValue) == "") {
continue
}
if param.ParamValue != nil {
val = *param.ParamValue
} else if param.DefaultValue != nil {
val = *param.DefaultValue
}
}
val = normalizeUniversalValueV6(val, param)
pPayload := genericParamReq{
InstanceOperationUid: opUid,
SvcOperationCfsParamId: param.SvcOperationCfsParamId,
ParamValue: val,
}
_, _, err := c.doRequest(ctx, "POST", "/instanceOperationCfsParams", pPayload)
if err != nil {
return fmt.Errorf("не удалось отправить параметр по умолчанию %d: %w", param.SvcOperationCfsParamId, err)
}
}
_, _, err = c.doRequest(ctx, "GET", fmt.Sprintf("/instanceOperations/%s/validate-cfs", opUid), nil)
if err != nil {
return fmt.Errorf("валидация не пройдена: %w", err)
}
_, _, err = c.doRequest(ctx, "POST", fmt.Sprintf("/instanceOperations/%s/run", opUid), map[string]interface{}{})
if err != nil {
return err
}
// НЕ МЕНЯТЬ: завершение операции определяется по dtFinish
return c.waitForOperationFinish(ctx, opUid, c.operationTimeoutForContext(ctx, state.ServiceId, action))
}
@@ -0,0 +1,61 @@
package core
import (
"encoding/json"
"strings"
)
// ===== Нормализация значений CFS-параметров =====
// buildMapFixedDefault строит JSON-объект из дефолтов sub-параметров map-fixed.
func buildMapFixedDefault(param universalCfsParam) string {
result := make(map[string]string, len(param.DataDescriptor))
for key, sub := range param.DataDescriptor {
result[key] = sub.DefaultValue
}
b, err := json.Marshal(result)
if err != nil {
return "{}"
}
return string(b)
}
func normalizeUniversalValueV6(val string, param universalCfsParam) string {
trimmed := strings.TrimSpace(val)
if strings.EqualFold(trimmed, "null") {
trimmed = ""
}
if trimmed == "\"\"" {
trimmed = ""
}
// map-fixed с DataDescriptor: если значение пустое или "{}" — строим JSON из дефолтов sub-params.
dataType := strings.ToLower(param.DataType)
if (dataType == "map-fixed" || strings.HasPrefix(dataType, "map")) && len(param.DataDescriptor) > 0 {
if trimmed == "" || trimmed == "{}" {
return buildMapFixedDefault(param)
}
return trimmed
}
if trimmed != "" {
return trimmed
}
nameHint := strings.ToLower(param.Name + " " + param.Code + " " + param.Label + " " + param.SvcOperationCfsParam)
if strings.Contains(dataType, "array") || strings.Contains(nameHint, "array") || strings.Contains(nameHint, "list") {
return "[]"
}
if strings.Contains(dataType, "map") || strings.Contains(dataType, "json") || strings.Contains(nameHint, "map") || strings.Contains(nameHint, "json") {
return "{}"
}
if strings.Contains(dataType, "integer") || strings.Contains(dataType, "int") {
return "0"
}
if strings.Contains(dataType, "boolean") || strings.Contains(dataType, "bool") {
return "false"
}
return trimmed
}
@@ -0,0 +1,258 @@
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
}
// ВАЖНО: в Required-атрибут нельзя писать null — после apply state обязан совпасть с планом,
// иначе Terraform вернёт "Provider produced inconsistent result after apply". Если ключа ещё нет
// (SNAT ни разу не включали, HAR fresh-create) — оставляем текущее значение state.
if raw, ok := live["ipSpaceName"]; ok && strings.TrimSpace(raw) != "" {
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 {
// Реальная ошибка API (не «шлюза нет») — нельзя молча терять SNAT: ресурс уйдёт из state,
// а SNAT останется включённым.
resp.Diagnostics.AddError("Ошибка клиента", err.Error())
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("клиент не инициализирован")
}
// Пустую строку молча подменять нельзя (скрытое поведение + риск вечного diff).
// Выключение SNAT — явное каноническое значение "no-needed".
value := strings.TrimSpace(ipSpaceName.ValueString())
if value == "" {
return fmt.Errorf("ip_space_name не может быть пустым: укажите имя ipSpace или %q для выключения SNAT", 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,
})
}
@@ -0,0 +1,413 @@
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"`
Organization types.String `tfsdk:"organization"`
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. " +
"Организация создаётся вручную в ЛК, в конфиге указывается её имя или UUID. " +
"Операция имеет replace-семантику: массив перезаписывается целиком.",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"organization": schema.StringAttribute{
Required: true,
MarkdownDescription: "Организация, на которой выделяются внешние IP: имя из ЛК (например `organ`) " +
"или её UUID.",
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"vip_configure": schema.StringAttribute{
Required: true,
MarkdownDescription: "JSON-массив аллокаций: `[{\"name\":\"internet-ipv4-v1\",\"count\":\"3\"}]`. " +
"Значение перезаписывает текущую аллокацию целиком. `count` — строка. " +
"Порядок ключей и форматирование не важны (сравнение смысловое). " +
"Снять аллокацию (`[]`) через этот атрибут **нельзя** — только удалением ресурса (`destroy`).",
},
"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
}
orgUID, err := r.resolveOrganizationUID(ctx, plan.Organization)
if err != nil {
resp.Diagnostics.AddError("Ошибка клиента", err.Error())
return
}
if err := r.applyAllocation(ctx, orgUID, plan.VIPConfigure); err != nil {
resp.Diagnostics.AddError("Ошибка клиента", err.Error())
return
}
plan.ID = types.StringValue(orgUID)
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
}
orgUID, err := r.resolveOrganizationUID(ctx, plan.Organization)
if err != nil {
resp.Diagnostics.AddError("Ошибка клиента", err.Error())
return
}
if err := r.applyAllocation(ctx, orgUID, plan.VIPConfigure); err != nil {
resp.Diagnostics.AddError("Ошибка клиента", err.Error())
return
}
plan.ID = types.StringValue(orgUID)
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
}
if strings.TrimSpace(state.Organization.ValueString()) == "" || r.client == nil {
return
}
orgUID, err := r.resolveOrganizationUID(ctx, state.Organization)
if err != nil {
resp.Diagnostics.AddError("Ошибка клиента", err.Error())
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
}
// Атрибут принадлежит пользователю: НЕ переписываем его, если смысл совпал — иначе Terraform
// увидит расхождение config vs state и покажет ложный дрейф (jsonencode отдаёт ключи по алфавиту).
// Писать null в Required-атрибут тоже нельзя (это даёт "Provider produced inconsistent result").
raw, ok := live["vIPConfigure"]
if ok {
liveItems, parseErr := parseVipConfigure(raw)
if parseErr != nil {
resp.Diagnostics.AddError("Ошибка чтения состояния", parseErr.Error())
return
}
stateItems, _ := parseVipConfigure(state.VIPConfigure.ValueString())
if !vipAllocationsEqual(liveItems, stateItems) {
state.VIPConfigure = types.StringValue(formatVipConfigure(liveItems))
}
}
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
}
if strings.TrimSpace(state.Organization.ValueString()) == "" || r.client == nil {
return
}
orgUID, err := r.resolveOrganizationUID(ctx, state.Organization)
if err != nil {
resp.Diagnostics.AddError("Ошибка клиента", err.Error())
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 {
// Реальная ошибка API (не «инстанса нет») — нельзя молча терять квоту: ресурс уйдёт из state,
// а выделенные IP останутся висеть.
resp.Diagnostics.AddError("Ошибка клиента", err.Error())
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("organization"), uid)...)
}
// resolveOrganizationUID принимает имя организации из ЛК или её UUID и возвращает UUID.
// Резолв делает клиент — тем же путём, что сгенерированный nubes_vc_vdc
// (core.ResolveRefSvcParamValue, сравн. 21_vc_vdc_resource.go).
func (r *OrgIpAllocationResource) resolveOrganizationUID(ctx context.Context, organization types.String) (string, error) {
if r.client == nil {
return "", fmt.Errorf("клиент не инициализирован")
}
raw := strings.TrimSpace(organization.ValueString())
if raw == "" {
return "", fmt.Errorf("organization обязателен")
}
resolved, err := r.client.ResolveRefSvcParamValue(ctx, 19, raw)
if err != nil {
return "", fmt.Errorf("не удалось определить организацию %q: %w", raw, err)
}
resolved = strings.TrimSpace(resolved)
if resolved == "" {
return "", fmt.Errorf("организация %q не найдена", raw)
}
return resolved, nil
}
// applyAllocation отправляет modify с массивом vIPConfigure целиком.
func (r *OrgIpAllocationResource) applyAllocation(ctx context.Context, orgUID string, vipConfigure types.String) error {
uid := strings.TrimSpace(orgUID)
if uid == "" {
return fmt.Errorf("organization обязателен")
}
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":"…"}]
// (порядок ключей name,count; count — строка). Канон ЕДИНЫЙ для отправки и для Read,
// иначе план и state расходятся по строке — см. vipConfigureCanonical.
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))
}
return "[" + strings.Join(parts, ",") + "]"
}
// vipAllocationsEqual сравнивает аллокации по СМЫСЛУ: порядок элементов и формат не важны.
// Имена ipSpace в рамках организации уникальны, поэтому сравнение идёт по имени.
func vipAllocationsEqual(a, b []vipAllocation) bool {
if len(a) != len(b) {
return false
}
byName := make(map[string]string, len(b))
for _, item := range b {
byName[item.Name] = item.Count
}
for _, item := range a {
count, ok := byName[item.Name]
if !ok || count != item.Count {
return false
}
}
return true
}
@@ -0,0 +1,257 @@
package resources_core
import (
"context"
"reflect"
"strings"
"terraform-provider-nubes/internal/core"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/types"
)
type StateField struct {
Code string
}
type InputField struct {
Code string
Field string
Type string
}
var (
typeString = reflect.TypeOf(types.String{})
typeBool = reflect.TypeOf(types.Bool{})
typeInt64 = reflect.TypeOf(types.Int64{})
typeMap = reflect.TypeOf(types.Map{})
typeList = reflect.TypeOf(types.List{})
)
func RefreshResourceState[T any](ctx context.Context, client *core.UniversalClient, instanceID string, serviceID int, state T, outputs []StateField, inputs []InputField) (T, diag.Diagnostics) {
var diags diag.Diagnostics
if client == nil || strings.TrimSpace(instanceID) == "" {
return state, diags
}
out, outDiags := FetchInstanceOutputs(ctx, client, instanceID)
diags.Append(outDiags...)
v := reflect.ValueOf(&state).Elem()
if v.Kind() != reflect.Struct {
return state, diags
}
stateOutMap := map[string]string{}
if !out.StateOut.IsNull() && !out.StateOut.IsUnknown() {
mapped, mapDiags := StringMapFromTypesMap(ctx, out.StateOut)
diags.Append(mapDiags...)
if len(mapped) > 0 {
stateOutMap = mapped
}
}
for _, field := range outputs {
code := strings.TrimSpace(field.Code)
if code == "" {
continue
}
fieldName := toCamel(code)
fv := v.FieldByName(fieldName)
if !fv.IsValid() || !fv.CanSet() {
continue
}
switch code {
case "state_params":
setFieldValue(fv, out.StateParams)
case "state_out":
setFieldValue(fv, out.StateOut)
case "state_params_flat":
setFieldValue(fv, out.StateParamsFlat)
case "state_out_flat":
setFieldValue(fv, out.StateOutFlat)
case "vault_secrets":
setFieldValue(fv, out.VaultSecrets)
case "vault_url":
setFieldValue(fv, out.VaultUrl)
case "vault_user_path":
setFieldValue(fv, out.VaultUserPath)
case "vault_fields":
setFieldValue(fv, out.VaultFields)
default:
if val, ok := stateOutMap[code]; ok {
setFieldValue(fv, ParseString(val))
}
}
}
paramsMap, paramsDiags := StringMapFromTypesMap(ctx, out.StateParams)
diags.Append(paramsDiags...)
if serviceID > 0 && len(paramsMap) > 0 {
paramsMap, paramsDiags = ResolveRefSvcParamDisplayNames(ctx, client, serviceID, paramsMap)
diags.Append(paramsDiags...)
}
resourceRealm := strings.TrimSpace(paramsMap["resourceRealm"])
if resourceRealm != "" {
outFlatMap, flatDiags := StringMapFromTypesMap(ctx, out.StateOutFlat)
diags.Append(flatDiags...)
if len(outFlatMap) > 0 {
fixed := FixInternalConnectMasterSuffix(outFlatMap, resourceRealm)
if fixed {
out.StateOutFlat, diags = mapValueFrom(ctx, outFlatMap, diags)
fv := v.FieldByName(toCamel("state_out_flat"))
if fv.IsValid() && fv.CanSet() {
setFieldValue(fv, out.StateOutFlat)
}
}
}
}
for _, input := range inputs {
code := strings.TrimSpace(input.Code)
if code == "" {
continue
}
// Поле модели достаём ДО проверки наличия значения в API: оно нужно
// и в ветке "API не вернул код" (см. схлопывание unknown → null ниже).
fieldName := strings.TrimSpace(input.Field)
if fieldName == "" {
fieldName = toCamel(code)
}
fv := v.FieldByName(fieldName)
if !fv.IsValid() || !fv.CanSet() {
continue
}
value, ok := paramsMap[code]
if !ok {
// ИНВАРИАНТ: Computed-атрибут обязан быть KNOWN после apply/read.
//
// Параметры, которые провайдер читает обратно, объявлены в схеме как
// Optional+Computed (см. helpers.ShouldBeOptionalComputed). Если
// пользователь такой параметр не задал, в плане он = unknown, и именно
// провайдер обязан проставить конкретное значение. Когда платформа
// не вернула код в state_params, единственное корректное конкретное
// значение — null.
//
// Если оставить unknown, Terraform упадёт с
// "Provider produced invalid result object after apply: ... was unknown".
if inputFieldUnknown(fv) {
setInputFieldNull(fv)
}
continue
}
if strings.EqualFold(code, "jsonEnv") && fv.Type() == typeString {
// Preserve planned json_env when API returns equivalent JSON with different ordering.
if planned, ok := fv.Interface().(types.String); ok && !planned.IsNull() && !planned.IsUnknown() {
if JSONStringsEquivalent(planned.ValueString(), value) {
fv.Set(reflect.ValueOf(planned))
continue
}
}
}
switch strings.ToLower(strings.TrimSpace(input.Type)) {
case "bool":
if fv.Type() == typeBool {
fv.Set(reflect.ValueOf(ParseBool(value)))
}
case "int", "int64", "number":
if fv.Type() == typeInt64 {
fv.Set(reflect.ValueOf(ParseInt64(value)))
}
default:
if fv.Type() == typeString {
fv.Set(reflect.ValueOf(ParseString(value)))
}
}
}
return state, diags
}
// inputFieldUnknown сообщает, находится ли поле модели в состоянии unknown.
//
// Зачем: соблюдение инварианта «Computed-атрибут обязан быть known после
// apply/read». Если платформа не вернула значение в state_params, unknown
// оставлять нельзя — его надо схлопнуть в null (см. setInputFieldNull).
func inputFieldUnknown(fv reflect.Value) bool {
switch fv.Type() {
case typeString:
v, ok := fv.Interface().(types.String)
return ok && v.IsUnknown()
case typeBool:
v, ok := fv.Interface().(types.Bool)
return ok && v.IsUnknown()
case typeInt64:
v, ok := fv.Interface().(types.Int64)
return ok && v.IsUnknown()
}
return false
}
// setInputFieldNull записывает в поле модели типизированный null.
//
// Зачем: null — это конкретное (known) значение, в отличие от unknown. Именно
// null приводит состояние Terraform в консистентный вид, когда платформа не
// сообщила значение для read-back параметра.
func setInputFieldNull(fv reflect.Value) {
switch fv.Type() {
case typeString:
fv.Set(reflect.ValueOf(types.StringNull()))
case typeBool:
fv.Set(reflect.ValueOf(types.BoolNull()))
case typeInt64:
fv.Set(reflect.ValueOf(types.Int64Null()))
}
}
func setFieldValue(field reflect.Value, value interface{}) {
switch field.Type() {
case typeString:
switch v := value.(type) {
case types.String:
field.Set(reflect.ValueOf(v))
case string:
field.Set(reflect.ValueOf(ParseString(v)))
}
case typeBool:
if v, ok := value.(types.Bool); ok {
field.Set(reflect.ValueOf(v))
}
case typeInt64:
if v, ok := value.(types.Int64); ok {
field.Set(reflect.ValueOf(v))
}
case typeMap:
if v, ok := value.(types.Map); ok {
field.Set(reflect.ValueOf(v))
}
case typeList:
if v, ok := value.(types.List); ok {
field.Set(reflect.ValueOf(v))
}
}
}
func toCamel(s string) string {
parts := strings.FieldsFunc(s, func(r rune) bool { return r == '_' || r == '-' })
for i, p := range parts {
if len(p) == 0 {
continue
}
parts[i] = strings.ToUpper(p[:1]) + p[1:]
}
out := strings.Join(parts, "")
if out == "" {
return "R"
}
first := rune(out[0])
if (first >= 'A' && first <= 'Z') || (first >= 'a' && first <= 'z') || first == '_' {
return out
}
return "R" + out
}
+78
View File
@@ -0,0 +1,78 @@
# Список языковых моделей и агентов
> Данные перенесены из предоставленного списка. Достоверность названий, параметров и стоимости отдельно не проверялась.
## 1. DeepSeek
| Имя | Размер контекста | Возможности | Вход | Выход |
|---|---:|---|---:|---:|
| DeepSeek V4 Flash | 1M | Инструменты, Видение | Не указана | Не указана |
| DeepSeek V4 Flash Vision Exp | 1M | Инструменты, Видение | Не указана | Не указана |
| DeepSeek V4 Pro | 1M | Инструменты, Видение | Не указана | Не указана |
| DeepSeek V4.1 Hash | 1M | Инструменты, Видение | Не указана | Не указана |
## 2. Copilot
| Имя | Размер контекста | Возможности | Вход | Выход |
|---|---:|---|---:|---:|
| Auto | Не указан | Инструменты | Не указана | Не указана |
| Claude Fable 5 | 1M | Инструменты, Видение | 1000 | 5000 |
| Claude Fable 5.1 | 1M | Инструменты, Видение | 1000 | 5000 |
| Claude Haiku 4.5 | 160K | Инструменты, Видение | 100 | 500 |
| Claude Opus 4.7 | 1M | Инструменты, Видение | 500 | 2500 |
| Claude Opus 4.8 | 1M | Инструменты, Видение | 500 | 2500 |
| Claude Opus 4.8 (fast mode) (Preview) | 1M | Инструменты, Видение | 1000 | 5000 |
| Claude Opus 5 | 1M | Инструменты, Видение | 500 | 2500 |
| Claude Opus 5.5 | 1M | Инструменты, Видение | 400 | 2000 |
| Claude Sonnet 5 | 1M | Инструменты, Видение | 200 | 1000 |
| Gemini 3.5 Flash | 1M | Инструменты, Видение | 150 | 900 |
| Gemini 3.6 Flash | 1M | Инструменты, Видение | 75 | 375 |
| Gemini 3.7 Flash | 1M | Инструменты, Видение | 75 | 375 |
| Gemini 3.8 Flash | 1M | Инструменты, Видение | 75 | 375 |
| GPT-5 mini | 192K | Инструменты, Видение | 25 | 200 |
| GPT-5.3-Codex | 400K | Инструменты, Видение | 175 | 1400 |
| GPT-5.4 | 1M | Инструменты, Видение | 250 | 1500 |
| GPT-5.4 mini | 400K | Инструменты, Видение | 75 | 450 |
| GPT-5.5 | 1M | Инструменты, Видение | 500 | 3000 |
| GPT-5.6 Luna | 1M | Инструменты, Видение | 20 | 120 |
| GPT-5.6 Sol | 1M | Инструменты, Видение | 400 | 2000 |
| GPT-5.6 Terra | 1M | Инструменты, Видение | 200 | 1200 |
| GPT-6 Astra | 1M | Инструменты, Видение | 1000 | 5000 |
| GPT-6 Luna | 1M | Инструменты, Видение | 10 | 50 |
## 3. Общий список языковых моделей
Стоимость указана в кредитах за 1 млн токенов.
| Имя | Размер контекста | Возможности | Вход | Выход |
|---|---:|---|---:|---:|
| Claude Fable 5 | 1M | Инструменты, Видение | 1000 | 5000 |
| Claude Fable 5.1 | 1M | Инструменты, Видение | 1000 | 5000 |
| Claude Haiku 4.5 | 160K | Инструменты, Видение | 100 | 500 |
| Claude Opus 4.7 | 1M | Инструменты, Видение | 500 | 2500 |
| Claude Opus 4.8 | 1M | Инструменты, Видение | 500 | 2500 |
| Claude Opus 4.8 (fast mode) | 1M | Инструменты, Видение | 1000 | 5000 |
| Claude Opus 5 | 1M | Инструменты, Видение | 500 | 2500 |
| Claude Opus 5.5 | 1.1M | Инструменты, Видение | 400 | 2000 |
| Claude Sonnet 5 | 1M | Инструменты, Видение | 200 | 1000 |
| Gemini 3.5 Flash | 1M | Инструменты, Видение | 150 | 900 |
| Gemini 3.6 Flash | 1M | Инструменты, Видение | 75 | 375 |
| Gemini 3.7 Flash | 1M | Инструменты, Видение | 75 | 375 |
| Gemini 3.8 Flash | 1M | Инструменты, Видение | 75 | 375 |
| GPT-5 mini | 192K | Инструменты, Видение | 25 | 200 |
| GPT-5.3-Codex | 400K | Инструменты, Видение | 175 | 1400 |
| GPT-5.4 | 1M | Инструменты, Видение | 250 | 1500 |
| GPT-5.4 mini | 400K | Инструменты, Видение | 75 | 450 |
| GPT-5.5 | 1M | Инструменты, Видение | 500 | 3000 |
| GPT-5.6 Luna | 1M | Инструменты, Видение | 20 | 120 |
| GPT-5.6 Sol | 1M | Инструменты, Видение | 400 | 2000 |
| GPT-5.6 Terra | 1M | Инструменты, Видение | 200 | 1200 |
| GPT-6 Astra | 1.1M | Инструменты, Видение | 1000 | 5000 |
| GPT-6 Luna | 1M | Инструменты, Видение | 10 | 50 |
| GPT-6 Sol | 1M | Инструменты, Видение | 200 | 1000 |
| Grok 4.5 | 500K / 553K | Инструменты, Видение | 200 | 600 |
| Grok 4.6 | 500K / 553K | Инструменты, Видение | 200 | 600 |
| Grok 4.7 | 500K / 553K | Инструменты, Видение | 200 | 600 |
| Kimi K2.7 Code | 256K | Инструменты, Видение | 95 | 400 |
| Kimi K3 | 1M | Инструменты, Видение | 300 | 1500 |
| MAI Code 1.1-Flash | 256K | Инструменты, Видение | 20 | 120 |
+176
View File
@@ -0,0 +1,176 @@
# adidas Terrex Anylander Unisex Trail Shoes
## Навигация и доступность
- Нажмите Alt+1 для режима чтения с экрана.
- Нажмите Alt+0 для отмены.
- Используйте сайт в режиме чтения с экрана.
- Руководство по доступности для чтения с экрана, отзывов и сообщений о проблемах.
- Перейти к содержимому.
## Магазин
Cosmos Sport
- 22 594000
- Заработайте x10 CASHBACK при первой покупке GoX.
- EASY RETURN
- FIND YOUR ORDER
- «GOX» и найдите предложение.
- Новинки
- Мужчины
- Женщины
- Дети
- Аксессуары
- Спорт
- Бренды
- Распродажи
- Подарочная карта
## Предложения
- Новинки. Только что запущено.
- Не пропустите: заработайте до 20 € в GoX Wallet.
- Не пропустите: On Shoes.
- Откройте коллекцию.
- Чёрные кроссовки.
- Исследуйте коллекцию.
## Товар
**Главная → Спорт → Лыжи → adidas Terrex Anylander Unisex Trail Shoes**
- Посмотреть похожие товары
- Бренд: adidas Terrex
- Название: adidas Terrex Anylander Unisex Trail Shoes
- Цена: €75.00
- Артикул: `9000198351_63596`
- Размер: выберите размер
- Таблица размеров
- Добавить в корзину
- Оформить заказ
- Наличие в магазинах
- Самовывоз из магазина за 2 часа
- Выберите цвет
Участник GoX получает 0,75 € кэшбэка. Зарегистрироваться.
- Бесплатный возврат в течение 30 дней.
- BOX NOW Lockers: быстрая доставка 24/7.
- Ваш заказ уже в пути!
- История цен.
## Характеристики
- Вес: 390 г.
- Перепад: 10 мм.
## Описание
### Уход за обувью
### Доставка и возврат
Бесплатная доставка и возврат.
### Описание товара
adidas Terrex Anylander Unisex Trail Shoes предназначены для коротких прогулок по лесу и длительных однодневных походов. Эти туристические кроссовки adidas Terrex обеспечивают поддержку и комфорт на разных типах троп. Низкая посадка и мягкая амортизация в межподошве обеспечивают лёгкость и комфорт. Рельефная подошва Traxion даёт сцепление во всех направлениях и помогает уверенно держаться на поверхности.
Используя переработанные материалы, adidas повторно применяет уже созданные материалы и сокращает отходы. Возобновляемые материалы помогают снизить зависимость от ограниченных ресурсов. В моделях, изготовленных из смеси переработанных и возобновляемых материалов, их общая доля составляет не менее 20%.
### Особенности
- Обычная посадка.
- Шнуровка.
- Текстильный верх с усиленным носком.
- Текстильная подкладка.
- Межподошва EVA.
- Подошва Traxion.
- Вес: 390 г, размер UK 8,5.
- Перепад межподошвы: 10 мм; пятка 27 мм, носок 17 мм.
- Содержит не менее 20% переработанных и возобновляемых материалов.
- Цвет: чёрный.
- SKU: `ID0895`
- Артикул: `9000198351_63596`
## О бренде adidas Terrex
adidas Terrex — линейка экипировки adidas для пешего туризма, альпинизма, трейлраннинга и скалолазания. Она является значимым продолжением спортивных традиций adidas и предлагает инновационные решения для активного отдыха.
Линейка была запущена в конце 2000-х годов, когда adidas заметила растущую потребность в прочной и высокопроизводительной экипировке. Ассортимент Terrex значительно расширился и включает товары, отличающиеся долговечностью, технологичностью и функциональностью.
Terrex уделяет внимание устойчивому развитию и стремится уменьшить воздействие на окружающую среду. Бренд использует переработанные материалы и внедряет экологичные производственные практики.
adidas Terrex продолжает развиваться и внедрять инновации, помогая спортсменам расширять свои возможности и безопасно наслаждаться природой.
## Связанные категории
- Купить больше: мужская одежда, обувь и аксессуары.
- Купить больше: женская коллекция.
- Купить больше: мужская одежда, обувь и аксессуары → мужская обувь.
- Купить больше: женская коллекция → женская обувь.
- Купить больше: модели для трейлраннинга и походов.
- Купить больше: мужская одежда, обувь и аксессуары adidas.
- Купить больше: мужская одежда, обувь и аксессуары → мужская обувь → мужские трейловые кроссовки.
## Сервисы Cosmos Sport
- Электронная подарочная карта: подарки на выбор среди более чем 20 000 моделей; номинал от 20 €.
- Бесплатный Click & Collect в 40 пунктах.
- BOX NOW: бесплатная доставка в постаматы.
- GoX Cashback: 1% кэшбэка в GoX Wallet для следующих покупок.
- Магазин для спортсменов: более 60 брендов.
- Бесплатный Click & Collect в более чем 40 пунктах.
- Бесплатный возврат в течение 30 дней.
- Бесплатная доставка при заказе от 49 €.
## Новости и контакты
CosmosNews: получайте первые предложения со скидками до 30% и эксклюзивные предложения.
Интересует:
- Мужчина.
- Женщина.
- Ребёнок.
Популярные ссылки:
- Найти заказ.
- Бесплатная доставка при заказе от 49 €.
- Бесплатный возврат в течение 30 дней.
- Контакты.
- 22 594000.
- Часы работы колл-центра: понедельник–пятница 09:00–21:00, суббота 09:00–20:00.
## Компания
- Магазины и пункты выдачи.
- Наша история.
- GoX.
- Условия и положения.
- Вакансии.
- Условия использования.
- Корпоративная социальная ответственность.
- Cosmos People.
- Льготы.
- TEAM WEAR.
## Обслуживание
- Служба поддержки.
- Контакты.
- Бесплатный возврат любым способом.
- Доставка и получение товаров.
- Способы оплаты.
- Бесплатный Click & Collect.
- Часто задаваемые вопросы.
- Подарочные карты и электронные подарочные карты.
- Политика конфиденциальности.
- Файлы cookie.
- Декларация о файлах cookie.
Присоединяйтесь: Facebook, Instagram.
© 2026 CosmosSport. Разработано Sleed.
+65 -14
View File
@@ -9,18 +9,21 @@ reflected here FIRST, then implemented in `gen_v2` and other tools.
## Core Principles ## Core Principles
1) YAML per service is generated ONLY from API data. 1) YAML per service is generated ONLY from API data.
2) The provider core is universal and must not contain service-specific logic. 2) The provider core (`provider/internal/core`) is universal and must not contain
3) Service-specific Go code is fully generated from YAML. No manual edits. service-specific logic.
3) Service-specific Go code (`provider/internal/resources_gen`) is fully generated
from YAML. No manual edits to generated code.
4) Documentation is generated from the same YAML. 4) Documentation is generated from the same YAML.
5) Build artifacts for 3 OS targets are published to the registry, and docs are 5) Build artifacts for 3 OS targets are published to the registry, and docs are
published to the website. published to the website.
6) `devops/ARCHITECTURE.md` (this file) is the primary spec. Code follows. 6) `TOOLS/ARCHITECTURE.md` (this file) is the primary spec. Code follows.
## Service Selection ## Service Selection
- The inclusion list is defined by: `devops/config/services_list.txt` (repo-relative path) - The inclusion list is defined by: `TOOLS/config/<dev|test|prod>/services_list.txt`
(по одному списку на стенд; repo-relative path)
- Each line starts with service_id, followed by service name/alias. - Each line starts with service_id, followed by service name/alias.
- Operation timeouts source is defined by: `devops/config/operation_timeouts.json`. - Operation timeouts source is defined by: `TOOLS/config/<dev|test|prod>/operation_timeouts.json`.
## API Endpoint ## API Endpoint
@@ -106,18 +109,38 @@ Each operation has a kind:
## Provider Model ## Provider Model
- Core is universal: no service-specific logic inside the core. - Core (`provider/internal/core`) is universal: no service-specific logic inside it.
- Generated service resources contain only schema/params and references. - Generated service resources (`provider/internal/resources_gen`) contain only
schema/params and references.
- Hand-written service resources live in `provider/internal/resources_core` and are
registered in `provider/internal/provider/provider.go` `Resources()`. They are
NOT generated; they must not contain arbitrary service logic, only:
- a schema, and
- wiring between schema fields and the universal core API
(`RunInstanceOperationUniversalByCode`, `ResolveRefSvcParamValue`, etc.).
### API Resilience ### API Resilience
- Core MUST retry transient 401 errors from Gateway (3 attempts, exponential backoff). - Core MUST retry transient 401 errors from Gateway (3 attempts, exponential backoff).
Gateway may temporarily reject valid JWT tokens. Gateway may temporarily reject valid JWT tokens.
- GET operations (GetInstanceState, GetInstanceStateRaw) retry 401 with 2s/4s/8s backoff. - Implemented: `isRetryable` (`core/http.go`) includes 401 alongside {429, 502, 503, 504};
- `doRequest` treats 401 as retryable for GET requests (alongside 429, 502, 503, 504). retry applies to GET requests only.
- Network errors and HTTP retryable statuses are retried for **GET only** (decided
2026-09-30). POST is NEVER retried: `POST /instanceOperations` is not idempotent, and a
blind retry would duplicate the operation. The API has no `Idempotency-Key` support.
- Draft operation on failure (decided 2026-09-30): if an error occurs after
`POST /instanceOperations` but before `.../run`, the operation remains created but was
never executed (the instance is unaffected). No cancellation API is known — neither the
provider code nor the captured HAR contain `DELETE /instanceOperations/{uid}`.
- Transient 401 is retried for GET (see `isRetryable`).
### Generated Code Resilience ### Generated Code Resilience
- **Partial state при ошибке create** (шаблон `instance.go`): если инстанс успел создаться
(`CreateResourceWithTimeout` вернул непустой uid ВМЕСТЕ с ошибкой), uid фиксируется в state
до вывода ошибки. Иначе облачный инстанс «осиротеет»: Terraform о нём не знает, а повторный
`apply` упрётся в страж дубликатов. Ядро (`CreateGenericInstanceUniversalV6`) возвращает uid
при любой ошибке ПОСЛЕ создания инстанса; пустой uid — только если создание не состоялось.
- **Zero-value fallback** (`normalizeUniversalValueV6`): если параметр отсутствует - **Zero-value fallback** (`normalizeUniversalValueV6`): если параметр отсутствует
в пользовательском `.tf`, подставлять zero-value по `dataType`: в пользовательском `.tf`, подставлять zero-value по `dataType`:
- `integer` → `"0"`, `boolean` → `"false"`, `map-fixed` → `"{}"`, `array` → `"[]"`, `string` → `""` - `integer` → `"0"`, `boolean` → `"false"`, `map-fixed` → `"{}"`, `array` → `"[]"`, `string` → `""`
@@ -207,28 +230,56 @@ From the unified YAML, generate:
5) Upload provider artifacts to registry. 5) Upload provider artifacts to registry.
6) Build and publish docs to site. 6) Build and publish docs to site.
## Lifecycle Vocabulary (single contract)
**Target contract (decided 2026-09-30):** two runtime flags — `keep_on_destroy` and
`suspend_on_destroy` — express destroy behaviour for ALL resource kinds. The compile-time
`delete_strategy` (YAML) is a generator input that MAPS onto them:
| `delete_strategy` | Runtime meaning |
|---|---|
| `noop_warn` | `keep_on_destroy = true` (platform effect left untouched) |
| `inverse` | normal destroy (inverse modify is performed) |
| `error` | destroy refused with a validation error |
Current state (2026-09-30) — to be migrated:
1. generated instance resources: already `suspend_on_destroy` / `keep_on_destroy`;
2. generated modifiers: `delete_strategy` only (no runtime flag yet);
3. hand-written modifiers: already `keep_on_destroy`.
## Non-Negotiable Rules ## Non-Negotiable Rules
- No manual edits to generated YAML or generated Go code. - No manual edits to generated YAML or generated Go code.
- Any change must come from API or generator logic updates. - Any change to generated code must come from API or generator logic updates.
- The generator must enforce these rules and fail fast on drift. - The generator must enforce these rules and fail fast on drift.
## Exception Registry (service-specific DATA, never logic) ## Exception Registry (service-specific DATA, never logic)
Principle: provider core and generator logic are universal for all stands and Principle: provider core and generator logic are universal for all stands and
services. The ONLY allowed deviations are DATA entries, and they MUST live in services. Service-specific deviations are of two kinds:
exactly two named registries:
1. **Generated modifiers** — a `modify` op that should become a dedicated modifier
resource. The generator (`TOOLS/resource-generator/internal/loader/loader.go`)
supports `kind: modifier` with `delete_strategy` (`noop_warn`/`inverse`/`error`)
and `idempotency` (`none`/`check_before_run`). The overlay data file
`modifiers.yaml` that would drive this is **documented but NOT yet created**;
until then, modifiers are hand-written in `provider/internal/resources_core/`.
(The legacy registry `serviceSpecificModifiers` in `TOOLS/yaml-generator/main.go`
was REMOVED during the 2026-09-23 refactoring.)
2. **Doc examples** — named registry:
| Registry | File | Declares | | Registry | File | Declares |
|---|---|---| |---|---|---|
| `serviceSpecificModifiers` | `TOOLS/yaml-generator/main.go` | which service `modify` op becomes a modifier resource and its name (key = normalized service name) |
| `serviceSpecificDocExamples` | `TOOLS/docs-generator/internal/writers/writers.go` | per-service doc examples, gated on service name + required state/vault keys | | `serviceSpecificDocExamples` | `TOOLS/docs-generator/internal/writers/writers.go` | per-service doc examples, gated on service name + required state/vault keys |
Rules: Rules:
- Key by stable service NAME (slug), never by raw numeric ID. - Key by stable service NAME (slug), never by raw numeric ID.
- Each entry answers WHAT / WHAT IT DOES / WHY / WHERE (see code comments). - Each entry answers WHAT / WHAT IT DOES / WHY / WHERE (see code comments).
- Adding an exception = editing one of these two registries → visible in diff. - Doc-example exception = editing the named registry above → visible in diff.
- Modifier exception (until `modifiers.yaml` exists) = hand-written resource in
`provider/internal/resources_core/` + registration in `provider.go`.
- Never annotate API-YAML: it is machine-regenerated and edits would be lost. - Never annotate API-YAML: it is machine-regenerated and edits would be lost.
Enforced by scripts (run before build/commit): Enforced by scripts (run before build/commit):
+39
View File
@@ -2,6 +2,45 @@
Каждый инструмент — независимый Go-модуль. Каждый инструмент — независимый Go-модуль.
## Канонический пайплайн (порядок шагов)
```bash
# 1) YAML-спеки сервисов из API (per-stand!)
./TOOLS/scripts/01_generate_yamls.sh --profile TOOLS/config/dev
# 2) Go-ресурсы + документация из этих YAML
./TOOLS/scripts/02_generate_resources_and_docs_v2.sh --profile TOOLS/config/dev
# 3) (релиз) сборка 3 платформ + публикация в реестр
./TOOLS/scripts/03_build_and_upload_provider.sh --profile TOOLS/config/dev
```
`--profile` обязателен: без него скрипты выходят с кодом 2 (никаких дефолтов).
### Шаг 1 — безопасная генерация YAML
`01_generate_yamls.sh` работает по принципу «сначала во временное, потом атомарная замена»:
- генерация идёт в staging-каталог `generated/<stand>/resources_yaml.staging.<pid>/`;
- рабочий `generated/<stand>/resources_yaml/` **не** удаляется и **не** модифицируется до полного успеха;
- при полном успехе старый каталог уезжает в бэкап `resources_yaml.bak-<UTC>`,
а staging встаёт на его место (атомарный `mv` в пределах одного FS),
хранятся последние `KEEP_BACKUPS` (по умолчанию 5);
- при любой ошибке замена **отменяется**: старый каталог цел, частичный результат лежит в staging для разбора, скрипт выходит с кодом 1;
- в каталоге лежит маркер `.stand`, защищающий от генерации не в тот стенд.
Перегенерировать все стенды подряд:
```bash
for s in dev test prod; do
./TOOLS/scripts/01_generate_yamls.sh --profile TOOLS/config/$s || break
done
```
> Примечание: часть параметров API отдаёт со случайным `default`-суффиксом
> (`db-ievgpdvu` → `db-ujama5rb` и т.п.), поэтому побайтовое сравнение двух
> прогонов даёт различия в этих строках — это не регрессия.
## yaml-generator ## yaml-generator
API Nubes → `resources_yaml/*.yaml` API Nubes → `resources_yaml/*.yaml`
+1 -1
View File
@@ -4,7 +4,7 @@ TOKEN_FILE="secrets/dev.token"
# Release versions # Release versions
# Version # Version
VERSION="2.0.17" VERSION="2.0.0"
NAMESPACE="nubes-dev" NAMESPACE="nubes-dev"
PROVIDER_NAME="nubes" PROVIDER_NAME="nubes"
+1 -1
View File
@@ -49,7 +49,7 @@
148 vcMgmtSthutrvalCluster # Менеджмент Kubernetes кластер Штурвал 148 vcMgmtSthutrvalCluster # Менеджмент Kubernetes кластер Штурвал
149 valoTenant # VALO Cloud 149 valoTenant # VALO Cloud
150 k8sSthutrvalCluster # Kubernetes кластер Штурвал 150 k8sSthutrvalCluster # Kubernetes кластер Штурвал
# 151 k8sOpenbao # Vault — нет в PROD UI 151 k8sOpenbao # Vault
# 153 nifi # Nifi (DEV) # 153 nifi # Nifi (DEV)
163 llmAi # LLM 163 llmAi # LLM
# 175 k8sGo # Go — нет в UI # 175 k8sGo # Go — нет в UI
-55
View File
@@ -1,55 +0,0 @@
# Общий список сервисов — объединение DEV/TEST/PROD
# service_id service_name # RU description
# do not delete lines; to exclude a service, comment it with #
1 dummy # Болванка
2 template # Темплейт k8s
12 s3 # S3 Object Storage
13 s3bucket # S3 бакет
19 vcOrg # Организация в Cloud Director
# 20 vcOrgSaas # Организация [DEPRECATED] — нет в UI
21 vc_vdc # Виртуальный датацентр (vDC)
22 vc_nsxt # Сетевой шлюз периметра (Edge)
# 23 vc_vm # VM в Cloud Director (старый формат) (vc_vm) — нет в UI
# 24 vcNat # DEPRECATED Правила маршрутизации для VM (vc_nat)
25 vcexternalip # Публичные IP адреса
26 vapp # Виртуальный каталог ВМ (vApp)
27 vc_vm_v2 # VM в Cloud Director (vc_vmV2)
28 vc_vm_v3 # Виртуальная машина
29 vcVdcGroup # Группа датацентров
# 32 vmpostgre # vc_vm_postgresql_std
50 nextcloud # Nextcloud
81 superset # Apache Superset
82 harbor # Container Registry
86 k8sVelero # Velero
# 87 k8svalkey # Valkey — нет в UI
# 88 k8sZitiController # k8sZitiController — нет в UI
89 flask # Web-сервер с фреймворком Flask
90 postgres # PostgreSQL
91 redis # Redis
92 mongodb # MongoDB
93 rabbitmq # RabbitMQ
94 lucee # Lucee
95 nodejs # NodeJS
96 pgadmin # pgAdmin
# 97 nodered # NodeRed
98 http # Простой HTTP контейнер
99 gitea # Gitea
# 100 openwhisk # Serverless Openwhisk — нет в UI
109 zonesV2 # Управление DNS
# 110 dnszone # DNS зона — нет в UI
111 dnsrecord # DNS запись
# 112 tenant # Тенант в Grafana — нет в UI
# 113 vcComplex # Быстрый старт — нет в UI
# 114 GiteaComplex # Комплексная услуга по созданию gitea — нет в UI
115 mariadb # Mariadb
116 kafka # ApacheKafka
# 117 nifi # Nifi
119 Akhq # Akhq
120 clickhouse # ClickHouse
148 vcMgmtSthutrvalCluster # Менеджмент Kubernetes кластер Штурвал
149 valoTenant # VALO Cloud
150 k8sSthutrvalCluster # Kubernetes кластер Штурвал
151 k8sOpenbao # Vault
# 153 nifi # Nifi (DEV)
163 llmAi # LLM
# 175 k8sGo # Go — нет в UI
@@ -313,6 +313,12 @@ func (r *{{ToCamel .Name}}Resource) Create(ctx context.Context, req resource.Cre
} }
id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, {{.ServiceID}}, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout) id, err := resources_core.CreateResourceWithTimeout(ctx, r.client, {{.ServiceID}}, resourceName, data.AdoptExistingOnCreate.ValueBool(), params, operationTimeout)
if err != nil { if err != nil {
// Partial state (Q5): фиксируем ТОЛЬКО id. Остальные атрибуты плана могут быть
// unknown/computed — запись их в state даёт "invalid new value ... unknown" и
// маскирует исходную ошибку. Read затем синхронизирует реальное состояние.
if id != "" {
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), types.StringValue(id))...)
}
resp.Diagnostics.AddError("Ошибка клиента", err.Error()) resp.Diagnostics.AddError("Ошибка клиента", err.Error())
return return
} }
+84 -84
View File
@@ -43,10 +43,9 @@ resolve_root_path() {
echo "${ROOT_DIR}/${path_value}" echo "${ROOT_DIR}/${path_value}"
} }
# Список сервисов — только из профиля стенда (TOOLS/config/<stand>/services_list.txt).
# Общий TOOLS/config/services_list.txt удалён 2026-09-30 (не использовался ни одним профилем).
SERVICES_FILE_DEFAULT="${PROFILE_DIR}/services_list.txt" SERVICES_FILE_DEFAULT="${PROFILE_DIR}/services_list.txt"
if [[ -f "${PROFILE_DIR}/services_list.txt" ]]; then
SERVICES_FILE_DEFAULT="${PROFILE_DIR}/services_list.txt"
fi
SERVICES_FILE="${SERVICES_FILE:-$SERVICES_FILE_DEFAULT}" SERVICES_FILE="${SERVICES_FILE:-$SERVICES_FILE_DEFAULT}"
SERVICES_FILE="$(resolve_root_path "$SERVICES_FILE")" SERVICES_FILE="$(resolve_root_path "$SERVICES_FILE")"
@@ -54,7 +53,7 @@ SERVICES_FILE="$(resolve_root_path "$SERVICES_FILE")"
REQUEST_DELAY="${REQUEST_DELAY:-0.5}" REQUEST_DELAY="${REQUEST_DELAY:-0.5}"
ATTEMPTS="${ATTEMPTS:-3}" ATTEMPTS="${ATTEMPTS:-3}"
# Auth: either an explicit token or a token file (latest *.token if not set). # Auth: токен из NUBES_API_TOKEN или из файла TOKEN_FILE (profile.env).
TOKEN_FILE="${TOKEN_FILE:-}" TOKEN_FILE="${TOKEN_FILE:-}"
NUBES_API_TOKEN="${NUBES_API_TOKEN:-}" NUBES_API_TOKEN="${NUBES_API_TOKEN:-}"
if [[ -n "$TOKEN_FILE" ]]; then if [[ -n "$TOKEN_FILE" ]]; then
@@ -62,37 +61,39 @@ if [[ -n "$TOKEN_FILE" ]]; then
fi fi
# Назначение: # Назначение:
# - Берет список сервисов из services_list.txt. # - Берет список сервисов из services_list.txt профиля стенда.
# - Для каждого сервиса запрашивает единый spec через API и пишет YAML в # - Для каждого сервиса запрашивает единый spec через API и пишет YAML в
# ${ROOT_DIR}/provider/resources_yaml. # ${ROOT_DIR}/generated/<stand>/resources_yaml (per-stand, см. ниже).
# - Формат имени файла: ID_имя.yaml (например, 13_s3bucket.yaml). # - Формат имени файла: ID_имя.yaml (например, 13_s3bucket.yaml).
# #
# Безопасность записи (best practice, с 2026-09-30):
# - Генерация идёт во временный каталог (staging), старый каталог не удаляется заранее.
# - Замена каталога атомарна и делается ТОЛЬКО при полном успехе; предыдущий
# каталог уезжает в бэкап resources_yaml.bak-<UTC>, хранятся последние KEEP_BACKUPS.
# - При любом сбое рабочий каталог остаётся нетронутым, частичный результат — в staging.
#
# Источники данных: # Источники данных:
# - Список сервисов: TOOLS/config/{stand}/services_list.txt # - Список сервисов: TOOLS/config/{stand}/services_list.txt
# - API endpoint: ${API_ENDPOINT} # - API endpoint: ${API_ENDPOINT} (обязателен, только из profile.env)
# - Токен: переменная NUBES_API_TOKEN или файл .token. # - Токен: переменная NUBES_API_TOKEN или файл TOKEN_FILE (из profile.env).
# #
# Формат services_list.txt: # Формат services_list.txt:
# - В начале строки: service_id. # - Поле 1: service_id (обязательно).
# - Остальное в строке — произвольная заметка для девопса (не используется). # - Поле 2: имя сервиса (используется для лога; можно опустить).
# - Всё после # — заметка для девопса (не используется).
# #
# Задержки и попытки: # Задержки и попытки:
# - REQUEST_DELAY (секунды) между запросами и ретраями (по умолчанию 0.2). # - REQUEST_DELAY (секунды) между запросами и ретраями (по умолчанию 0.5).
# - ATTEMPTS — число попыток на сервис (по умолчанию 3). # - ATTEMPTS — число попыток на сервис (по умолчанию 3).
# #
# Логи ошибок: # Логи ошибок:
# - Список неуспешных сервисов пишется в /tmp/yaml_gen_failures.txt. # - Список неуспешных сервисов пишется в generated/<stand>/tmp/yaml_gen_failures.txt.
# #
# Токен берется из переменной NUBES_API_TOKEN. # Токен: NUBES_API_TOKEN, иначе файл TOKEN_FILE (из profile.env).
# Если переменная пуста, берется файл .token из ${ROOT_DIR}: # Легаси-поиск «последнего» *.token в корне репо удален (2026-09-30): токенов там
# - "последний" = файл с самым новым временем изменения (ls -t | head -n 1). # нет, а такой поиск мог молча подхватить чужой токен.
if [[ -z "$NUBES_API_TOKEN" ]]; then if [[ -z "$NUBES_API_TOKEN" && -n "$TOKEN_FILE" && -f "$TOKEN_FILE" ]]; then
if [[ -z "$TOKEN_FILE" ]]; then NUBES_API_TOKEN=$(cat "$TOKEN_FILE")
TOKEN_FILE=$(ls -t "${ROOT_DIR}"/*.token 2>/dev/null | head -n 1 || true)
fi
if [[ -n "$TOKEN_FILE" && -f "$TOKEN_FILE" ]]; then
NUBES_API_TOKEN=$(cat "$TOKEN_FILE")
fi
fi fi
if [[ -z "$NUBES_API_TOKEN" ]]; then if [[ -z "$NUBES_API_TOKEN" ]]; then
@@ -105,12 +106,18 @@ if [[ ! -f "$SERVICES_FILE" ]]; then
exit 2 exit 2
fi fi
# ⛔ LEGACY: deck-api.ngcloud.ru ЗАКРЫВАЕТСЯ. Default = Gateway. Override via NUBES_API_ENDPOINT. # Endpoint ОБЯЗАТЕЛЕН: берется ТОЛЬКО из NUBES_API_ENDPOINT (profile.env).
API_ENDPOINT="${NUBES_API_ENDPOINT:-https://lk-api-gateway.ngcloud.ru/api/v1/svc}" # Легаси-фолбэк на PROD gateway удален (2026-09-30): молчаливый уход в прод недопустим.
if [[ -z "${NUBES_API_ENDPOINT:-}" ]]; then
echo "Error: NUBES_API_ENDPOINT is required (set it in ${PROFILE_DIR}/profile.env)." >&2
exit 2
fi
API_ENDPOINT="$NUBES_API_ENDPOINT"
# Auto-detect API style: if endpoint contains "index.cfm" → legacy proxy (?endpoint=), # Auto-detect API style: if endpoint contains "index.cfm" → legacy proxy (?endpoint=),
# otherwise → new REST gateway (direct paths). No forced /index.cfm normalization. # otherwise → new REST gateway (direct paths). No forced /index.cfm normalization.
GENERATED_DIR="${ROOT_DIR}/generated/$(basename "$PROFILE_DIR")" STAND="$(basename "$PROFILE_DIR")"
GENERATED_DIR="${ROOT_DIR}/generated/${STAND}"
YAML_OUTPUT_DIR_DEFAULT="${GENERATED_DIR}/resources_yaml" YAML_OUTPUT_DIR_DEFAULT="${GENERATED_DIR}/resources_yaml"
FAILURES_FILE_DEFAULT="${GENERATED_DIR}/tmp/yaml_gen_failures.txt" FAILURES_FILE_DEFAULT="${GENERATED_DIR}/tmp/yaml_gen_failures.txt"
@@ -119,12 +126,27 @@ FAILURES_FILE="${FAILURES_FILE:-$FAILURES_FILE_DEFAULT}"
YAML_OUTPUT_DIR="$(resolve_root_path "$YAML_OUTPUT_DIR")" YAML_OUTPUT_DIR="$(resolve_root_path "$YAML_OUTPUT_DIR")"
FAILURES_FILE="$(resolve_root_path "$FAILURES_FILE")" FAILURES_FILE="$(resolve_root_path "$FAILURES_FILE")"
# Убедимся, что папка есть. Полной очистки нет: обрабатываем только сервисы из списка. YAML_PARENT_DIR="$(dirname "$YAML_OUTPUT_DIR")"
mkdir -p "$YAML_OUTPUT_DIR" YAML_BASE_NAME="$(basename "$YAML_OUTPUT_DIR")"
mkdir -p "$YAML_PARENT_DIR"
mkdir -p "$(dirname "$FAILURES_FILE")" mkdir -p "$(dirname "$FAILURES_FILE")"
if [[ ! -f "${YAML_OUTPUT_DIR}/embed.go" ]]; then # Защита от генерации не в тот стенд: каталог помечается маркером .stand.
cat > "${YAML_OUTPUT_DIR}/embed.go" <<'EOF' if [[ -d "$YAML_OUTPUT_DIR" && -f "${YAML_OUTPUT_DIR}/.stand" ]]; then
existing_stand="$(cat "${YAML_OUTPUT_DIR}/.stand" 2>/dev/null || true)"
if [[ -n "$existing_stand" && "$existing_stand" != "$STAND" ]]; then
echo "Error: ${YAML_OUTPUT_DIR} belongs to stand '${existing_stand}', not '${STAND}'. Refusing." >&2
exit 2
fi
fi
# Staging: старый каталог НЕ удаляется заранее, замена — атомарная (mv в конце).
STAGING_DIR="${YAML_OUTPUT_DIR}.staging.$$"
rm -rf "$STAGING_DIR"
mkdir -p "$STAGING_DIR"
# embed.go обязателен: пакет resources_yaml используется через go:embed *.yaml.
cat > "${STAGING_DIR}/embed.go" <<'EOF'
package resources_yaml package resources_yaml
import "embed" import "embed"
@@ -134,7 +156,7 @@ import "embed"
//go:embed *.yaml //go:embed *.yaml
var Files embed.FS var Files embed.FS
EOF EOF
fi printf '%s\n' "$STAND" > "${STAGING_DIR}/.stand"
rm -f "$FAILURES_FILE" rm -f "$FAILURES_FILE"
@@ -164,7 +186,12 @@ while IFS= read -r line; do
continue continue
fi fi
sid=$(echo "$line" | awk '{print $1}') sid=$(echo "$line" | awk '{print $1}')
svc_name="" # Имя сервиса берётся из 2-го поля services_list.txt. API ради имени не дёргаем:
# Go-генератор сам запрашивает spec и нормализует имя (это убирает лишний запрос).
svc_name="$(echo "$line" | awk '{print $2}')"
if [[ "$svc_name" == \#* ]]; then
svc_name=""
fi
if [[ -z "$sid" ]]; then if [[ -z "$sid" ]]; then
continue continue
@@ -176,59 +203,13 @@ while IFS= read -r line; do
continue continue
fi fi
# Если имя не указано в списке — подтягиваем по API. # Один YAML на сервис формируется Go-генератором — пишем во временный каталог.
if [[ -z "$svc_name" ]]; then
svc_name=$(python3 - <<PY
import json, sys, time, urllib.parse, urllib.request
sid = "${sid}"
endpoint = "${API_ENDPOINT}"
token = "${NUBES_API_TOKEN}"
max_retries = 3
# Auto-detect API style: "index.cfm" → legacy proxy, otherwise → REST gateway.
if "index.cfm" in endpoint:
params = urllib.parse.urlencode({"endpoint": f"/services/{sid}"})
url = f"{endpoint}?{params}"
else:
url = f"{endpoint}/services/{sid}"
for attempt in range(1, max_retries + 1):
try:
req = urllib.request.Request(url)
if token:
req.add_header("Authorization", f"Bearer {token}")
req.add_header("User-Agent", "Mozilla/5.0 (compatible; Terraform-Provider-Nubes/BashGenerator)")
with urllib.request.urlopen(req, timeout=30) as resp:
data = json.loads(resp.read().decode("utf-8"))
svc = data.get("svc", {})
name = svc.get("svcShort") or svc.get("name") or svc.get("title") or f"service_{sid}"
print(name)
sys.exit(0)
except Exception as e:
if attempt < max_retries:
time.sleep(2 * attempt)
else:
print(f"ERROR: failed to fetch service {sid} after {max_retries} attempts: {e}", file=sys.stderr)
sys.exit(1)
PY
)
if [[ "$svc_name" == ERROR:* ]]; then
echo "$svc_name" >&2
echo "${sid} api_error" >> "$FAILURES_FILE"
continue
fi
fi
# Один YAML на сервис формируется Go-генератором.
echo "Generating unified spec for ${sid} (${svc_name})" echo "Generating unified spec for ${sid} (${svc_name})"
success=0 success=0
# Generator normalizes names, so verify output by ID prefix only. # Generator normalizes names, so verify output by ID prefix only.
output_glob="${YAML_OUTPUT_DIR}/${sid}_*.yaml" output_glob="${STAGING_DIR}/${sid}_*.yaml"
# Для выбранного сервиса удаляем старый YAML и генерируем заново с ретраями.
rm -f "$output_glob"
# Старый YAML НЕ удаляем заранее: полная замена каталога — атомарная, в конце.
# Генерация YAML для выбранного сервиса с ретраями. # Генерация YAML для выбранного сервиса с ретраями.
for attempt in $(seq 1 "$ATTEMPTS"); do for attempt in $(seq 1 "$ATTEMPTS"); do
( (
@@ -237,7 +218,7 @@ PY
NUBES_SERVICE_ID="$sid" \ NUBES_SERVICE_ID="$sid" \
NUBES_SERVICE_NAME="$svc_name" \ NUBES_SERVICE_NAME="$svc_name" \
NUBES_API_ENDPOINT="$API_ENDPOINT" \ NUBES_API_ENDPOINT="$API_ENDPOINT" \
NUBES_OUTPUT_DIR="$YAML_OUTPUT_DIR" \ NUBES_OUTPUT_DIR="$STAGING_DIR" \
"${ROOT_DIR}/TOOLS/bin/yaml-generator" "${ROOT_DIR}/TOOLS/bin/yaml-generator"
) && success=1 || success=0 ) && success=1 || success=0
@@ -265,9 +246,28 @@ PY
done < "$SERVICES_FILE" done < "$SERVICES_FILE"
echo "Done. YAML files are in ${YAML_OUTPUT_DIR}" if [[ -s "$FAILURES_FILE" ]]; then
if [[ -f "$FAILURES_FILE" ]]; then
echo "Failed services (see $FAILURES_FILE):" >&2 echo "Failed services (see $FAILURES_FILE):" >&2
cat "$FAILURES_FILE" >&2 cat "$FAILURES_FILE" >&2
echo "" >&2
echo "Замена каталога ОТМЕНЕНА: рабочий ${YAML_OUTPUT_DIR} не тронут." >&2
echo "Частичный результат оставлен для разбора: $STAGING_DIR" >&2
exit 1 exit 1
fi fi
# Атомарная замена: старый каталог уезжает в бэкап, новый встаёт на его место.
BACKUP_DIR="${YAML_PARENT_DIR}/${YAML_BASE_NAME}.bak-$(date -u +%Y%m%dT%H%M%SZ)"
if [[ -d "$YAML_OUTPUT_DIR" ]]; then
mv "$YAML_OUTPUT_DIR" "$BACKUP_DIR"
echo "Backup of previous specs: $BACKUP_DIR"
fi
mv "$STAGING_DIR" "$YAML_OUTPUT_DIR"
# Ротация бэкапов: держим последние KEEP_BACKUPS (по умолчанию 5).
KEEP_BACKUPS="${KEEP_BACKUPS:-5}"
mapfile -t stale_backups < <(ls -1dt "${YAML_PARENT_DIR}/${YAML_BASE_NAME}.bak-"* 2>/dev/null | tail -n +"$((KEEP_BACKUPS + 1))")
if [[ "${#stale_backups[@]}" -gt 0 ]]; then
rm -rf -- "${stale_backups[@]}"
fi
echo "Done. YAML files are in ${YAML_OUTPUT_DIR}"
@@ -1,22 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
# Generate docs using ClickHouse template (v2, no color markup).
# Canonical docs generator for unified resources_yaml.
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="${ROOT_DIR:-$(cd "${SCRIPT_DIR}/../.." && pwd)}"
PROVIDER_DIR="${ROOT_DIR}/provider"
RESOURCES_YAML_DIR="${PROVIDER_DIR}/resources_yaml"
DOCS_DIR="${ROOT_DIR}/docs/30_registry/resources"
SERVICES_LIST_PATH="${PROFILE_DIR}/services_list.txt"
cd "$PROVIDER_DIR"
${ROOT_DIR}/TOOLS/bin/docs-generator \
-resources "$RESOURCES_YAML_DIR" \
-docs "$DOCS_DIR" \
-services "$SERVICES_LIST_PATH" \
-exclude "clickhouse"
echo "Docs generated in ${DOCS_DIR}"
@@ -164,6 +164,13 @@ if [[ -n "${MKDOCS_DOCS_DIR:-}" ]]; then
mkdir -p "${MKDOCS_DOCS_DIR}/curated" mkdir -p "${MKDOCS_DOCS_DIR}/curated"
cp -r "${ROOT_DIR}/docs/curated/"* "${MKDOCS_DOCS_DIR}/curated/" 2>/dev/null || true cp -r "${ROOT_DIR}/docs/curated/"* "${MKDOCS_DOCS_DIR}/curated/" 2>/dev/null || true
fi fi
# Картинки схемы зависимостей (docs/diagrams/*.svg|png) — чтобы их можно было
# показывать на опубликованных страницах. Исходники (.mmd, .py) не копируются.
if [[ -d "${ROOT_DIR}/docs/diagrams" ]]; then
mkdir -p "${MKDOCS_DOCS_DIR}/diagrams"
cp "${ROOT_DIR}/docs/diagrams/"*.svg "${MKDOCS_DOCS_DIR}/diagrams/" 2>/dev/null || true
cp "${ROOT_DIR}/docs/diagrams/"*.png "${MKDOCS_DOCS_DIR}/diagrams/" 2>/dev/null || true
fi
fi fi
# Per-стенд подстановка во все скопированные Markdown-файлы. # Per-стенд подстановка во все скопированные Markdown-файлы.
-44
View File
@@ -1,44 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="${ROOT_DIR:-$(cd "${SCRIPT_DIR}/../.." && pwd)}"
SCRIPT="${ROOT_DIR}/TOOLS/scripts/01_generate_yamls.sh"
TOKEN_FILE="${TOKEN_FILE:-}"
REQUEST_DELAY="${REQUEST_DELAY:-0.2}"
ATTEMPTS="${ATTEMPTS:-3}"
RUNS="${RUNS:-10}"
LOG="${LOG:-/tmp/yaml_gen_runs.log}"
export TOKEN_FILE
if [[ -z "$TOKEN_FILE" ]]; then
TOKEN_FILE=$(ls -t "${ROOT_DIR}"/*.token 2>/dev/null | head -n 1 || true)
fi
if [[ -z "$TOKEN_FILE" || ! -f "$TOKEN_FILE" ]]; then
echo "Error: token file not found in ${ROOT_DIR}" >&2
exit 2
fi
: > "$LOG"
echo "Runs: $RUNS" | tee -a "$LOG"
echo "Request delay: $REQUEST_DELAY" | tee -a "$LOG"
echo "Attempts per service: $ATTEMPTS" | tee -a "$LOG"
echo "Token file: $TOKEN_FILE" | tee -a "$LOG"
echo "" | tee -a "$LOG"
for i in $(seq 1 "$RUNS"); do
start=$(date -u +%Y-%m-%dT%H:%M:%SZ)
echo "RUN $i start $start" | tee -a "$LOG"
REQUEST_DELAY="$REQUEST_DELAY" ATTEMPTS="$ATTEMPTS" "$SCRIPT" >> "$LOG" 2>&1
rc=$?
end=$(date -u +%Y-%m-%dT%H:%M:%SZ)
echo "RUN $i end $end rc=$rc" | tee -a "$LOG"
echo "" | tee -a "$LOG"
sleep 0.5
done
echo "Log: $LOG" | tee -a "$LOG"
@@ -1,18 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="${ROOT_DIR:-$(cd "${SCRIPT_DIR}/../.." && pwd)}"
TOKEN_FILE="${TOKEN_FILE:-}"
if [[ -z "$TOKEN_FILE" ]]; then
TOKEN_FILE=$(ls -t "${ROOT_DIR}"/*.token 2>/dev/null | head -n 1 || true)
fi
if [[ -z "$TOKEN_FILE" || ! -f "$TOKEN_FILE" ]]; then
echo "Error: token file not found in ${ROOT_DIR}" >&2
exit 2
fi
export TOKEN_FILE
exec "${ROOT_DIR}/TOOLS/scripts/10_yaml_stability_run.sh"
-18
View File
@@ -1,18 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="${ROOT_DIR:-$(cd "${SCRIPT_DIR}/../.." && pwd)}"
TOKEN_FILE="${TOKEN_FILE:-}"
if [[ -z "$TOKEN_FILE" ]]; then
TOKEN_FILE=$(ls -t "${ROOT_DIR}"/*.token 2>/dev/null | head -n 1 || true)
fi
if [[ -z "$TOKEN_FILE" || ! -f "$TOKEN_FILE" ]]; then
echo "Error: token file not found in ${ROOT_DIR}" >&2
exit 2
fi
export TOKEN_FILE
exec "${ROOT_DIR}/TOOLS/scripts/01_generate_yamls.sh"
-16
View File
@@ -1,16 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="${ROOT_DIR:-$(cd "${SCRIPT_DIR}/../.." && pwd)}"
PROVIDER_DIR="${ROOT_DIR}/provider"
if [[ ! -d "${PROVIDER_DIR}/resources_yaml" ]]; then
echo "Error: resources_yaml not found: ${PROVIDER_DIR}/resources_yaml" >&2
exit 2
fi
# Start from a clean slate to avoid stale YAMLs.
rm -f "${PROVIDER_DIR}/resources_yaml"/*.yaml
exec "${ROOT_DIR}/TOOLS/scripts/12_generate_yamls_latest.sh"
+28 -10
View File
@@ -3,27 +3,45 @@ set -euo pipefail
# check_hardcoded_service_ids.sh — запрет сервис-специфичных хардкодов по числовому ID. # check_hardcoded_service_ids.sh — запрет сервис-специфичных хардкодов по числовому ID.
# #
# Ищет сравнения вида svc.ID == N / ServiceID == N / spec.ServiceID == N (N > 0) # Ищет:
# в Go-коде TOOLS/. Исключения должны жить ТОЛЬКО в именованных реестрах (данные): # 1) сравнения вида svc.ID == N / ServiceID == N / spec.ServiceID == N (N > 0)
# - TOOLS/yaml-generator/main.go (serviceSpecificModifiers) # 2) литеральные ref-service id в вызовах резолва:
# ResolveRefSvcParamValue(ctx, 19, ...) / ResolveRefSvcParamDisplayName(ctx, 22, ...)
#
# Область: TOOLS/** и provider/internal/** (кроме сгенерированного resources_gen/**).
# Исключения должны жить в именованных реестрах/константах (данные), а не как магические числа.
# - TOOLS/docs-generator/internal/writers/writers.go (serviceSpecificDocExamples) # - TOOLS/docs-generator/internal/writers/writers.go (serviceSpecificDocExamples)
# - provider/internal/resources_core/*.go (именованные svcID* константы)
# #
# Выход: 0 — хардкодов нет; 1 — найдены. # Выход: 0 — хардкодов нет; 1 — найдены.
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="${ROOT_DIR:-$(cd "${SCRIPT_DIR}/../.." && pwd)}" ROOT_DIR="${ROOT_DIR:-$(cd "${SCRIPT_DIR}/../.." && pwd)}"
matches="$(grep -rnE '\.(ServiceID|ID)[[:space:]]*==[[:space:]]*[1-9][0-9]*' "$ROOT_DIR/TOOLS" --include='*.go' || true)" SCAN_DIRS=("$ROOT_DIR/TOOLS" "$ROOT_DIR/provider/internal")
matches=""
for dir in "${SCAN_DIRS[@]}"; do
[[ -d "$dir" ]] || continue
# Сгенерированный код не сканируем — он не редактируется руками (см. ARCHITECTURE.md).
found="$(grep -rnE \
-e '\.(ServiceID|ID)[[:space:]]*==[[:space:]]*[1-9][0-9]*' \
-e '(ResolveRefSvcParamValue|ResolveRefSvcParamDisplayName)\([^,]+, *[1-9][0-9]*' \
"$dir" --include='*.go' --exclude-dir=resources_gen || true)"
if [[ -n "$found" ]]; then
matches+="$found"$'\n'
fi
done
if [[ -n "$matches" ]]; then if [[ -n "$matches" ]]; then
echo "HARDCODED SERVICE ID FOUND (service-specific logic must live in a registry):" >&2 echo "HARDCODED SERVICE ID FOUND (service-specific logic must live in a named registry):" >&2
echo "$matches" >&2 printf '%s' "$matches" >&2
echo "" >&2 echo "" >&2
echo "Вынеси исключение в один из реестров:" >&2 echo "Вынеси исключение в именованный реестр/константу:" >&2
echo " - TOOLS/yaml-generator/main.go (serviceSpecificModifiers)" >&2
echo " - TOOLS/docs-generator/internal/writers/writers.go (serviceSpecificDocExamples)" >&2 echo " - TOOLS/docs-generator/internal/writers/writers.go (serviceSpecificDocExamples)" >&2
echo "См. TOOLS/ARCHITECTURE.md, раздел «Реестр исключений»." >&2 echo " - provider/internal/resources_core/*.go (именованные svcID* константы)" >&2
echo "См. TOOLS/ARCHITECTURE.md, раздел «Exception Registry»." >&2
exit 1 exit 1
fi fi
echo "OK: no hardcoded service IDs in TOOLS/." echo "OK: no hardcoded service IDs in TOOLS/ and provider/internal/."
+12 -54
View File
@@ -30,7 +30,12 @@ type ServiceRef struct {
// Load читает конфигурацию из переменных окружения и файлов. // Load читает конфигурацию из переменных окружения и файлов.
func Load() (Config, error) { func Load() (Config, error) {
apiEndpoint := getenvDefault("NUBES_API_ENDPOINT", "https://lk-api-gateway.ngcloud.ru/api/v1/svc") // Endpoint ОБЯЗАТЕЛЕН. Легаси-дефолт на PROD gateway удален (2026-09-30):
// провайдер не должен молча ходить в прод при незаданном endpoint.
apiEndpoint := strings.TrimSpace(os.Getenv("NUBES_API_ENDPOINT"))
if apiEndpoint == "" {
return Config{}, errors.New("NUBES_API_ENDPOINT is required (no default: refusing to guess a stand endpoint)")
}
apiEndpoint = normalize.APIEndpoint(apiEndpoint) apiEndpoint = normalize.APIEndpoint(apiEndpoint)
apiToken, err := loadToken() apiToken, err := loadToken()
if err != nil { if err != nil {
@@ -51,13 +56,11 @@ func Load() (Config, error) {
services := []ServiceRef{} services := []ServiceRef{}
if singleID == 0 { if singleID == 0 {
// Без per-service ID нужен явный список. Угадывание пути удалено (2026-09-30):
// старый дефолт указывал на несуществующий provider/devops/config/services_list.txt.
listPath := strings.TrimSpace(os.Getenv("NUBES_SERVICES_FILE")) listPath := strings.TrimSpace(os.Getenv("NUBES_SERVICES_FILE"))
if listPath == "" { if listPath == "" {
repoRoot, err := FindRepoRoot() return Config{}, errors.New("NUBES_SERVICES_FILE is required when NUBES_SERVICE_ID is not set")
if err != nil {
return Config{}, err
}
listPath = filepath.Join(repoRoot, "devops", "config", "services_list.txt")
} }
list, err := ReadServicesList(listPath) list, err := ReadServicesList(listPath)
if err != nil { if err != nil {
@@ -76,14 +79,6 @@ func Load() (Config, error) {
}, nil }, nil
} }
func getenvDefault(key string, def string) string {
val := strings.TrimSpace(os.Getenv(key))
if val == "" {
return def
}
return val
}
func loadToken() (string, error) { func loadToken() (string, error) {
if tok := strings.TrimSpace(os.Getenv("NUBES_API_TOKEN")); tok != "" { if tok := strings.TrimSpace(os.Getenv("NUBES_API_TOKEN")); tok != "" {
return tok, nil return tok, nil
@@ -95,46 +90,9 @@ func loadToken() (string, error) {
} }
return strings.TrimSpace(string(b)), nil return strings.TrimSpace(string(b)), nil
} }
repoRoot, err := FindRepoRoot() // Легаси-фолбэк «найти последний *.token в корне репо» удален (2026-09-30):
if err != nil { // токенов там нет, а такой поиск мог молча подхватить чужой токен.
return "", err return "", errors.New("NUBES_API_TOKEN or TOKEN_FILE is required")
}
latest, err := findLatestToken(repoRoot)
if err != nil {
return "", err
}
if latest == "" {
return "", errors.New("NUBES_API_TOKEN or TOKEN_FILE is required")
}
b, err := os.ReadFile(latest)
if err != nil {
return "", err
}
return strings.TrimSpace(string(b)), nil
}
func findLatestToken(dir string) (string, error) {
entries, err := os.ReadDir(dir)
if err != nil {
return "", err
}
var latest string
var latestTime int64
for _, e := range entries {
if e.IsDir() || !strings.HasSuffix(e.Name(), ".token") {
continue
}
info, err := e.Info()
if err != nil {
continue
}
mt := info.ModTime().Unix()
if mt > latestTime {
latestTime = mt
latest = filepath.Join(dir, e.Name())
}
}
return latest, nil
} }
// ReadServicesList читает список сервисов из файла. // ReadServicesList читает список сервисов из файла.
+9 -5
View File
@@ -2,11 +2,15 @@
> Обновлять после **каждой** заливки. Это единственный источник правды. > Обновлять после **каждой** заливки. Это единственный источник правды.
| Стенд | Namespace | Версия | Дата заливки | | Стенд | Namespace | Версия | Дата заливки | Примечание |
|---|---|---|---| |---|---|---|---|---|
| PROD | `nubes` | `1.0.0` | 2026-09-03 | (новая нумерация) | | PROD | `nubes` | `1.0.0` | 2026-09-30 | **перезалит свежей сборкой** (перегенерация YAML + сборка из текущего кода) |
| DEV | `nubes-dev` | `2.0.22` | 2026-09-24 | (feat: третий режим destroy `keep_on_destroy` = state_only для всех instance-ресурсов (в т.ч. Эдж, у которого нет suspend) + предупреждения «заморожен/оставлен как есть» в `Delete`; реализовано универсально в генераторе) | | DEV | `nubes-dev` | `2.0.0` | 2026-09-30 | **перезалит свежей сборкой**; в реестре также остаются `2.0.21`–`2.0.24` |
| TEST | `nubes-test` | `3.0.0` | 2026-09-03 | (новая нумерация) | | TEST | `nubes-test` | `3.0.0` | 2026-09-30 | **перезалит свежей сборкой** |
> ⚠️ **Хранение в реестре (2026-09-30):** в dev-реестре (`nubes-dev/nubes`) оставлены только
> `2.0.21`–`2.0.24`; версии `2.0.0`–`2.0.20` удалены физически (бакет un-versioned).
> Подробности и проверки: `HISTORY/2026-09-30_dev_registry_prune_versions.md`.
## Как проверить ## Как проверить
@@ -0,0 +1,222 @@
# Как работает провайдер Nubes: поведение и отличия от канонического Terraform
> Страница для тех, кто уже работал с Terraform и хочет понять, чего ожидать от провайдера Nubes,
> и для DevOps, которым важно знать, что реально произойдёт в облаке при `plan`, `apply` и `destroy`.
> Здесь описан наблюдаемый контракт (что уже проверено на живых стендах) и ограничения платформы.
## 1. Суть в одном абзаце
Провайдер Nubes — это не плагин к гипервизору, а **обёртка над API личного кабинета**: каждый Terraform-ресурс
соответствует *инстансу услуги* в облаке, а `create` / `update` / `delete` транслируются в **асинхронные операции**
платформы (`create`, `modify`, `suspend`, `resume`, `delete`). Отсюда и все отличия от привычного Terraform:
- операции **долгие** — провайдер ждёт их завершения (поллинг);
- меняется **уже созданный объект**, а не создаётся заново;
- ошибки приходят **внутри тела ответа**, а не HTTP-кодом;
- ресурс ищется **по имени**, а не только по `id`;
- удаление может быть **«мягким»** — объект остаётся в облаке.
## 2. Какие ресурсы за что отвечают
| Услуга облака | Ресурс Terraform | Что делает |
|---|---|---|
| Организация в Cloud Director (19) | `nubes_vc_org_ip_allocation` | **модификатор**: меняет квоту внешних IP в организации (саму организацию создают в ЛК, Terraform её не трогает) |
| Виртуальный датацентр (21) | `nubes_vc_vdc` | создаёт vDC с ресурсами (CPU/RAM/Storage) |
| Сетевой шлюз периметра (22) | `nubes_vc_nsxt` | создаёт Edge (routed-сеть, ALB/AVI) |
| Сетевой шлюз периметра (22) | `nubes_vc_nsxt_snat` | **модификатор**: включает/переключает SNAT (`ipSpaceName`) на существующем Edge |
| Виртуальный каталог ВМ (26) | `nubes_vapp` | создаёт vApp (каталог для ВМ) на существующих vDC и Edge |
| Виртуальная машина (28) | `nubes_vc_vm_v3` | создаёт ВМ внутри vApp (CPU/RAM/диск, образ, SSH-ключ, внешний IP) |
| Kubernetes кластер Штурвал (150) | `nubes_k8s_sthutrval_cluster` | создаёт кластер (control plane + группы воркеров) |
Общее правило. Ресурс, у которого есть свой «объект в облаке», **создаёт** этот объект. А то, что платформа
меняет операцией `modify` (квота IP, включение SNAT, настройки шлюза), собрано в отдельные ресурсы-
**модификаторы**: они не создают ничего нового, а правят уже существующий объект.
## 3. Отличия от «учебного» Terraform
| Ожидание по канону | Как в Nubes | Причина |
|---|---|---|
| `create` = один вызов API | Создание инстанса — **последовательность**: `POST /instances` → `POST /instanceOperations` → параметры (`instanceOperationCfsParams`) → `run` → поллинг до `dtFinish` | Так устроен API платформы |
| `id` — произвольная строка | `id` = UUID инстанса в облаке; провайдер читает его из ответа платформы | Состояние живёт в облаке, не в Terraform |
| `update` = замена при несовместимых параметрах | `update` = операция **`modify`** над тем же инстансом; часть параметров — **create-only** (менять нельзя → ошибка на этапе plan) | Платформа не пересоздаёт объекты «из коробки» |
| есть `data sources` для поиска существующего | Поиск/ссылки — через **ref-параметры**: можно указать UUID **или имя** инстанса | Экономит data sources, но требует дисциплины в значениях |
| `import` — явная команда | Плюс к `import` есть **авто-усыновление** `adopt_existing_on_create = true`: ресурс сам находит инстанс **по имени** | В проде объекты живут неделями, и пересоздавать их нельзя |
| `plan` показывает, что ресурс уже существует | **Нет**: проверка «инстанс с таким именем уже есть / adopt» выполняется в `Create`, то есть на `apply` | Иначе ломается `destroy` и работа с tainted-ресурсами |
| `delete` удаляет | `delete` может быть `delete` / `suspend` / `state_only` — см. §5 | У платформы не всё удаляется, а часть объектов удалять нельзя, пока жив потребитель |
| ошибка приходит HTTP-кодом | Ошибка операции — **в теле**: `isSuccessful=false` + `errorLog`, при успешном HTTP-коде | API платформы отвечает 200/201 почти всегда |
| операции быстрые | Операции асинхронные и долгие (Штурвал — десятки минут) → параметр `operation_timeout`; одновременные операции на одном инстансе не поддерживаются | Наследство платформы (внутри — CFS-оркестратор) |
| план обязан совпадать с config | Тоже, но: **ref-параметры в плане остаются как написаны** (`name` не подменяется на `uuid`), а уже перед вызовом API провайдер сам разберётся, имя это или UUID | Иначе Terraform упадёт с «Provider produced invalid plan» |
| параметры сравниваются как строки | JSON-параметры сравниваются **канонично**: порядок ключей и пробелы не важны, скаляры приводятся к строке, UUID — без учёта регистра | API возвращает JSON в своём виде, иначе будет «вечный diff» |
| идентичность — по `id` | Дополнительно: инстанс ищется по `resource_name` (displayName) → **переименование = новый ресурс** | Имя задаётся при создании и не меняется |
### Что из этого следует на практике
- `terraform plan` **не может** проверить, что «такой объект уже есть»: он это покажет как `will be created`,
а разбираться будет `apply`.
- `terraform apply` на существующей инфраструктуре — это нормальный сценарий (adopt), если включён
`adopt_existing_on_create`; без флага вы получите явную ошибку-конфликт, а не дубль ресурса.
- Долгие операции требуют терпения: смотрите `operation_timeout`, не запускайте вторую операцию по тому же объекту.
## 4. Создание, чтение, изменение, удаление
**Создание.** Провайдер формирует параметры операции, отправляет её и ждёт завершения. Параметры, зависящие
от других ресурсов (например `vdc_uid`, `nsxt_uid`), передаются как ref-значения: UUID или имя.
**Чтение (refresh).** Значения `state_params` / `state_out` приходят из облака: так в state попадают
адреса (`kubernetesApiAddress`, `ingressAddress`), имена, текущие параметры.
**Изменение.** Если параметры поменялись, вызывается `modify` (для модификаторов — обратный `modify` при удалении).
Параметры, помеченные как create-only, изменить нельзя — провайдер скажет об этом до обращения к API.
**Удаление.** См. следующий раздел — это самое неочевидное место.
## 5. Удаление: `delete`, `suspend` и «оставить как есть»
У платформы три разных исхода, и провайдер умеет все три. Управляется флагами:
| Флаг | Где применим | Поведение при `destroy` | Предупреждение в выводе |
|---|---|---|---|
| `suspend_on_destroy = true` | кластер Штурвала, vDC, vApp, ВМ (по умолчанию `true`) | объект **приостанавливается**, не удаляется | «Ресурс заморожен, а не удалён» |
| `keep_on_destroy = true` | Edge, SNAT, квота IP (по умолчанию `false`) | объект **не трогается в облаке**, только убирается из state | «Оставлен как есть» (для SNAT — «SNAT не выключался», для квоты IP — «Аллокация IP не снималась») |
| оба `false` | любой | обычное удаление | — |
Если выставлены оба флага, победит `keep_on_destroy` (объект просто не тронут). По умолчанию провайдер
удаляет по-настоящему (`keep_on_destroy = false`), а «заморозку» или «оставить как есть» включают явно
в конфигурации стенда.
### Почему Edge остаётся `running`
Edge **физически не умеет `suspend`**: в списке доступных операций шлюза есть только `delete`, `modify`,
`reconcile`. Усыпить его платформа не даёт, поэтому единственный корректный способ «не удалять шлюз» —
не трогать его: `keep_on_destroy = true`. Побочный эффект — шлюз продолжает работать и тарифицироваться,
и через него продолжают публиковаться внешние адреса (API кластера и ingress). Именно поэтому удаление Edge
при живом кластере Штурвала (или других зависимых объектах: vApp, VM) считается недопустимым.
### Почему SNAT остаётся включённым, а квота IP не обнуляется
- SNAT-модификатор с `keep_on_destroy = true` **не отправляет** обратный `modify` с `ipSpaceName = "no-needed"`,
то есть SNAT для виртуальных машин остаётся в прежнем состоянии.
- Квота внешних IP: уменьшать `count` **ниже фактически занятых адресов платформа не разрешает**
(ошибка вида «Кол-во занятых Ip … Невозможно выставить параметр count ниже этого параметра»).
Адреса держит кластер Штурвала (API + ingress), и `suspend` кластера их **не освобождает**;
если у ВМ есть внешний адрес, он тоже держится инстансом ВМ — даже остановленной.
Поэтому при «заморозке» квота не изменяется вовсе, а реальное освобождение адресов возможно только после
удаления кластера — отдельным шагом.
### Почему кластер и vDC уходят в `suspend`
Для них `suspend` поддерживается платформой, и это самый близкий к «выключить» вариант: ВМ останавливаются,
объекты не удаляются, адреса и конфигурация сохраняются. Обратный ход делает `apply`:
| Что | При `destroy` (заморозка) | При следующем `apply` |
|---|---|---|
| Кластер Штурвала | `suspend` | adopt по имени + `resume` |
| vDC | `suspend` | adopt по имени + `resume` |
| vApp | `suspend` | adopt по имени + `resume` |
| ВМ | `suspend` | adopt по имени + `resume` |
| Edge | не трогается (`running`) | adopt (инстанс уже работает) |
| SNAT | не трогается (включён) | повторный `modify` теми же значениями (фактически no-op) |
| Квота IP | не трогается | `modify` с тем же `count` (no-op) |
Итого цикл «`destroy` → `apply`» на стенде выглядит как «усыпить → разбудить», а не «снести → поднять заново».
Полностью удалить такой стенд можно только явным отказом от заморозки (`keep_on_destroy = false` /
`suspend_on_destroy = false`) и в правильном порядке (см. §6).
## 6. Конкретный пайплайн стенда: vDC → Edge → внешние IP → SNAT → vApp → ВМ → Штурвал
Порядок создания и зависимости:
1. Организация — **вручную в ЛК** (Terraform её не создаёт).
2. `nubes_vc_vdc` — виртуальный датацентр.
3. `nubes_vc_nsxt` — Edge. Обязательно включать балансировщик (параметр `need_enable_avi = true`)
и задать не меньше 3 виртуальных сервисов (`virtual_services_count >= 3`) — это нужно кластеру Штурвала.
4. `nubes_vc_org_ip_allocation` — внешние адреса в организации (минимум 3 по инструкции услуги Штурвала,
плюс ещё один, если у ВМ будет внешний адрес).
5. `nubes_vc_nsxt_snat` — SNAT на Edge (по `depends_on` после аллокации адресов).
6. `nubes_vapp` — vApp: привязка к vDC (`vdc_uid`) и Edge (`nsxt_uid`); создаётся после SNAT,
чтобы сеть была уже готова.
7. `nubes_vc_vm_v3` — ВМ внутри vApp (`vapp_uid`): образ, CPU/RAM/диск, учётка и SSH-ключ, порты FW.
8. `nubes_k8s_sthutrval_cluster` — кластер Штурвала (по `depends_on` после SNAT: нодам нужен выход в интернет).
При удалении Terraform идёт в обратном порядке. Ограничения платформы, которые встречаются на этом пути:
- **1 кластер Штурвала = 1 vDC** (действующее ограничение услуги).
- vDC удаляется только **через 14 дней после `suspend`**; при живых Edge/vApp/VM/кластере — через поддержку.
- У vApp удаление требует **предварительного `suspend`**, полное удаление — тоже через 14 дней.
- У vApp и ВМ есть `suspend` и `resume`; у Edge операции `suspend` нет вообще.
- **ВМ не включится, если в vDC не осталось vCPU**: Штурвал при настройках по умолчанию занимает всё
(2 ноды × 4 vCPU при `vdc_cpu_allocated = 8`). Платформа отвечает общей ошибкой
`[400:VALIDATION] Unable to perform this action` на этапе power-on, инстанс остаётся в `not created`.
- Создание ВМ идёт дольше остальных ресурсов (customization + power-on) — задавайте `operation_timeout = "15m"`.
- Edge не удаляется при живых зависимых объектах (по инструкции услуги).
- Квота IP не опускается ниже занятых адресов (см. §5).
Как проверить, что конфигурация корректна: `terraform plan` после `apply` должен говорить
`No changes`. Если появляется стабильный diff — сверяйте его с `terraform state show` и с тем,
что реально показывается в личном кабинете.
## 7. Регистр UUID и канонизация значений
UUID в облаке не имеет «правильного» регистра: один и тот же идентификатор может прийти как `2c37fed1-…`
и как `2C37FED1-…`. Поэтому провайдер сравнивает UUID **без учёта регистра** — в том числе внутри JSON-параметров
(например, `startupConfiguration` кластера Штурвала). Подробный разбор проблемы и всех мест, где она может
проявиться, — в `60_strategy/terraform_case_sensitivity_fix.md` (внутренний документ).
Для JSON-параметров сравнение также игнорирует порядок ключей и пробелы, а скаляры приводит к строковому виду —
это нужно, чтобы `plan` не показывал «изменение» там, где API вернул то же значение в другом формате.
## 8. Чек-лист DevOps
- Пин версии провайдера привязан к стенду: **prod `1.*`, dev `2.*`, test `3.*`**; после смены версии —
`terraform init -upgrade`.
- Перед `apply` и после — смотрите `plan`; ожидаемый финальный результат — `No changes`.
- **Читайте предупреждения `destroy`**: «заморожен», «оставлен как есть», «SNAT не выключался» — это не косметика,
а отчёт о том, что объекты продолжают жить и тарифицироваться.
- Не удаляйте объекты стенда руками в ЛК между `destroy` и `apply`: авто-усыновление ищет их по имени, и на удалённом
объекте упрётся в состояние `not created`.
- Долгие операции: задавайте `operation_timeout` (для Штурвала — `60m`), не запускайте параллельные операции
по одному объекту.
- Проверяя состояние через API ЛК, помните про обязательные заголовки (браузерный `User-Agent` и `Referer`) —
иначе отдаёт `403`.
## 9. FAQ
**Почему `plan` пишет `will be created`, если объект уже есть?**
Потому что проверка существования и усыновление выполняются на `apply` (в `Create`). В state ресурса нет → план
честно планирует создание. Решение — `adopt_existing_on_create = true`.
**Почему `destroy` сказал `5 destroyed`, а в облаке всё живо?**
Сработала «заморозка»: кластер и vDC приостановлены, Edge, SNAT и квота IP не изменялись. Terraform «удалил»
ресурсы только из своего состояния. Смотрите предупреждения в выводе.
**Почему IP не освободились?**
Квота не может стать меньше числа занятых адресов, а их держит кластер. Пока кластер существует (даже в `suspend`),
платформа не даст уменьшить `count`.
**Почему Edge нельзя приостановить?**
У платформы для шлюза нет операции `suspend` — только `delete`, `modify`, `reconcile`.
**Почему после `apply` кластер ожил сам?**
Сработало усыновление: ресурс нашёл инстанс по имени (`adopt_existing_on_create`), увидел статус `suspended`
и выполнил `resume`.
**State пуст, а объекты в облаке есть. Что делать?**
Это ожидаемый результат «заморозки». `terraform apply` в том же каталоге усыновит объекты обратно.
**Кто-то удалил объект в ЛК. Что будет?**
Усыновление не найдёт его и провайдер завершится ошибкой с указанием статуса (`not created`) — нужно либо создать
объект заново, либо разбираться с конфигурацией.
**А можно всё-таки удалить стенд полностью?**
Да, но осознанно: выставить `keep_on_destroy = false` и `suspend_on_destroy = false`, затем удалять в порядке
кластер → квота IP → SNAT → Edge → vDC, при необходимости — через поддержку (для vDC действует правило 14 дней).
## 10. Куда смотреть дальше
- [Как развернуть цепочку: vDC → Edge → внешние IP → SNAT → vApp → ВМ → Штурвал](https://tf-docs.nodejsk8s.dev.nubes.ru/nubes-dev/curated/pipeline/vdc_edge_ip_snat/) —
пошаговая инструкция по всей цепочке вашего стенда.
- `curated/modifiers/org_ip_and_snat.md` — ресурсы-модификаторы.
- `30_registry/guides/terraform-structure.md` — структура манифестов.
- Внутренние документы (не публикуются): `60_strategy/provider_philosophy.md`,
`60_strategy/modifier_resources_ideology_and_specification.md`,
`60_strategy/adopt_ref_validation.md`, `60_strategy/terraform_case_sensitivity_fix.md`.
@@ -0,0 +1,116 @@
# Зависимости облачных сервисов каталога DEV
Промежуточная инвентаризация для будущего общего графа. Источник — YAML-контракты
в `generated/dev/resources_yaml`.
Вершина — сервис каталога. Направление будущей стрелки: `потребитель -> зависимость`.
В таблицу зависимостей включены только связи, явно подтверждённые полем
`ref_svc_id` или тем же контрактом сервиса.
## Все сервисы каталога
| ID | Сервис | YAML |
|---:|---|---|
| 1 | Болванка | `1_dummy.yaml` |
| 2 | Темплейт k8s | `2_template.yaml` |
| 12 | S3 Object Storage | `12_s3.yaml` |
| 13 | S3 бакет | `13_s3bucket.yaml` |
| 19 | Организация в Cloud Director | `19_vc_org.yaml` |
| 21 | Виртуальный датацентр (vDC) | `21_vc_vdc.yaml` |
| 22 | Сетевой шлюз периметра (Edge) | `22_vc_nsxt.yaml` |
| 25 | Публичные IP адреса | `25_vcexternalip.yaml` |
| 26 | Виртуальный каталог ВМ (vApp) | `26_vapp.yaml` |
| 28 | Виртуальная машина | `28_vc_vm_v3.yaml` |
| 29 | Группа датацентров | `29_vc_vdc_group.yaml` |
| 50 | Nextcloud | `50_nextcloud.yaml` |
| 81 | Apache Superset | `81_superset.yaml` |
| 82 | Container Registry | `82_harbor.yaml` |
| 86 | Velero | `86_k8s_velero.yaml` |
| 87 | Valkey | `87_k8svalkey.yaml` |
| 88 | k8sZitiController | `88_k8s_ziti_controller.yaml` |
| 89 | Web-сервер с фреймворком Flask | `89_flask.yaml` |
| 90 | PostgreSQL | `90_postgres.yaml` |
| 91 | Redis | `91_redis.yaml` |
| 92 | MongoDB | `92_mongodb.yaml` |
| 93 | RabbitMQ | `93_rabbitmq.yaml` |
| 94 | Lucee | `94_lucee.yaml` |
| 95 | NodeJS | `95_nodejs.yaml` |
| 96 | pgAdmin | `96_pgadmin.yaml` |
| 97 | NodeRed | `97_nodered.yaml` |
| 98 | Простой HTTP контейнер | `98_http.yaml` |
| 99 | Gitea | `99_gitea.yaml` |
| 109 | Управление DNS | `109_zones_v2.yaml` |
| 111 | DNS запись | `111_dnsrecord.yaml` |
| 115 | Mariadb | `115_mariadb.yaml` |
| 116 | ApacheKafka | `116_kafka.yaml` |
| 119 | Akhq | `119_akhq.yaml` |
| 120 | ClickHouse | `120_clickhouse.yaml` |
| 148 | Менеджмент Kubernetes кластер Штурвал | `148_vc_mgmt_sthutrval_cluster.yaml` |
| 149 | VALO Cloud | `149_valo_tenant.yaml` |
| 150 | Kubernetes кластер Штурвал | `150_k8s_sthutrval_cluster.yaml` |
| 151 | Vault | `151_k8s_openbao.yaml` |
| 153 | Nifi | `153_nifi.yaml` |
| 163 | ML Улей | `163_llm_ai.yaml` |
## Подтверждённые зависимости
| ID | Потребитель | Зависимость | Поле связи | Смысл связи |
|---|---|---|---|---|
| D-01 | DNS запись (111) | Управление DNS (109) | `zoneUid` | запись создаётся в DNS-зоне |
| D-02 | S3 бакет (13) | S3 Object Storage (12) | `s3UserUid` | бакет создаётся для корневой S3-услуги |
| D-03 | Виртуальный датацентр (21) | Организация в Cloud Director (19) | `organizationUid` | vDC принадлежит организации |
| D-04 | Сетевой шлюз периметра (22) | Виртуальный датацентр (21) | `vdcUid` | Edge привязывается к vDC |
| D-05 | Сетевой шлюз периметра (22) | Группа датацентров (29) | `vdcGroupUid` | альтернативная привязка Edge к группе vDC (vdcType=vdcGroup) |
| D-06 | Виртуальный каталог ВМ (26) | Сетевой шлюз периметра (22) | `nsxtUid` | vApp использует Edge для сети |
| D-07 | Виртуальный каталог ВМ (26) | Виртуальный датацентр (21) | `vdcUid` | vApp размещается в vDC |
| D-08 | Виртуальная машина (28) | Виртуальный каталог ВМ (26) | `vappUid` | VM размещается в vApp |
| D-09 | Группа датацентров (29) | Виртуальный датацентр (21) | `vdcUid` | группа объединяет vDC (create и add_vdc) |
| D-10 | Akhq (119) | ApacheKafka (116) | `kafkaUid` | UI подключается к Kafka-кластеру |
| D-11 | Container Registry (82) | S3 Object Storage (12) | `s3Uid` | registry использует S3 для хранения |
| D-12 | Gitea (99) | PostgreSQL (90) | `psqlUid` | Gitea использует PostgreSQL |
## Переиспользование существующих инстансов
Зависимости (кроме Организации) являются ссылками на **существующие инстансы** через UUID:
- Новую ВМ (28) можно создать в уже существующем vApp, vDC, Edge — ссылкой `vappUid`/`vdcUid`/`nsxtUid`.
- Edge (22) можно переиспользовать, если он привязан к нужному vDC или groupvDC.
- Для Штурвала (150) переиспользуемый Edge должен иметь включённый ALB и `virtualServicesCount >= 3`.
- Внешние IP резервируются только после: Организация + vDC + Каталог (vApp) + Edge (по `25_vcexternalip.yaml`, `service_man`).
## Где выделяются ресурсы (CPU / RAM / Storage / IP)
| Элемент | Где задаётся | Параметр(ы) |
|---|---|---|
| Пул CPU/RAM/Storage организации | vDC (21) create/modify | `cpuAllocated`, `memAllocated`, `storageConfig` (+ разовый `cpuGuaranteed` 0/50/80) |
| Запрос ресурсов нод Штурвала | Кластер Штурвал (150) | `controlPlaneConfiguration.sizingPolicy/sizingDisk/count`, `workerConfiguration…` (политики из vDC) |
| Запрос ресурсов ВМ | Виртуальная машина (28) | `vmCpu`, `vmRam`, `vmDisk` (доп. диск) |
| Внешние IP | vcOrg (19) `modify` (`vIPConfigure`) или `vcexternalip` (25) | `ipSpaceName`, квота адресов |
| SNAT-правило | Edge (22) `modify` | `ipSpaceName` |
| DNAT/SNAT-правила адреса | Публичные IP (25) | `dnatCreate`/`snatCreate`, `internalAddrAccess`, `internalPortAccess` |
## Чек-лист создания Штурвала (150) — по `service_man`
1. Организация (19) — create.
2. vDC (21) — create, выделить ресурсы под все ноды.
3. Edge (22) — create, включить ALB и `virtualServicesCount >= 3`.
4. Внешние адреса в организации — vcOrg `modify`, суммарно 3 адреса.
5. SNAT — Edge `modify`, выбрать `ipSpaceName`.
6. Кластер Штурвал (150) — create (требует `vdcUid` и `nsxtUid`).
## Чек-лист создания ВМ (28) — по create-параметрам
1. vDC (21) — существующий (`vdcUid`).
2. Edge (22) — существующий (`nsxtUid`).
3. vApp (26) — существующий (`vappUid`), привязан к vDC и Edge.
4. ВМ (28) — create; обязательные: `vappUid`, `vmName`, `vmCpu`, `vmRam`, `accessPortList`, `imageVm`, `userLogin`, `userPublicKey`, `ipSpaceName`.
## Правила для будущего графа
1. Рисовать стрелку только по подтверждённой связи сервиса каталога.
2. `ref_svc_id` считать прямой зависимостью; `descr` использовать для подписи
стрелки и проверки смысла.
3. Не считать общей платформой, DNS-полем или внешним IP автоматическую связь
между сервисами без ссылки на конкретный сервисный ID.
4. Поля без `ref_svc_id` вынести в отдельный список кандидатов на проверку, но не
смешивать с подтверждёнными рёбрами.

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