Баг (воспроизведён на TEST 2026-10-01): при усыновлении уже существующего
пользователя БД Create выходит по раннему return (ветка 'Подресурс уже существует'
-> State.Set -> return), оставляя Computed-атрибут password в состоянии unknown.
Terraform отказывался: 'Provider returned invalid result object after apply: the
provider still indicated an unknown value for nubes_postgres_user.crud_user_0.password'.
Исправление:
- новая функция resources_core.ResolveUserPasswordFromVault(ctx, client, instanceUID,
username, current): читает пароль из Vault родителя, иначе возвращает current,
иначе типизированный null (null для Terraform — конкретное, known значение);
- в шаблоне подресурса password инициализируется null сразу после получения
instanceUID, а перед КАЖДЫМ resp.State.Set в Create проставляется конкретным
значением — включая все ветки усыновления;
- регрессионный тест усилен: считает сохранения состояния и вызовы заполнения в
Create и падает, если хоть в одной ветке password останется unknown.
Проверено: generated/test/go/90_postgres_user_resource.go — 4 State.Set и 4
вызова заполнения; go test ./... ok; go build — чисто.
Проблема: у части сервисов пароль пользователя генерирует платформа и кладёт его в
секрет Vault РОДИТЕЛЬСКОГО инстанса. vault_secrets родителя — Computed и обновляется
только при его Read, поэтому внутри одного apply после create_user пароль недоступен
(Invalid index). Из-за этого в pg/outputs.tf приходилось читать vault_secrets кластера,
а стенду требовались два apply.
Решение: подресурс-пользователь отдаёт пароль СВОИМ выходом сразу после create_user.
- types.go: GenSubresource.VaultUserPassword (признак из данных спека);
- loader.go: признак = подресурс user + у сервиса есть vault-выходы + create_user
принимает username и НЕ принимает password; имя сервиса нигде не проверяется;
- templates/subresource.go: поле модели + Computed/Sensitive атрибут password,
чтение Vault родителя в Create (GetInstanceStateDetails + GetInstanceVaultSecrets)
и перенос уже полученного пароля в Update (чтобы Computed-атрибут не стал unknown);
- resources_core/subresource_user_password.go: ExtractUserPassword — разбор
{"<username>":{"password":"..."}} с безопасным возвратом пустой строки;
- writers: регрессионный тест «фича включена/выключена».
Проверено генерацией и сборкой test-стенда: выход получили 4 подресурса
(postgres_user, kafka_user, clickhouse_user, mongodb_user); mariadb_user НЕ затронут
(там пароль входной); k8s_*_user и vc_org_user не затронуты (пользователь
адресуется не через username). go test ./... — ok, go build — чисто.
Записал в ОБЩИЙ файл архитектуры (TOOLS/ARCHITECTURE.md, PRIMARY SOURCE OF TRUTH):
- раздел «Subresource-born secrets» в Subresource Resources — проблема, принцип,
правило «service-specific DATA, never logic», факт postgres vs mariadb;
- пункт 3 в Exception Registry — как объявлять признак в YAML-спеке и прокидывать
через types.go + loader.go, без svc.Name == "..." в шаблоне.
В provider_philosophy.md оставлена короткая ссылка на ARCHITECTURE.md (источник один).
Проверяет три части фичи в сгенерированном коде:
1) поле модели KeepOnDestroy с тэгом tfsdk:keep_on_destroy;
2) атрибут схемы Optional + Default=false (поведение по умолчанию не меняется);
3) в Delete проверка флага идёт РАНЬШЕ вызова операции удаления — иначе destroy
всё равно удалял бы объект в облаке.
Вывод генератора перед сравнением нормализуется по пробелам: gofmt выравнивает
поля структур и ключи map, из-за чего поиск подстроки «как в шаблоне» не работает.
Запуск: cd TOOLS/resource-generator && go test ./internal/writers/... — ok.
- у подресурсов появился раздел «Поведение при destroy» с таблицей флагов
(keep_on_destroy / skip_missing_on_delete / adopt_existing_on_create) и предупреждением
про расхождение state и облака;
- в блоке destroy у инстансовых ресурсов добавлен keep_on_destroy (раньше не документировался
вообще ни на одной странице);
- в types добавлено поле Lifecycle.KeepOnDestroyDefault.
Проверено генерацией: keep_on_destroy упоминается на 49 страницах test-стенда,
включая postgres_user.md (подресурс) и postgres_params_create.md (инстанс).
Подресурсы (nubes_postgres_user/database и остальные 20) удалялись всегда, даже когда
родительский инстанс при destroy только приостанавливается. Из-за этого пользователь БД
удалялся, а следующий apply создавал его заново с НОВЫМ паролем.
Добавлен атрибут keep_on_destroy (как у инстансовых ресурсов, Default=false):
- поле KeepOnDestroy в модели;
- атрибут схемы (Optional+Computed, Default=false);
- ранний выход в Delete с предупреждением (режим state_only).
Проверено: 22 подресурса получили атрибут; сборка провайдера с перегенерённым кодом — BUILD_OK.
Дефолт false → поведение существующих конфигураций не меняется.
Инцидент 2026-09-30 показал: невалидное имя атрибута (s3-inst) проходило генерацию,
сборку и заливку, а ломалось только у пользователя (Terraform отвергает схему целиком).
- TOOLS/scripts/check_schema_names.sh: проверяет все tfsdk:"..." в сгенерированном Go
на [a-z0-9_]; exit 1 при нарушении, с подсказкой про helpers.ToSnake.
- 03_build_and_upload_provider.sh: вызов стража после 02 (до сборки и заливки).
- ARCHITECTURE.md: страж добавлен в список enforced-скриптов.
- HISTORY: пункт 'не закрыто' заменён на 'закрыто' с проверками.
Проверено: dev/test/prod -> OK; искусственный пример (tfsdk:"s3-inst") -> exit 1.
Инцидент: в 1_dummy (API dev) появились коды s3-inst / s3-ref-root. ToSnake не убирал
дефисы -> в схему уходило tfsdk:"s3-inst" -> Terraform отвергает такие имена и НЕ
загружает схему провайдера целиком (plan/apply падали).
- helpers.go: ToSnake завершается sanitizeAttrName ([a-z0-9_] допустимы, остальное -> _).
Код для API не меняется: json:"s3-inst" в генерате сохранён.
- Проверено: в generated/dev/go нет tfsdk-имён с недопустимыми символами;
go build OK; go test ./internal/... -short PASS.
- DEV_STAND/FPipeGmail: провайдер 2.0.23 -> 2.0.1; после сброса lock/кэша
terraform validate -> Success.
- HISTORY: описан инцидент, причина (данные API изменились после утра), фикс и
особенность: перезапись артефакта под тем же номером требует сброса lock
(init -upgrade хеш не пересчитывает).
dev 2.0.1 перезалит (sha256 linux d25a71a31dbc9ab16e494b3d1f68b38b2214bd530045c5a3003f515bc725e407).
Цикл 03 (01+02 -> сборка linux/windows/darwin -> SHA256SUMS -> GPG -> S3) по dev.
Версия 2.0.1 (ранее удалена при чистке, создана заново).
Проверено:
- sha256 локальной сборки == SHA256SUMS (linux/amd64, 13 172 824 B);
- GET /v1/providers/nubes-dev/nubes/versions -> содержит 2.0.1;
- download linux/amd64 2.0.1 -> HTTP 200;
- в S3 5 объектов (3 zip + SHA256SUMS + .sig), 37.92 MiB.
test и prod не перезаливались (правки ядра туда не включены).
Документация: VERSIONS.md, HISTORY/2026-09-30_dev_release_2_0_1.md.
Код-ревью (раунд 5), п.1/6: pre-check брал схему из GET /instanceOperations/default/{opId},
а payload строился по живой схеме ?fields=cfsParams — два источника. default может
расходиться с живой => риск ложного пропуска modify.
Решение: pre-check вообще не запрашивает схему.
- modifierDesiredEqualsLive(ctx, uid, desired): сравнение по live-кодам
(live[lower(code)]), единственный источник — state.params.
- Значения: похожи на JSON ({/[) — смысловое сравнение; иначе скалярное с
нормализацией (null/"" -> ""; true/false без учёта регистра) — закрывает и
регистр bool.
- Fail-safe сохранён: пусто/нет кода/ошибка live => modify выполняется.
- Удалены: fetchOperationSchemaByID, modifierValuesEqual, lookupLiveParam больше
не участвует в pre-check (остаётся для досылки).
- Тесты переписаны (RawValuesEqual_Scalars/JSON, DesiredEqualsLive: 4 кейса).
Документация: TOOLS/ARCHITECTURE.md -> новый раздел «Modifier Idempotency»
(5 правил контракта); HISTORY — журнал раунда 5.
Проверено: go build ./... OK; go test ./internal/... -short PASS.
- 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).
Проблема: 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.
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.
- 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' -> один реестр + ручные ресурсы.
Полный цикл 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
Сверка 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) расхождений не имели.
Удалено (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)
Все ссылки в документации указывали на профильные списки либо будут
исправлены отдельным коммитом.
- TOOLS/README.md: раздел «Канонический пайплайн (порядок шагов)» и описание
безопасной генерации (staging → атомарная замена, бэкапы, маркер .stand);
- TOOLS/ARCHITECTURE.md: ссылки devops/… → TOOLS/config/<stand>/…;
- HISTORY/2026-09-30_yaml_pipeline_hardening.md: полная история изменений
(что было не так, что сделано, прогон по стендам, коммиты, проверки).
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.