add: documentation

This commit is contained in:
“Naeel”
2026-06-30 15:45:24 +04:00
parent 540c1f7293
commit ca276d200f
1055 changed files with 47294 additions and 0 deletions
+178
View File
@@ -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.
+119
View File
@@ -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**.
Сгенерированные ресурсы пересозданы после изменения генератора.