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 ресурс)
This commit is contained in:
@@ -0,0 +1,292 @@
|
||||
# План исправления 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` — отметить дату исправления
|
||||
|
||||
Reference in New Issue
Block a user