add: documentation
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
# 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: <uuid>
|
||||
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 → убедиться что ошибка понятная
|
||||
@@ -0,0 +1,67 @@
|
||||
# Docs Generation Strategy: ClickHouse Template
|
||||
|
||||
## Purpose
|
||||
This document captures the current documentation generation logic based on the ClickHouse template. It is a living reference and should be updated when the generator logic changes or issues are found.
|
||||
|
||||
## Scope
|
||||
- Applies to all resources generated from API-derived YAML specs in:
|
||||
- `universal_rebuild/resources_yaml/*.yaml`
|
||||
- Output target:
|
||||
- `docs/30_registry/resources/`
|
||||
- ClickHouse is the reference template and is excluded from automatic generation by default.
|
||||
|
||||
## Current Generator
|
||||
- Go generator:
|
||||
- `universal_rebuild/tools/docs_template_gen/main.go`
|
||||
- Runner script:
|
||||
- `devops/02_generate_resources_and_docs_template.sh`
|
||||
|
||||
## Input Sources
|
||||
- Primary source of truth: API YAML specs
|
||||
- `universal_rebuild/resources_yaml/*.yaml`
|
||||
- Service ordering:
|
||||
- `devops/services_list.txt`
|
||||
|
||||
## Output Structure (per resource)
|
||||
For each resource `name`, the generator writes:
|
||||
- `name.md` (Manual)
|
||||
- `name_params_create.md`
|
||||
- `name_params_modify.md`
|
||||
- `name_outputs.md`
|
||||
- `name_ops.md`
|
||||
- `name_example.md`
|
||||
- `name_params.md` (landing)
|
||||
|
||||
Each page uses a fixed nav row:
|
||||
`Manual | Create params | Modify params | Output params | Operations | Example`
|
||||
|
||||
## Template Notes
|
||||
- Manual page uses `service_man` content from YAML.
|
||||
- Example page builds a copy-ready manifest with required params first, then default params.
|
||||
- Create params are split into:
|
||||
- Required params (no Default column)
|
||||
- Params with defaults (Default column kept)
|
||||
- Modify params table has no Required or Default columns.
|
||||
- Output params list is derived from `outputs.params`.
|
||||
- Operations page lists actions with links to Create/Modify pages and subresources where present.
|
||||
- Lifecycle defaults are rendered as a small note at the end of Create params if present in YAML.
|
||||
|
||||
## Snapshot + Validation Process
|
||||
Before running the generator, take a snapshot of the current ClickHouse docs:
|
||||
- Location: `docs/30_registry/resources/_snapshot_clickhouse_YYYYMMDD/`
|
||||
- Content: copy all `clickhouse*.md`
|
||||
|
||||
After generation, compare the new outputs to the snapshot to validate:
|
||||
- Nav row consistency
|
||||
- Table structure and column rules
|
||||
- Lifecycle note format and line breaks
|
||||
- Example formatting and default params separation
|
||||
|
||||
## Known Exclusions
|
||||
- ClickHouse is excluded by default to preserve manual refinements.
|
||||
- To include ClickHouse for testing, remove it from the generator exclude list.
|
||||
|
||||
## Update Policy
|
||||
- When generator logic changes, update this document.
|
||||
- Record any issues found during comparison and how they were resolved.
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
# Terraform Provider Development Strategy & AI Integration
|
||||
|
||||
## 1. Vision: "The Helpful Kitchen Assistant"
|
||||
The provider should not just be a silent executor of commands. It should act as an intelligent assistant for users of all skill levels (the "Cook" persona). It must provide context, warnings, and advice *before* any potentially destructive or wasteful actions are taken.
|
||||
|
||||
## 2. Advanced Pre-Apply Logic
|
||||
To prevent "bad applies" and incomplete infrastructure states, the provider will implement:
|
||||
|
||||
### A. Context-Aware Validation (Scanning the Cloud)
|
||||
- **Data Sources as Sensors:** Use Data Sources not just for referencing existing resources, but as "sensors" to understand the current cloud state during `terraform plan`.
|
||||
- **Pre-emptive Warnings:** Compare the proposed `plan` against current cloud limits, existing resource names, and regional availability.
|
||||
- **Example:** Trigger a warning if a user is creating a 6th bucket while 5 existing ones are empty.
|
||||
|
||||
### B. Cascading & Logical Validation (Deep Validation)
|
||||
- **Attribute Inter-dependency:** Check compatibility between linked resources (e.g., App vs DB versions, S3 permissions vs CDN requirements).
|
||||
- **Early Termination:** If Resource A in a chain is logically "broken" or suboptimal, the provider should error out or warn on the entire chain before `apply` begins.
|
||||
|
||||
## 3. Human-Centric Output & AI Insights
|
||||
- **Standardized Info:** `terraform plan` should output human-readable summaries of what's already in the cloud, not just what's being changed.
|
||||
- **AI-Driven Advice:**
|
||||
- The provider outputs a structured JSON state.
|
||||
- An AI Agent (Sub-chef) analyzes this JSON.
|
||||
- Result: "Hey, I noticed you're creating a DB in Region A but your VMs are in Region B. This will cause latency. Want to fix it?"
|
||||
|
||||
## 4. Implementation via Taffy API Specifics
|
||||
- **Lifecycle Utilization:** Leverage the 3-step Taffy lifecycle (`Operation -> Parameters -> Run`).
|
||||
- **Validation Step:** Use the `/validate-cfs` API endpoint during the Terraform `Plan` or `Create` phase (before the final `Run`) to ensure the cloud accepts the parameters without actually committing the change.
|
||||
|
||||
## 5. Reliability & Uncertainty Tracking
|
||||
- All documentation and internal agent logic should use **Confidence Scores** (e.g., `[CONFIDENCE: 85%]`).
|
||||
- Explicitly mark `[UNCERTAINTY]` tags for deprecated API features or areas where documentation contradicts observed behavior (from HAR/Traffic analysis).
|
||||
|
||||
---
|
||||
*This document serves as the architectural north star for the project.*
|
||||
|
||||
## 6. Подресурсы (subresource) и поведение Terraform
|
||||
|
||||
### Зачем нужен ForceNew
|
||||
Для подресурсов вроде `create_user`, `delete_user`, `create_database` часто **нет** операции `modify` в API. Это значит, что при изменении ключевых полей (например, `username` или `dbName`) мы не можем обновить объект на месте — только удалить и создать заново.
|
||||
|
||||
**ForceNew** — это стандартный механизм Terraform: изменение таких полей приводит к **замене** ресурса (Delete + Create). Это предотвращает ложные обновления, когда Terraform считает, что всё изменилось, а в API реально ничего не выполнено.
|
||||
|
||||
### Правило для генерации подресурсов
|
||||
- Если у подресурса **нет** операции `modify`, то все его параметры считаются **ForceNew**.
|
||||
- Если `modify` есть, то **ForceNew** получают параметры, которые участвуют в идентификации/удалении (ключи, по которым ресурс можно уникально определить).
|
||||
|
||||
### Итоговое поведение
|
||||
- Изменение ключевых параметров подресурса => ресурс заменяется.
|
||||
- Это соответствует возможностям API и сохраняет корректность состояния Terraform.
|
||||
|
||||
## 7. Каноничная lifecycle-логика для сервисов с suspend/resume
|
||||
|
||||
Этот раздел обязателен для всех агентов, генераторов и разработчиков.
|
||||
Если в других документах встречаются старые правила (`resume_if_exists`, `delete_mode`), приоритет всегда у этого раздела.
|
||||
|
||||
Область действия:
|
||||
- Только `instance`-ресурсы, у которых API поддерживает `suspend`/`resume`.
|
||||
- Для сервисов без `suspend` действует обычная логика create/modify/delete.
|
||||
|
||||
Флаги:
|
||||
- `adopt_existing_on_create` (optional, default: `false`) — разрешает усыновление уже `running` ресурса при create/apply.
|
||||
- `suspend_on_destroy` (optional, default: `true`) — при destroy/удалении из манифеста выполнять `suspend` вместо удаления из state.
|
||||
|
||||
Правила по умолчанию:
|
||||
- Никакого неявного adopt/import: без `adopt_existing_on_create=true` найденный `running` ресурс считается конфликтом.
|
||||
- На destroy выполняется `suspend` по умолчанию (`suspend_on_destroy=true`).
|
||||
- Режим state-only допускается только при явном `suspend_on_destroy=false`.
|
||||
|
||||
## 8. Обязательные правила apply / plan / destroy / modify
|
||||
|
||||
### Apply/Create (instance с suspend/resume)
|
||||
- Всегда проверить облако по `resource_name` перед create.
|
||||
- Если ресурс не найден в облаке или найден только в статусе `deleted` — всегда `create`.
|
||||
- Если ресурс в статусе `suspend`:
|
||||
- при `adopt_existing_on_create=true` и совпадении основных параметров — `resume` + `adopt`, и явное сообщение в plan/apply;
|
||||
- при `adopt_existing_on_create=false` — hard error (явно требовать включить флаг для resume/adopt);
|
||||
- при несовпадении — hard error с перечислением несовпавших ключей.
|
||||
- Если ресурс в статусе `running`:
|
||||
- при `adopt_existing_on_create=true` — adopt (import-поведение) с явным сообщением в plan;
|
||||
- при `adopt_existing_on_create=false` — hard error "resource already exists".
|
||||
- Если ресурс в статусе `not created` — hard error "проверьте ресурс в личном кабинете" (без auto-adopt/create).
|
||||
- Если ресурс в статусе `creating`/`pending`/`failed` — hard error.
|
||||
|
||||
### Формат диагностик (обязательно)
|
||||
- Диагностики должны быть многострочными и читаемыми.
|
||||
- В тексте обязательно указывать:
|
||||
- итоговое решение (`adopt`/`resume`/`error`),
|
||||
- какой флаг повлиял (`adopt_existing_on_create`/`suspend_on_destroy`),
|
||||
- детализированные поля (`resource_name`, `service_id`, `instance_uid`, `status`, `status_raw`, `operation_pending`, `operation_in_progress`) отдельными строками.
|
||||
- Для конфликта `resource already exists` при `adopt_existing_on_create=false` диагностика обязана явно предлагать оба пути:
|
||||
- изменить `resource_name`, если нужен новый ресурс;
|
||||
- включить `adopt_existing_on_create=true` и повторить `apply` для импорта/усыновления существующего ресурса.
|
||||
|
||||
### Destroy / удаление из манифеста (instance с suspend/resume)
|
||||
- Если `suspend_on_destroy=true` — выполнить `suspend`.
|
||||
- Если `suspend_on_destroy=false` — удалить только из Terraform state, без delete/suspend вызовов в API (осознанный override).
|
||||
|
||||
### Modify
|
||||
- При diff по immutable/create-only параметрам — hard error (replace запрещён).
|
||||
- При diff только по mutable параметрам — выполнить `modify`.
|
||||
|
||||
### Replace
|
||||
- Для сервисов с `suspend` запрещён любой implicit replace (`delete+create`).
|
||||
- Попытка replace должна завершаться ошибкой на этапе plan.
|
||||
|
||||
## 9. Контрольный список для реализации
|
||||
|
||||
### Plan messages (обязательно)
|
||||
- Явно указывать обнаруженный cloud status: `deleted`/`suspend`/`running`/`not created`/`creating`.
|
||||
- Явно указывать, какой флаг повлиял на решение: `adopt_existing_on_create` или `suspend_on_destroy`.
|
||||
- Для веток `resume` и `adopt` показывать причину выбора действия.
|
||||
|
||||
### Safety guards
|
||||
- Для `suspend` + mismatch не допускать auto-resume.
|
||||
- Для `running` без `adopt_existing_on_create=true` не допускать auto-adopt.
|
||||
- После `resume`/`adopt` выполнять read-back и обновлять state только по фактическому ответу API.
|
||||
|
||||
### Терминология
|
||||
- `resume_if_exists` и `delete_mode` считаются legacy-терминами и не используются в новой логике.
|
||||
@@ -0,0 +1,243 @@
|
||||
# Terraform Provider: Проблема регистра UUID (case-sensitivity)
|
||||
|
||||
> Документ описывает проблему, все неработающие подходы и правильное решение.
|
||||
> **2 часа разбора.** Записано, чтобы не повторять.
|
||||
|
||||
---
|
||||
|
||||
## 1. Суть проблемы
|
||||
|
||||
Пользователь пишет в `.tf`:
|
||||
|
||||
```hcl
|
||||
resource "nubes_vc_vm_v3" "vm" {
|
||||
vapp_uid = "6214BA32-4A55-4715-8DEC-0E18D7B09F35" # ВЕРХНИЙ РЕГИСТР
|
||||
}
|
||||
```
|
||||
|
||||
API нашего облака принимает UUID в любом регистре, но **всегда возвращает lowercase**:
|
||||
|
||||
```json
|
||||
{ "vappUid": "6214ba32-4a55-4715-8dec-0e18d7b09f35" }
|
||||
```
|
||||
|
||||
Terraform после `apply` проверяет:
|
||||
|
||||
```
|
||||
plan.vapp_uid = "6214BA32-..." (= то что написал пользователь, == config)
|
||||
state.vapp_uid = "6214ba32-..." (= то что вернул API)
|
||||
```
|
||||
|
||||
Они НЕ равны → Terraform бросает:
|
||||
|
||||
```
|
||||
Error: Provider produced inconsistent result after apply
|
||||
```
|
||||
|
||||
После этого ресурс помечается как **tainted** и при следующем `plan` предлагает пересоздать его с нуля.
|
||||
|
||||
---
|
||||
|
||||
## 2. Ключевое правило Terraform Plugin Framework
|
||||
|
||||
**Официальная документация** (https://developer.hashicorp.com/terraform/plugin/framework/resources/plan-modification):
|
||||
|
||||
> _"If an attribute value is configured, it is **NEVER valid** to change that value in the plan."_
|
||||
|
||||
Это означает: `plan` **обязан** равняться `config`. Terraform core проверяет это ПОСЛЕ всех PlanModifiers и ModifyPlan. Никаким образом нельзя изменить план, если пользователь указал значение атрибута.
|
||||
|
||||
**Единственно правильное решение: привести STATE к значению из PLAN (= config), а не наоборот.**
|
||||
|
||||
---
|
||||
|
||||
## 3. Все неработающие подходы (НЕ ДЕЛАТЬ)
|
||||
|
||||
### ❌ Подход 1: `strings.ToLower` для plan-значений
|
||||
|
||||
```go
|
||||
// НЕПРАВИЛЬНО: меняем plan
|
||||
data.VappUid = types.StringValue(strings.ToLower(data.VappUid.ValueString()))
|
||||
```
|
||||
|
||||
Результат: `plan != config` → framework паникует ещё до apply.
|
||||
|
||||
### ❌ Подход 2: Write-back в `ModifyPlan`
|
||||
|
||||
```go
|
||||
func (r *Resource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
|
||||
// Пробовали нормализовать UUID в plan здесь
|
||||
resp.Plan.SetAttribute(ctx, path.Root("vapp_uid"), types.StringValue(lower))
|
||||
}
|
||||
```
|
||||
|
||||
Результат: Terraform core отвергает — config-значение изменить нельзя на уровне плана.
|
||||
|
||||
### ❌ Подход 3: `Computed: true` для ref_svc полей
|
||||
|
||||
Делает поле "вычислимым" — но тогда пользователь не может его задать явно без мерцания diff'а.
|
||||
|
||||
### ❌ Подход 4: `UUIDNormalize()` PlanModifier
|
||||
|
||||
```go
|
||||
schema.StringAttribute{
|
||||
PlanModifiers: []planmodifier.String{resources_core.UUIDNormalize()},
|
||||
}
|
||||
```
|
||||
|
||||
Файл `uuid_planmodifier.go` был создан, но не работает по той же причине — нельзя менять config-значение в plan.
|
||||
|
||||
---
|
||||
|
||||
## 4. Правильное решение
|
||||
|
||||
**Источник истины**: plan = config = "6214BA32-..." (регистр пользователя, неизменен)
|
||||
**Задача**: state после apply тоже должен быть "6214BA32-..."
|
||||
|
||||
### Шаг 1 — Сохраняем оригинал ДО resolve
|
||||
|
||||
В `Create` генерируемого ресурса сохраняем значение ИЗ PLAN до того, как `ResolveRefSvcParamValue` переведёт его в lowercase (нужный для API):
|
||||
|
||||
```go
|
||||
// В шаблоне instanceTemplate (Create):
|
||||
originalVappUid := data.VappUid // сохраняем "6214BA32-..." из plan
|
||||
|
||||
// Resolve для API (переводит в lowercase):
|
||||
resolved, _ := r.client.ResolveRefSvcParamValue(ctx, refSvcId, data.VappUid.ValueString())
|
||||
data.VappUid = types.StringValue(resolved) // теперь data.VappUid = "6214ba32-..."
|
||||
|
||||
// ... CreateResourceWithTimeout отправляет lowercase в API ...
|
||||
// ... RefreshResourceState возвращает state с lowercase из API ...
|
||||
```
|
||||
|
||||
### Шаг 2 — Восстанавливаем регистр пользователя в state
|
||||
|
||||
После `RefreshResourceState` (которая заполняет state из API-ответа в lowercase):
|
||||
|
||||
```go
|
||||
// Restore user-provided casing: state must match plan (= config) exactly.
|
||||
if !originalVappUid.IsNull() && !originalVappUid.IsUnknown() &&
|
||||
!state.VappUid.IsNull() && !state.VappUid.IsUnknown() {
|
||||
// strings.EqualFold — сравниваем без учёта регистра
|
||||
if strings.EqualFold(state.VappUid.ValueString(), originalVappUid.ValueString()) {
|
||||
state.VappUid = originalVappUid // "6214ba32-..." → "6214BA32-..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Итог:
|
||||
```
|
||||
plan.vapp_uid = "6214BA32-..." (config, пользователь)
|
||||
state.vapp_uid = "6214BA32-..." (восстановлено из original)
|
||||
↓
|
||||
plan == state ✓ → нет ошибки → нет taint
|
||||
```
|
||||
|
||||
### Аналогичная логика для Read и Update
|
||||
|
||||
- **Read**: нет plan, но есть `prior state` (уже хранится с регистром пользователя). Сравниваем `newState` (из API) с `state` (prior) через EqualFold → берём из prior.
|
||||
- **Update**: plan = config = регистр пользователя. Сравниваем новый `state` из API с `plan` через EqualFold → берём из plan.
|
||||
|
||||
---
|
||||
|
||||
## 5. Где это реализовано
|
||||
|
||||
### `tools/gen_v2/generate_resources_v2.go` — ГЕНЕРАТОР
|
||||
|
||||
Все изменения в шаблоне `instanceTemplate`. Вручную трогать generated-файлы запрещено.
|
||||
|
||||
| Блок | Что делает |
|
||||
|------|-----------|
|
||||
| `original{{ToCamel .Code}} := data.{{ToCamel .Code}}` | Шаг 1: сохраняем перед resolve |
|
||||
| Restore-блок в Create | Шаг 2: восстанавливаем после RefreshResourceState |
|
||||
| Restore-блок в Read | Берём регистр из prior state |
|
||||
| Restore-блок в Update | Берём регистр из plan |
|
||||
| `hasRestoreCasingParams()` | Определяет нужен ли `"strings"` import |
|
||||
| `NeedsStringsImport` | `hasRestoreCasingParams(SchemaParams) || analyzeNeedsStrings(ModifyParams)` |
|
||||
|
||||
**Условие генерации restore-блоков**: `RefSvcId > 0 AND RefSvcId != 12 AND тип string`
|
||||
|
||||
> Почему `!= 12`? Для S3 (refSvcId=12) пользователь **обязан** указать UUID — имена вида "s3-111805" не принимаются. Поэтому регистр-специфический restore для S3 не генерируется.
|
||||
|
||||
### `internal/core/client.go` — функция `resolveRefSvcParamValues`
|
||||
|
||||
Нормализует UUID к lowercase для отправки в API. Это правильно — API получает lowercase. Но это НЕ влияет на то что хранится в state (state управляется generated resource).
|
||||
|
||||
### `internal/resources_core/params_compare.go` — функция `normalizeCompareValue`
|
||||
|
||||
Нормализует UUID к lowercase при сравнении desired vs actual в `ParamsMatchForResume`. Нужно для suspend/resume: без этого "6214BA32-..." != "6214ba32-..." и система думала что параметры изменились → создавала новый инстанс вместо resume.
|
||||
|
||||
### `internal/resources_core/resource_diagnostics_required.go`
|
||||
|
||||
Функция `containsValueInsensitive` для проверки realm. До фикса `containsValueCaseSensitive` не пускала пользователей, если realm написан не в том регистре.
|
||||
|
||||
---
|
||||
|
||||
## 6. NeedsStringsImport логика (важная деталь)
|
||||
|
||||
Изначально было `NeedsStringsImport = gr.HasRefSvcParams` — добавляло `"strings"` если есть ANY ref_svc поля.
|
||||
Но harbor (serviceId=82) имеет только `s3Uid` с `refSvcId=12` → restore-блок НЕ генерируется → `"strings"` не используется → ошибка компиляции `"strings" imported and not used`.
|
||||
|
||||
Правильная логика:
|
||||
|
||||
```go
|
||||
// hasRestoreCasingParams: есть ли поля с RefSvcId > 0 && != 12 (для них генерируется restore-блок)
|
||||
gr.NeedsStringsImport = hasRestoreCasingParams(gr.SchemaParams) || analyzeNeedsStrings(gr.ModifyParams)
|
||||
```
|
||||
|
||||
`analyzeNeedsStrings(ModifyParams)` покрывает ресурсы, у которых есть строковые modify-параметры → в Update генерируется `strings.EqualFold` для `hasServiceParamChanges`.
|
||||
|
||||
---
|
||||
|
||||
## 7. Итоговый поток данных
|
||||
|
||||
```
|
||||
Пользователь: vapp_uid = "6214BA32-..."
|
||||
│
|
||||
▼
|
||||
[plan] = "6214BA32-..." ← НЕИЗМЕНЕН, это правило Terraform
|
||||
│
|
||||
▼
|
||||
Create: originalVappUid = "6214BA32-..." ← сохраняем
|
||||
│
|
||||
▼
|
||||
ResolveRefSvcParamValue → "6214ba32-..." ← для отправки в API
|
||||
│
|
||||
▼
|
||||
API call: POST ...?vappUid=6214ba32-... ← lowercase
|
||||
│
|
||||
▼
|
||||
RefreshResourceState: state.VappUid = "6214ba32-..." ← API вернул lowercase
|
||||
│
|
||||
▼
|
||||
Restore: EqualFold("6214ba32-...", "6214BA32-...") == true
|
||||
→ state.VappUid = "6214BA32-..." ← восстанавливаем
|
||||
│
|
||||
▼
|
||||
resp.State.Set(state) → state = "6214BA32-..."
|
||||
│
|
||||
▼
|
||||
plan == state == "6214BA32-..." ✓ → no diff → no taint
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Если снова появится похожая ошибка
|
||||
|
||||
> `Error: Provider produced inconsistent result after apply`
|
||||
> `Error: Provider produced invalid plan`
|
||||
|
||||
**Алгоритм диагностики:**
|
||||
|
||||
1. Найти поле, которое отличается в plan vs state.
|
||||
2. Проверить: API возвращает то же значение но в другом регистре / формате?
|
||||
3. **Не трогать plan.** Правило: plan == config всегда.
|
||||
4. Добавить restore-логику в state ПОСЛЕ `RefreshResourceState`.
|
||||
5. Использовать `strings.EqualFold` для сравнения строк с возможными регистровыми различиями.
|
||||
6. **Никогда не создавать PlanModifier для config-атрибутов** — он не может изменить значение заданное пользователем.
|
||||
|
||||
---
|
||||
|
||||
## 9. Версия
|
||||
|
||||
Фикс введён в версии провайдера **5.0.46**.
|
||||
Сгенерированные ресурсы пересозданы после изменения генератора.
|
||||
Reference in New Issue
Block a user