- Определена архитектура 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 ресурс)
293 lines
10 KiB
Markdown
293 lines
10 KiB
Markdown
# План исправления 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` — отметить дату исправления
|
||
|