# Adopt/Resume: валидация ref-параметров и защита от внешних изменений > Дата: 2026-03-29 > Статус: **реализовано в v5.0.50** > History: `docs/50_history/22_adopt_ref_validation_duplicate_detection_5_0_50.md` > Триггер: баг `inconsistent result after apply` при adopt VM с протухшим vapp_uid --- ## 1. ОПИСАНИЕ БАГА ### Цепочка событий 1. `terraform destroy` → vApp suspend, VM suspend. Terraform state обнулён. 2. Юзер удаляет vApp из ЛК (или vApp удалён 14-дневным auto-delete). UUID: `fba91c08`. 3. `terraform apply` → - `nubes_vapp.vapp` Create → новый инстанс, UUID: `f037ddea` (running). - `nubes_vc_vm_v3.vm` Create → `adopt_existing_on_create=true` → `FindInstanceByDisplayName` находит suspended VM → resume. 4. Resumed VM хранит в `state.params.vappUid` = `fba91c08` (старый deleted vApp). 5. `RefreshResourceState` читает `vappUid` из API → `fba91c08`. 6. Terraform план ожидал `f037ddea` → получил `fba91c08` → **"inconsistent result after apply"**. ### Корневая причина При adopt/resume **не проверяются ref-параметры**. Провайдер молча adopt-ит инстанс с протухшими ссылками на deleted/suspended зависимости. ### Доказательства из API - `f037ddea` (vApp) — `explainedStatus: "running"`, создан 29.03, serviceId=26 - `fba91c08` (vApp) — `explainedStatus: "deleted"`, `isDeleted: true`, создан 27.03, serviceId=26 - Оба с `displayName: "vm-sless-vapp"` — дубликат по имени (один deleted, один running) --- ## 2. ПОЛНАЯ МАТРИЦА ПРОБЛЕМ ### A. Ref-параметры протухли | # | Ситуация | Текущее поведение | Целевое поведение | |---|----------|-------------------|-------------------| | A1 | VM.vappUid → deleted vApp | `inconsistent result after apply` | **Error**: "VM (resource_name='vm-sless-1') ссылается на vApp (fba91c08) со статусом 'deleted'. vApp был удалён из облака. Варианты: (1) удалите suspended VM из облака и пересоздайте всё; (2) восстановите vApp через ЛК." | | A2 | VM.vappUid → suspended vApp | Непредсказуемо | **Warning**: "VM ссылается на vApp (xxx) в статусе 'suspended'. Убедитесь что зависимые ресурсы активны перед apply." | | A3 | UUID в плане ≠ UUID в API (старый deleted, новый создан) | `inconsistent result` | **Error**: "VM хранит ссылку vapp_uid=fba91c08 (deleted), но конфигурация указывает f037ddea (running). Adopt невозможен — ref-параметры не совпадают. Удалите suspended VM из облака." | | A4 | Ref-инстанс 404 (не найден в API) | Паника или мусор в стейте | **Error**: "Инстанс fba91c08, на который ссылается параметр vapp_uid, не найден в API (404). Проверьте облако." | ### B. Множественные инстансы с одним display_name | # | Ситуация | Текущее поведение | Целевое поведение | |---|----------|-------------------|-------------------| | B1 | 2 инстанса: один deleted, один running | Пропускает deleted → OK | OK + **Warning**: "Обнаружено 2 инстанса с именем 'vm-sless-vapp' (serviceId=26). Используется running (f037ddea). Deleted: fba91c08." | | B2 | 2 инстанса оба suspended | Adopt первый попавшийся | **Error**: "Найдено 2 suspended инстанса с именем 'vm-sless-1' (serviceId=28). Невозможно определить какой adopt-ить. Удалите лишний через ЛК." | | B3 | 2 инстанса оба running | Adopt первый попавшийся | **Error**: "Найдено 2 running инстанса с именем 'xxx'. Укажите конкретный UUID через `terraform import`." | ### C. Порядок resume зависимых ресурсов | # | Ситуация | Текущее поведение | Целевое поведение | |---|----------|-------------------|-------------------| | C1 | VM resume раньше vApp resume | VM может не стартовать | **Warning**: "vApp (f037ddea) в статусе 'suspended'. VM может не запуститься. Рекомендуется: depends_on или сначала resume vApp." | | C2 | vApp resume, но Edge deleted | vApp стартует без сети | **Warning**: "Edge (0fe88e2a), на который ссылается vApp, в статусе 'deleted'. Сетевые функции могут быть недоступны." | ### D. Внешние изменения (ЛК / другой оператор) | # | Ситуация | Текущее поведение | Целевое поведение | |---|----------|-------------------|-------------------| | D1 | Юзер изменил CPU/RAM в ЛК | Plan покажет diff → modify обратно | **Warning при Read**: "Обнаружено расхождение: vm_cpu в облаке=4, в конфигурации=2. Возможно параметры изменены вручную через ЛК." | | D2 | Юзер переименовал ресурс в ЛК | Read по ID работает | **Warning**: "display_name в облаке 'new-name' не совпадает с конфигурацией 'vm-sless-1'." | | D3 | Юзер suspend из ЛК | Read видит suspended | **Warning**: "Инстанс (xxx) в статусе 'suspended' но в Terraform state не удалён. Возможно suspend выполнен вручную." | | D4 | Операция in_progress при plan/apply | Конфликт операций | **Error**: "Инстанс (xxx): операция в процессе (operation_in_progress=true). Дождитесь завершения." | ### E. State corruption / drift | # | Ситуация | Текущее поведение | Целевое поведение | |---|----------|-------------------|-------------------| | E1 | Terraform state → 404 инстанс | Read → ошибка | `RemoveResource` + **Warning**: "Инстанс (xxx) не найден в облаке (404). Удалён из Terraform state." | | E2 | ID в стейте есть, `isDeleted=true` | Read видит deleted | `RemoveResource` + **Warning**: "Инстанс (xxx) в статусе 'deleted'. Удалён из Terraform state." | --- ## 3. ПЛАН ПРАВОК ### Уровень 1 — критические (ломают apply) #### 1.1 Валидация ref-параметров при adopt/resume - **Файл**: `universal_rebuild/internal/resources_core/crud.go` - **Где**: новая функция `ValidateRefParamsOnAdopt`, вызов из `adoptExistingInstanceOnCreate` после resume, перед return - **Логика**: 1. Получить `state.params` resumed-инстанса (уже есть через GetInstanceState) 2. Для каждого param из `params map[int]string`, у которого значение UUID-like: - Запросить `/instances/{uuid}` → проверить `explainedStatus` - Если deleted/404 → hard error с описанием - Если suspended → warning 3. Сравнить плановые ref-значения с фактическими из API - Если не совпадают → hard error: "план ожидает X, API вернул Y (статус: Z)" - **Затрагивает**: все сгенерированные ресурсы с ref-параметрами (VM, и любые будущие) - **НЕ затрагивает**: сгенерированный код ресурсов (они вызывают crud.go) #### 1.2 Проверка operation_in_progress перед любой операцией - **Файл**: `universal_rebuild/internal/resources_core/crud.go` - **Где**: начало `CreateResourceWithTimeout`, перед `FindInstanceByDisplayName` - **Логика**: если existing != nil && (OperationIsInProgress || OperationIsPending) → hard error - **Также**: в `RefreshResourceState` — если operation_in_progress → warning #### 1.3 Обработка множественных инстансов - **Файл**: `universal_rebuild/internal/core/instance_lookup.go` - **Где**: `FindInstanceByDisplayName` — собирать ВСЕ совпадения, не только первое - **Логика**: - Если 1 non-deleted → вернуть его - Если >1 non-deleted → hard error "найдено N инстансов с именем X" - Если 0 non-deleted → вернуть nil (как сейчас) - Если есть deleted дубликаты → warning с их UUID ### Уровень 2 — защита от ЛК-вмешательства #### 2.1 Warning при drift параметров в Read - **Файл**: `universal_rebuild/internal/resources_core/state_refresh.go` - **Где**: в `RefreshResourceState`, при обработке InputFields - **Логика**: если значение из API != значение из текущего state → добавить Warning diagnostic - **Важно**: только warning, не error — Terraform сам обработает diff #### 2.2 Warning при suspended зависимостях - **Файл**: `universal_rebuild/internal/resources_core/crud.go` - **Где**: после resume в `adoptExistingInstanceOnCreate` - **Логика**: для каждого ref-param UUID → проверить статус → если suspended → warning #### 2.3 Обработка 404/deleted в Read - **Файл**: `universal_rebuild/internal/resources_core/state_refresh.go` - **Где**: в начале `RefreshResourceState` или в `ShouldRemoveFromState` - **Логика**: если инстанс 404 или isDeleted → RemoveResource + warning - **Примечание**: частично уже есть через `ShouldRemoveFromState`, проверить полноту ### Уровень 3 — качество диагностик #### 3.1 Единый формат diagnostics - **Файл**: новый файл `universal_rebuild/internal/resources_core/diagnostics.go` - **Содержание**: хелпер-функции для формирования стандартизированных многострочных сообщений - **Формат каждой диагностики**: ``` ПРОБЛЕМА: <краткое описание> Детали: resource_name: vm-sless-1 instance_uid: service_id: 28 параметр: vapp_uid ожидалось: f037ddea (running) фактически: fba91c08 (deleted) Рекомендации: 1. Удалите suspended VM из облака через ЛК 2. Повторите terraform apply -- ИЛИ -- 1. Восстановите vApp через ЛК 2. Повторите terraform apply ``` --- ## 4. ЗАТРАГИВАЕМЫЕ ФАЙЛЫ (итого) | Файл | Тип правки | Уровень | |------|-----------|---------| | `universal_rebuild/internal/resources_core/crud.go` | Добавить `ValidateRefParamsOnAdopt`, проверку operation_in_progress | 1 | | `universal_rebuild/internal/core/instance_lookup.go` | Расширить `FindInstanceByDisplayName` — множественные инстансы | 1 | | `universal_rebuild/internal/resources_core/state_refresh.go` | Warning при drift, обработка 404/deleted | 2 | | `universal_rebuild/internal/resources_core/diagnostics.go` | **Новый файл** — хелперы для стандартных диагностик | 3 | **НЕ затрагиваются**: файлы в `internal/resources_gen/` — они перегенерируются автоматически. **НЕ затрагиваются**: генераторы — логика целиком в core/resources_core. --- ## 5. ПОРЯДОК РЕАЛИЗАЦИИ 1. `diagnostics.go` (новый файл — хелперы, от которых зависят остальные правки) 2. `instance_lookup.go` — множественные инстансы 3. `crud.go` — ValidateRefParamsOnAdopt + operation_in_progress 4. `state_refresh.go` — drift warnings + 404/deleted 5. `go build` — проверка компиляции 6. Тест на стенде: destroy → delete vApp в ЛК → apply → убедиться что ошибка понятная