- Определена архитектура 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 ресурс)
10 KiB
План исправления 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с updatevault_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 строк
Логика обычно такая:
- Parse state:
var plan postgresResourceModel - Extract resource ID:
resourceID := plan.ID.ValueString() - Подготовить данные для API
- Вызвать API update:
operation := r.client.UpdatePostgres(...) - Poll операцию до completion
- Прочитать результат из API
- Обновить 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() должно быть:
- Создать ресурс через API (async operation)
- Poll операцию
- Когда успешно — прочитать ресурс из API
- Заполнить ID новый из ответа (только тут ID меняется!)
- Заполнить остальные поля
- Set state
В Update() должно быть:
- Взять существующий ID из state
- Отправить update в API
- Poll операцию
- Когда успешно — прочитать обновлённое состояние ресурса из API
- ID остаётся прежним! ← это главное отличие
- Обновить другие поля из API ответа
- 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
Ищи и исправь:
-
В методе Update() — строки 200-400 (примерно)
- Найди где устанавливается
plan.ID - Изменение: если
plan.ID = types.StringUnknown()— замени наplan.ID = state.ID
- Найди где устанавливается
-
После API call в Update()
- Должен быть код типа:
result := r.client.Update(...)или polling - После получения результата нужно обновить state из результата
- Исправление: добавить чтение state_out из результата, оставить ID неизменным
- Должен быть код типа:
-
В методе 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— отметить дату исправления