Files
sless/doc/provider-fix-plan.md
Repinoid 6dc2dc69ba IoT MVP: архитектура, план реализации, лог рассуждений
- Определена архитектура managed IoT service
- Согласовано решение: RabbitMQ (MVP), потом Kafka; EMQX + CRD контроллер
- Создан подробный план для Sonnet (doc/iot-mvp-plan.md)
- Добавлено правило в copilot-instructions: лог мышления в doc/thinking/ по датам
- Полный ход рассуждений в doc/thinking/2026-04-04.md

Ключевые решения:
- IoT код в iot/ (легко вынести потом)
- CRD IoTDevice + контроллер (как все остальное в sless)
- EMQX HTTP Auth Backend для динамической аутентификации устройств
- Архитектура broker-agnostic (легко переключить на Kafka)
- Terraform: расширяем текущий provider (sless_iot_device ресурс)
2026-04-04 08:34:14 +03:00

10 KiB
Raw Permalink Blame History

План исправления 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

Ожидаемая структура:

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

// ❌ НЕПРАВИЛЬНО:
plan.ID = types.StringUnknown()  // или types.StringValue(...)
resp.State.Set(ctx, plan)

Исправление:

// ✅ ПРАВИЛЬНО:
plan.ID = state.ID  // Keep existing ID
resp.State.Set(ctx, plan)

Проблема 3B: StateOut обновляется как Unknown

// ❌ НЕПРАВИЛЬНО:
newStateOut := types.StringUnknown()

Исправление:

// ✅ ПРАВИЛЬНО:
// StateOut — это Computed field, можно обновить из API ответа
// но это не влияет на ID
newStateOut := types.StringValue(extractedStateOut)

Проблема 3C: После API call не читается новое состояние

// ❌ НЕПРАВИЛЬНО:
resp.State.Set(ctx, plan)  // Set только plan, без рефреша из API

Исправление:

// ✅ ПРАВИЛЬНО:
// 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

Команда для поиска:

grep -n "ID.*StringUnknown\|ID.*StringValue\|ID = " postgres_resource.go | head -20

Ищи строки типа:

  • plan.ID = ...
  • resp.State.Set(...)
  • var data postgresResourceModel

Задача 5.2: Найди где вызывается API

Команда:

grep -n "client\.\|Update\|Create\|GetStatus" postgres_resource.go

Ищи:

  • r.client.UpdatePostgres(...)
  • r.client.CreatePostgres(...)
  • Polling loop

Задача 5.3: Найди где обновляются Computed fields

Команда:

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 Пересборка провайдера

cd /home/naeel/terra/terraform
go mod tidy
go build -o bin/terraform-provider-nubes terra.k8c.ru/naeel/nubes

7.2 Замена в окружении разработки

# Если используется dev override:
cp bin/terraform-provider-nubes /tmp/sless-provider-dev/
# или скопировать локально на хост и synced mount

7.3 Тест плана без apply

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

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 — отметить дату исправления