# План исправления ERR-PG-02: id=(known after apply) при Update nubes_postgres Дата: 2026-04-02 Файл провайдера: `/home/naeel/terra/terraform/internal/provider/postgres_resource.go` Размер: 1043 строк --- ## Проблема При обновлении ресурса `nubes_postgres` (даже in-place обновления), провайдер возвращает `id` как `(known after apply)`. Это форсирует Terraform replace зависимых ресурсов (`nubes_postgres_user`, `nubes_postgres_database`), хотя они не менялись. ### Симптом в плане ``` Plan: 3 to add, 1 to change, 2 to destroy. ``` Вместо ожидаемого: ``` Plan: 1 to add, 0 to change, 0 to destroy. ``` ### Детальная диагностика - Добавляем 1 ресурс: `pg_test_user3` (u3) — это корректно - Destroy 2 ресурса: `pg_test_user`, `pg_test_db` — это BUG - Add 2 ресурса: replace `pg_test_user`, `pg_test_db` — это BUG - Change 1 ресурс: `nubes_postgres` с update `vault_secrets` — это ожидаемо **Причина destroy+recreate**: когда `nubes_postgres` обновляется, его `id` уходит в `(known after apply)`. Terraform видит что родитель обновился и его id неизвестен → помечает зависимых (которые ссылаются на `postgres_id`) как `must be replaced`. --- ## Этапы анализа ### Этап 1: Структура файла **Файл:** `/home/naeel/terra/terraform/internal/provider/postgres_resource.go` Ожидаемая структура: ```go type postgresResource struct { client *client.Client } // CRUD методы: func (r *postgresResource) Create(ctx context.Context, ...) { ... } func (r *postgresResource) Read(ctx context.Context, ...) { ... } func (r *postgresResource) Update(ctx context.Context, ...) { ... } // ← ГЛАВНЫЙ ИНТЕРЕС func (r *postgresResource) Delete(ctx context.Context, ...) { ... } ``` ### Этап 2: Найди метод Update() **Что искать:** ``` func (r *postgresResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) ``` **Размер:** обычно 50-150 строк **Логика обычно такая:** 1. Parse state: `var plan postgresResourceModel` 2. Extract resource ID: `resourceID := plan.ID.ValueString()` 3. Подготовить данные для API 4. Вызвать API update: `operation := r.client.UpdatePostgres(...)` 5. Poll операцию до completion 6. **Прочитать результат из API** 7. **Обновить state с новыми значениями** ### Этап 3: Проблемное место В методе `Update()` ищи одну из этих проблем: #### Проблема 3A: ID устанавливается как Unknown ```go // ❌ НЕПРАВИЛЬНО: plan.ID = types.StringUnknown() // или types.StringValue(...) resp.State.Set(ctx, plan) ``` **Исправление:** ```go // ✅ ПРАВИЛЬНО: plan.ID = state.ID // Keep existing ID resp.State.Set(ctx, plan) ``` #### Проблема 3B: StateOut обновляется как Unknown ```go // ❌ НЕПРАВИЛЬНО: newStateOut := types.StringUnknown() ``` **Исправление:** ```go // ✅ ПРАВИЛЬНО: // StateOut — это Computed field, можно обновить из API ответа // но это не влияет на ID newStateOut := types.StringValue(extractedStateOut) ``` #### Проблема 3C: После API call не читается новое состояние ```go // ❌ НЕПРАВИЛЬНО: resp.State.Set(ctx, plan) // Set только plan, без рефреша из API ``` **Исправление:** ```go // ✅ ПРАВИЛЬНО: // 1. Операция выполнена // 2. Прочитать state_out из API (Read или GetStatus операции) // 3. Обновить plan.StateOut = newStateOut // 4. Оставить plan.ID = state.ID (ID не меняется!) respObject := r.client.GetPostgresStatus(resourceID) plan.StateOut = types.StringValue(respObject.StateOut) resp.State.Set(ctx, plan) ``` ### Этап 4: Сравни с Create() **В Create() должно быть:** 1. Создать ресурс через API (async operation) 2. Poll операцию 3. Когда успешно — прочитать ресурс из API 4. **Заполнить ID новый из ответа** (только тут ID меняется!) 5. Заполнить остальные поля 6. Set state **В Update() должно быть:** 1. Взять существующий ID из state 2. Отправить update в API 3. Poll операцию 4. Когда успешно — прочитать **обновлённое состояние** ресурса из API 5. **ID остаётся прежним!** ← это главное отличие 6. Обновить другие поля из API ответа 7. Set state --- ## Этап 5: Поиск конкретных строк ### Задача 5.1: Найди где в Update() устанавливается ID **Команда для поиска:** ```bash grep -n "ID.*StringUnknown\|ID.*StringValue\|ID = " postgres_resource.go | head -20 ``` Ищи строки типа: - `plan.ID = ...` - `resp.State.Set(...)` - `var data postgresResourceModel` ### Задача 5.2: Найди где вызывается API **Команда:** ```bash grep -n "client\.\|Update\|Create\|GetStatus" postgres_resource.go ``` Ищи: - `r.client.UpdatePostgres(...)` - `r.client.CreatePostgres(...)` - Polling loop ### Задача 5.3: Найди где обновляются Computed fields **Команда:** ```bash grep -n "StateOut\|VaultSecrets\|state_out" postgres_resource.go ``` Эти поля **могут** меняться при Update, но это нормально. --- ## Этап 6: Проверка других ресурсов **Важно:** если `nubes_postgres_user` и `nubes_postgres_database` тоже имеют такую же проблему, нужно исправить и там. Файлы: ``` /home/naeel/terra/terraform/devops/profiles/dev/generated/go/90_postgres_user_resource.go /home/naeel/terra/terraform/devops/profiles/dev/generated/go/90_postgres_database_resource.go ``` Логика должна быть та же — parent ID не должен меняться при Update(). --- ## Этап 7: Тестирование После исправления: ### 7.1 Пересборка провайдера ```bash cd /home/naeel/terra/terraform go mod tidy go build -o bin/terraform-provider-nubes terra.k8c.ru/naeel/nubes ``` ### 7.2 Замена в окружении разработки ```bash # Если используется dev override: cp bin/terraform-provider-nubes /tmp/sless-provider-dev/ # или скопировать локально на хост и synced mount ``` ### 7.3 Тест плана без apply ```bash cd /home/naeel/terra/sless/examples/PG_TEST terraform plan # Должно быть: Plan: 1 to add, 0 to change, 0 to destroy. # (только добавление pg_test_user3, без destroy/recreate остальных) ``` ### 7.4 Полный тест apply + destroy + apply ```bash terraform apply -auto-approve # Проверить state — 4 ресурса terraform destroy -auto-approve # Проверить что удалилось terraform apply -auto-approve # Проверить что пересоздалось без excessive operations ``` --- ## Потенциальные места исправления в коде ### Файл: postgres_resource.go **Ищи и исправь:** 1. **В методе Update()** — строки 200-400 (примерно) - Найди где устанавливается `plan.ID` - **Изменение:** если `plan.ID = types.StringUnknown()` — замени на `plan.ID = state.ID` 2. **После API call в Update()** - Должен быть код типа: `result := r.client.Update(...)` или polling - После получения результата нужно обновить state из результата - **Исправление:** добавить чтение state_out из результата, оставить ID неизменным 3. **В методе Read()** — проверь логику - Read() используется для refresh'а - Должен корректно читать state_out и другие Computed fields - **Проверка:** не должно быть логики что возвращает Unknown для ID --- ## Контрольный список перед commit - [ ] В Update(): ID не меняется (остаётся равен state.ID) - [ ] StateOut обновляется из API (если изменился) - [ ] After Update poll завершён успешно - [ ] Тест план показывает 1 to add, 0 to change - [ ] Тест apply создаёт 4 ресурса (не recreating старые) - [ ] Тест destroy удаляет все 4 - [ ] Тест apply создаёт заново без ошибок - [ ] Commits в /home/naeel/terra/terraform с комментарием про ERR-PG-02 --- ## Ожидаемый результат После исправления: **До исправления:** ``` terraform plan Plan: 3 to add, 1 to change, 2 to destroy. (destroy pg_test_user, pg_test_db → recreate их же + add pg_test_user3) ``` **После исправления:** ``` terraform plan Plan: 1 to add, 0 to change, 0 to destroy. (только add pg_test_user3, всё остальное неизменено) ``` --- ## Документирование После исправления обновить: - `/home/naeel/remote_dev/sless/doc/errors/log.md` — ERR-PG-02 status FIXED - `/home/naeel/remote_dev/sless/doc/progress.md` — отметить дату исправления