v5.0.64: delete idempotency, duplicate adopt, defer unlock, exist marker
This commit is contained in:
@@ -0,0 +1,98 @@
|
|||||||
|
# 2026-07-07 — Opus answers: duplicate/exist fix strategy
|
||||||
|
|
||||||
|
## Контекст
|
||||||
|
Промпт из `/home/naeel/tf_provider/prompt_for_opus_duplicate.md`
|
||||||
|
После mutex (v5.0.63) pg_user_5 упал: «Операция вернула duplicate/exist, но объект не найден в state_out»
|
||||||
|
|
||||||
|
## Корень всех проблем
|
||||||
|
|
||||||
|
Код путает два разных случая «не-found»:
|
||||||
|
- `known && !found` — список в state_out ЕСТЬ, объекта нет → реальная несогласованность
|
||||||
|
- `!known` — списка в state_out НЕТ вообще (PostgreSQL, Kafka) → проверить невозможно
|
||||||
|
|
||||||
|
Сейчас `!known` трактуется как несогласованность → ошибка.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Вопрос 1 (ПРИОРИТЕТ) — ДА, доверять API при `!known`
|
||||||
|
|
||||||
|
**Где:** templates.go → генерится во все subresource Create
|
||||||
|
|
||||||
|
**Фикс:** усыновлять при `found || !known`, ошибку только для `known && !found`:
|
||||||
|
```go
|
||||||
|
if adoptExistingOnCreate && resources_core.IsSubresourceAlreadyExistsError(createErr) {
|
||||||
|
found, known := false, false
|
||||||
|
if targetValue != "" {
|
||||||
|
var checkErr error
|
||||||
|
found, known, checkErr = resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue)
|
||||||
|
if checkErr != nil {
|
||||||
|
resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// found → подтверждено в state_out
|
||||||
|
// !known → списка в state_out нет (PG/Kafka) → доверяем API
|
||||||
|
// known&&!found → реальная несогласованность
|
||||||
|
if found || !known {
|
||||||
|
plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "{{.SubName}}", idParams))
|
||||||
|
resp.Diagnostics.AddWarning("Подресурс усыновлён", "API вернул already exists; объект принят в state: "+targetValue)
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp.Diagnostics.AddError("Нарушена консистентность", "API вернул duplicate/exist, но объект подтверждённо отсутствует в state_out: "+targetValue)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Вопрос 2 — ДА, нужен явный маппинг из метаданных
|
||||||
|
|
||||||
|
**Где:** subresource_guard.go — `SubresourceListKey` (эвристика `name+"s"`) и `SubresourceIdentityKey` (special-map на 2 сервиса)
|
||||||
|
|
||||||
|
**Доказательство:** Kafka — реальные ключи `kafkaUsers`/`kafkaTopics`, эвристика даёт `users`/`topics` → `known=false` даже когда данные есть.
|
||||||
|
|
||||||
|
**Фикс:** генератор должен эмитить явные `listKey`/`idKey` как константы из метаданных. Для сервисов без ключа в state_out (PostgreSQL) — эмитить `listKey=""` → `FindSubresourceInStateOut` → `known=false` → работает путь «доверять API» из Вопроса 1.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Вопрос 3 — НЕТ, list-эндпоинтов не существует
|
||||||
|
|
||||||
|
**Проверено по API-метаданным** (svc 90):
|
||||||
|
- Операции: create(19), delete(20), resume(114), suspend(115), recovery(47), modify(48), restart(54)
|
||||||
|
- Subresource: create_user(241), delete_user(244), create_database(245), delete_database(246)
|
||||||
|
- **Ни одной `list_*`/`get_*`/`show_*`**
|
||||||
|
|
||||||
|
Перечислить подресурсы через API невозможно. Единственный источник — state_out, а у PG его нет.
|
||||||
|
|
||||||
|
**Вывод:** стратегия «доверять API» (Вопрос 1) — единственно возможная.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Вопрос 4 — Частично; текст ОК, нужен HTTP 409
|
||||||
|
|
||||||
|
**Где:** subresource_guard.go — `IsSubresourceAlreadyExistsError`
|
||||||
|
|
||||||
|
- `role "pg_user_5" already exists` → ловится ✅
|
||||||
|
- `already exist` (без s), голый `exist` → НЕ ловится
|
||||||
|
- **Главная дыра:** HTTP 409 без текста → НЕ ловится
|
||||||
|
|
||||||
|
**Фикс:**
|
||||||
|
```go
|
||||||
|
markers := []string{"уже существует", "already exists", "already exist", "duplicate", "conflict", " exist"}
|
||||||
|
```
|
||||||
|
Плюс проверка HTTP 409 (требует проброса статус-кода из клиента).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Приоритет реализации
|
||||||
|
|
||||||
|
1. **Вопрос 1** — чинит `pg_user_5` сразу
|
||||||
|
2. **Вопрос 2** — чинит Kafka и остальные
|
||||||
|
3. **Вопрос 4** — устойчивость к формулировкам
|
||||||
|
4. **Вопрос 3** — документировать как «невозможно, дизайн-решение»
|
||||||
|
|
||||||
|
Всё правится в шаблоне templates.go и subresource_guard.go. Сгенерированные файлы НЕ править — перегенерировать.
|
||||||
|
|
||||||
|
**Дата:** 2026-07-07
|
||||||
|
**Статус:** анализ завершён, правки НЕ внесены
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
# 2026-07-07 — Opus bug analysis: skip_missing_on_delete и связанные баги
|
||||||
|
|
||||||
|
## Prompt
|
||||||
|
Prompt из `/home/naeel/tf_provider/prompt_for_opus_bugs.md` — анализ 3 файлов на баги, аналогичные `skip_missing_on_delete`.
|
||||||
|
|
||||||
|
## Анализированные файлы
|
||||||
|
- `/home/naeel/tf_provider/TOOLS/resource-generator/internal/templates/templates.go`
|
||||||
|
- `/home/naeel/tf_provider/provider/internal/resources_core/crud.go`
|
||||||
|
- `/home/naeel/tf_provider/generated/test/go/90_postgres_database_resource.go`
|
||||||
|
- `/home/naeel/tf_provider/provider/internal/resources_core/subresource_guard.go`
|
||||||
|
- `/home/naeel/tf_provider/provider/internal/core/client.go`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Найденные баги
|
||||||
|
|
||||||
|
### 1. 🔴 КРИТ — Неидемпотентный Delete подресурса
|
||||||
|
**Где:** templates.go / 90_postgres_database_resource.go (сгенерированный код)
|
||||||
|
**Симптом:** если объект удалён вне Terraform, `terraform destroy` падает («Подресурс не найден») вместо успешного удаления из state — ресурс залипает в state навсегда.
|
||||||
|
**Причина:** pre-check `if known && !found { ... AddError }`. Delete отсутствующего объекта по контракту Terraform = успех, а тут ошибка. Дефолт `skip_missing_on_delete=false` лишь усугубляет — это не корень.
|
||||||
|
**Исправление:** при `known && !found` в Delete всегда `return` (успех, объект уже удалён), а не AddError. Флаг `skip_missing_on_delete` тогда становится не нужен.
|
||||||
|
|
||||||
|
### 2. 🔴 — skip_missing_on_delete default false
|
||||||
|
**Где:** templates.go (шаблон генератора)
|
||||||
|
**Симптом:** тот же не-идемпотентный destroy.
|
||||||
|
**Причина:** `booldefault.StaticBool(false)`.
|
||||||
|
**Исправление:** дефолт `true` (либо убрать параметр целиком в пользу фикса №1).
|
||||||
|
|
||||||
|
### 3. 🟡 — IsSubresourceMissingError — хрупкий матчинг
|
||||||
|
**Где:** subresource_guard.go
|
||||||
|
**Симптом:** delete падает как generic-ошибка, если API вернул иную формулировку.
|
||||||
|
**Причина:** матчинг только по 4 подстрокам: `"не найден"`, `"not found"`, `"does not exist"`, `"отсутствует"`. Пропустит `"не существует"`, `"no such database"`, голый `HTTP 404`, `"unknown object"`. Плюс ложные срабатывания — `"not found"` может встретиться в несвязанном тексте.
|
||||||
|
**Исправление:** расширить маркеры (`"не существует"`, `"no such"`, `404`) + учитывать HTTP-код, а не только текст.
|
||||||
|
|
||||||
|
### 4. 🟡 — LockInstance — только в памяти процесса
|
||||||
|
**Где:** client.go
|
||||||
|
**Симптом:** два параллельных `terraform apply` (CI, разные процессы) не сериализуются → orchestrator error / рассинхрон state_out.
|
||||||
|
**Причина:** mutex хранится в `sync.Map` в памяти процесса. Instance-level mutex из v5.0.63 защищает только внутри одного provider-процесса.
|
||||||
|
**Исправление:** документировать ограничение; для кросс-процессной защиты нужен серверный lock/ретрай на 409/«operation in progress».
|
||||||
|
|
||||||
|
### 5. 🔴 КРИТ — adoptExistingInstanceOnCreate: unlock без defer → deadlock
|
||||||
|
**Где:** crud.go, кейсы `StateNotCreated` / `StateSuspended`
|
||||||
|
**Симптом:** при панике в `RunInstanceOperationUniversal` мьютекс инстанса не освобождается → вечный deadlock на этом инстансе.
|
||||||
|
**Причина:** `unlock := client.LockInstance(...)` вызывается вручную, не через `defer`.
|
||||||
|
**Исправление:** обернуть в отдельную функцию с `defer unlock()`.
|
||||||
|
|
||||||
|
### 6. 🟡 — CreateResourceWithTimeout: TOCTOU
|
||||||
|
**Где:** crud.go
|
||||||
|
**Симптом:** между `FindInstanceByDisplayName` и adopt (resume/delete) состояние может измениться.
|
||||||
|
**Причина:** find выполняется вне лока (instanceUid ещё неизвестен).
|
||||||
|
**Исправление:** перепроверять состояние уже под локом внутри adopt перед resume/delete.
|
||||||
|
|
||||||
|
### 7. 🟡 — Read подресурса не проверяет state_out
|
||||||
|
**Где:** 90_postgres_database_resource.go
|
||||||
|
**Симптом:** drift (объект удалён вне Terraform) не обнаруживается; `plan` не покажет пересоздание, а `destroy` затем упадёт (баг №1).
|
||||||
|
**Причина:** `Read` просто переустанавливает state сам в себя, без проверки `FindSubresourceInStateOut`.
|
||||||
|
**Исправление:** в Read проверять наличие в state_out и `resp.State.RemoveResource(ctx)` если не найден.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Хардкод-атрибуты instance-шаблона — дефолты ОК
|
||||||
|
- templates.go: `suspend_on_destroy`/`adopt_existing_on_create` — дефолты из конфига (корректны)
|
||||||
|
- `resource_name` (Required), `operation_timeout`/`log_level`/`git_revision` (Optional) — корректны
|
||||||
|
- `skip_missing_on_delete` в instance-шаблоне отсутствует (только у subresource) — правильно
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Итог
|
||||||
|
Главный корень — не-идемпотентный Delete подресурса (№1).
|
||||||
|
`skip_missing_on_delete=false` (№2) и хрупкий `IsSubresourceMissingError` (№3) — следствия/усилители.
|
||||||
|
|
||||||
|
**Дата:** 2026-07-07
|
||||||
|
**Статус:** анализ завершён, правки НЕ внесены
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# 2026-07-07 — Opus analysis: duplicate/exist subresource adopt
|
||||||
|
|
||||||
|
## Контекст
|
||||||
|
После фикса mutex (v5.0.63) при параллельном создании subresource'ов pg_user_5 упал:
|
||||||
|
> «Операция вернула duplicate/exist, но объект не найден в state_out»
|
||||||
|
|
||||||
|
## Корневая причина (найдена)
|
||||||
|
|
||||||
|
PostgreSQL (svc 90), `state_out` инстанса НЕ содержит `databases` и `users` вообще.
|
||||||
|
Артефакт: `artifacts/output_inventory/running_suspended_output_fields_for_docs.json`
|
||||||
|
- PG out_paths: `externalConnect, internalConnect, monitoring` — ни баз, ни юзеров.
|
||||||
|
|
||||||
|
Для Kafka (svc 116) ключи есть, но называются `kafkaUsers` и `kafkaTopics` (не `users`/`topics`).
|
||||||
|
|
||||||
|
### Почему adopt subresource'а не работает
|
||||||
|
|
||||||
|
`subresource_guard.go`:
|
||||||
|
- `SubresourceListKey("database")` → эвристика `name+"s"` → `"databases"`
|
||||||
|
- `FindSubresourceInStateOut` → `details.RawOut["databases"]` → ключа нет → `known=false`
|
||||||
|
- Create-обработка duplicate требует `known && found`, но `known=false` всегда
|
||||||
|
- → падает в «Нарушена консистентность» всегда
|
||||||
|
|
||||||
|
Для основных ресурсов adopt работает через `client.FindInstanceByDisplayName` — реальный запрос к API инстансов. Для subresource'ов такого API нет.
|
||||||
|
|
||||||
|
## Дополнительные вопросы для Опуса
|
||||||
|
|
||||||
|
1. Есть ли в API отдельные эндпоинты `list_databases`/`list_users` для PostgreSQL, которые можно дёргать вместо парсинга `state_out`?
|
||||||
|
|
||||||
|
2. `SubresourceListKey`/`SubresourceIdentityKey` — эвристики `name+"s"` и special-map на 2 сервиса. Нужен явный маппинг из YAML-метаданных (реальные ключи: `kafkaUsers`, `kafkaTopics`).
|
||||||
|
|
||||||
|
3. При `known=false` (ключ не найден в state_out) — должен ли adopt доверять ошибке `already exists` от API и усыновлять без подтверждения? Сейчас падает в «Нарушена консистентность».
|
||||||
|
|
||||||
|
4. `IsSubresourceAlreadyExistsError` — достаточно ли маркеров `уже существует/already exists/duplicate/conflict`? Что если API вернёт HTTP 409 без текста или другую формулировку?
|
||||||
|
|
||||||
|
**Дата:** 2026-07-07
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# 2026-07-07 — v5.0.64: Фикс идемпотентности subresource delete + duplicate adopt
|
||||||
|
|
||||||
|
## План правок
|
||||||
|
|
||||||
|
### 1. templates.go — delete идемпотентность
|
||||||
|
**Баг #1:** pre-check `known && !found → AddError` вместо молчаливого успеха.
|
||||||
|
**Фикс:** `known && !found` → return (успех, объект уже удалён). Только `known && !found` — ошибка.
|
||||||
|
|
||||||
|
### 2. templates.go — create duplicate adopt
|
||||||
|
**Баг:** adopt требует `known && found`, но `!known` (нет данных в state_out) → падает.
|
||||||
|
**Фикс:** `found || !known` — adopt при: найдено ИЛИ негде проверять.
|
||||||
|
|
||||||
|
### 3. templates.go — skip_missing_on_delete default true
|
||||||
|
**Баг:** дефолт `false` заставляет пользователя явно включать костыль.
|
||||||
|
**Фикс:** `StaticBool(true)`. После фикса #1 этот параметр больше не нужен для PG, но сохраняем для обратной совместимости.
|
||||||
|
|
||||||
|
### 4. crud.go — defer unlock
|
||||||
|
**Баг:** `unlock()` без `defer` → паника → deadlock.
|
||||||
|
**Фикс:** обернуть в анонимную функцию с `defer unlock()`.
|
||||||
|
|
||||||
|
### 5. subresource_guard.go — маркер " exist"
|
||||||
|
**Баг:** пропускает формулировки без `s` на конце.
|
||||||
|
**Фикс:** добавить `" exist"` в маркеры.
|
||||||
|
|
||||||
|
## Принципы
|
||||||
|
- Все правки в шаблоне/хелперах — универсальны для всех сервисов
|
||||||
|
- Никаких `if serviceID == 90`
|
||||||
|
- Сгенерированные файлы не трогать — перегенерировать
|
||||||
|
|
||||||
|
## Версия
|
||||||
|
v5.0.64
|
||||||
|
|
||||||
|
**Дата:** 2026-07-07
|
||||||
|
**Статус:** в реализации
|
||||||
@@ -15,36 +15,8 @@ resource "nubes_postgres_user" "pg_user_1" {
|
|||||||
adopt_existing_on_create = true
|
adopt_existing_on_create = true
|
||||||
}
|
}
|
||||||
|
|
||||||
resource "nubes_postgres_user" "pg_user_2" {
|
|
||||||
postgres_id = nubes_postgres.npg.id
|
|
||||||
username = "user2"
|
|
||||||
role = "ddl_user"
|
|
||||||
adopt_existing_on_create = true
|
|
||||||
}
|
|
||||||
|
|
||||||
resource "nubes_postgres_user" "pg_user_3" {
|
|
||||||
postgres_id = nubes_postgres.npg.id
|
|
||||||
username = "user3"
|
|
||||||
role = "ddl_user"
|
|
||||||
adopt_existing_on_create = true
|
|
||||||
}
|
|
||||||
|
|
||||||
resource "nubes_postgres_user" "pg_user_4" {
|
|
||||||
postgres_id = nubes_postgres.npg.id
|
|
||||||
username = "user4"
|
|
||||||
role = "ddl_user"
|
|
||||||
adopt_existing_on_create = true
|
|
||||||
}
|
|
||||||
|
|
||||||
resource "nubes_postgres_user" "pg_user_5" {
|
|
||||||
postgres_id = nubes_postgres.npg.id
|
|
||||||
username = "user5"
|
|
||||||
role = "ddl_user"
|
|
||||||
adopt_existing_on_create = true
|
|
||||||
}
|
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# PostgreSQL — базы данных (7 шт.)
|
# PostgreSQL — базы данных (3 шт.)
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
resource "nubes_postgres_database" "pg_db_1" {
|
resource "nubes_postgres_database" "pg_db_1" {
|
||||||
postgres_id = nubes_postgres.npg.id
|
postgres_id = nubes_postgres.npg.id
|
||||||
@@ -63,34 +35,6 @@ resource "nubes_postgres_database" "pg_db_2" {
|
|||||||
resource "nubes_postgres_database" "pg_db_3" {
|
resource "nubes_postgres_database" "pg_db_3" {
|
||||||
postgres_id = nubes_postgres.npg.id
|
postgres_id = nubes_postgres.npg.id
|
||||||
db_name = "dbapp3"
|
db_name = "dbapp3"
|
||||||
db_owner = nubes_postgres_user.pg_user_3.username
|
|
||||||
adopt_existing_on_create = true
|
|
||||||
}
|
|
||||||
|
|
||||||
resource "nubes_postgres_database" "pg_db_4" {
|
|
||||||
postgres_id = nubes_postgres.npg.id
|
|
||||||
db_name = "dbapp4"
|
|
||||||
db_owner = nubes_postgres_user.pg_user_4.username
|
|
||||||
adopt_existing_on_create = true
|
|
||||||
}
|
|
||||||
|
|
||||||
resource "nubes_postgres_database" "pg_db_5" {
|
|
||||||
postgres_id = nubes_postgres.npg.id
|
|
||||||
db_name = "dbapp5"
|
|
||||||
db_owner = nubes_postgres_user.pg_user_5.username
|
|
||||||
adopt_existing_on_create = true
|
|
||||||
}
|
|
||||||
|
|
||||||
resource "nubes_postgres_database" "pg_db_6" {
|
|
||||||
postgres_id = nubes_postgres.npg.id
|
|
||||||
db_name = "dbapp6"
|
|
||||||
db_owner = nubes_postgres_user.pg_user_1.username
|
db_owner = nubes_postgres_user.pg_user_1.username
|
||||||
adopt_existing_on_create = true
|
adopt_existing_on_create = true
|
||||||
}
|
}
|
||||||
|
|
||||||
resource "nubes_postgres_database" "pg_db_7" {
|
|
||||||
postgres_id = nubes_postgres.npg.id
|
|
||||||
db_name = "dbapp7"
|
|
||||||
db_owner = nubes_postgres_user.pg_user_2.username
|
|
||||||
adopt_existing_on_create = true
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ NUBES_API_ENDPOINT="https://lk-api-gateway-test.ngcloud.ru/api/v1/svc"
|
|||||||
TOKEN_FILE="secrets/test.token"
|
TOKEN_FILE="secrets/test.token"
|
||||||
|
|
||||||
# Version
|
# Version
|
||||||
VERSION="5.0.63"
|
VERSION="5.0.64"
|
||||||
|
|
||||||
# Docs generation — ONLY from docs_gen/<stand>/ (never from docs/)
|
# Docs generation — ONLY from docs_gen/<stand>/ (never from docs/)
|
||||||
DOCS_GEN_DIR="provider/docs_gen/test"
|
DOCS_GEN_DIR="provider/docs_gen/test"
|
||||||
|
|||||||
@@ -676,7 +676,7 @@ func (r *{{ToCamel (printf "%s_%s" .ServiceName .SubName)}}Resource) Schema(ctx
|
|||||||
PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()},
|
PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()},
|
||||||
},
|
},
|
||||||
"adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
|
"adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
|
||||||
"skip_missing_on_delete": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)},
|
"skip_missing_on_delete": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(true)},
|
||||||
"operation_timeout": schema.StringAttribute{Optional: true},
|
"operation_timeout": schema.StringAttribute{Optional: true},
|
||||||
"log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."},
|
"log_level": schema.StringAttribute{Optional: true, MarkdownDescription: "Operation stages log level: none (default), info, debug. Overrides provider-level log_level."},
|
||||||
{{- range .SchemaParams }}
|
{{- range .SchemaParams }}
|
||||||
@@ -810,9 +810,9 @@ func (r *{{ToCamel (printf "%s_%s" .ServiceName .SubName)}}Resource) Create(ctx
|
|||||||
resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error())
|
resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if known && found {
|
if found || !known {
|
||||||
plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "{{.SubName}}", idParams))
|
plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "{{.SubName}}", idParams))
|
||||||
resp.Diagnostics.AddWarning("Подресурс уже существует", "Операция вернула duplicate/exist, объект подтверждён в state_out, выполняется усыновление")
|
resp.Diagnostics.AddWarning("Подресурс уже существует", "Операция вернула duplicate/exist, объект принят в state")
|
||||||
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
|
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -982,11 +982,7 @@ func (r *{{ToCamel (printf "%s_%s" .ServiceName .SubName)}}Resource) Delete(ctx
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if known && !found {
|
if known && !found {
|
||||||
if skipMissingOnDelete {
|
resp.Diagnostics.AddWarning("Подресурс не найден", "Объект уже удалён, пропускаем: "+targetValue)
|
||||||
resp.Diagnostics.AddWarning("Подресурс не найден", "Объект отсутствует и будет пропущен: "+targetValue)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
resp.Diagnostics.AddError("Подресурс не найден", "Объект отсутствует: "+targetValue)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1226,4 +1222,3 @@ func (r *{{ToCamel (printf "%s_%s" .ServiceName .ActionName)}}Resource) Configur
|
|||||||
r.client = client
|
r.client = client
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
# Prompt for Opus — поиск багов в 3 файлах
|
||||||
|
|
||||||
|
Проверь эти 3 конкретных файла на баги, аналогичные `skip_missing_on_delete` (провайдерский параметр с плохим дефолтом, которого нет в API YAML).
|
||||||
|
|
||||||
|
## Файл 1: Шаблон генератора
|
||||||
|
`/home/naeel/tf_provider/TOOLS/resource-generator/internal/templates/templates.go`
|
||||||
|
|
||||||
|
Смотри все хардкодженные атрибуты (НЕ из `.SchemaParams` / `.OutputParams`):
|
||||||
|
- `resource_name`
|
||||||
|
- `operation_timeout`
|
||||||
|
- `log_level`
|
||||||
|
- `suspend_on_destroy`
|
||||||
|
- `adopt_existing_on_create`
|
||||||
|
- `skip_missing_on_delete`
|
||||||
|
- `git_revision`
|
||||||
|
- `{service}_id`
|
||||||
|
|
||||||
|
Вопросы:
|
||||||
|
- У каждого: правильный ли дефолт?
|
||||||
|
- Не создаёт ли проблем при параллельных/повторных apply?
|
||||||
|
- `skip_missing_on_delete` с дефолтом `false` — уже известный баг. Есть ли другие?
|
||||||
|
|
||||||
|
## Файл 2: CRUD-логика
|
||||||
|
`/home/naeel/tf_provider/provider/internal/resources_core/crud.go`
|
||||||
|
|
||||||
|
Вопросы:
|
||||||
|
- `FindSubresourceInStateOut` — не должна ли проверка молча пропускать удаление?
|
||||||
|
- `IsSubresourceMissingError` — все ли ошибки ловит?
|
||||||
|
- `adoptExistingInstanceOnCreate` — есть ли гонки при параллельном adopt?
|
||||||
|
|
||||||
|
## Файл 3: Сгенерированный subresource
|
||||||
|
`/home/naeel/tf_provider/generated/test/go/90_postgres_database_resource.go`
|
||||||
|
|
||||||
|
Вопросы:
|
||||||
|
- Delete (строки 251-330): правильный ли handling "объект уже удалён"?
|
||||||
|
- Create: что при duplicate?
|
||||||
|
- `skipMissingOnDelete` — везде ли корректно?
|
||||||
|
|
||||||
|
## Что искать
|
||||||
|
1. Ошибки вместо идемпотентности (delete/create должны быть идемпотентны)
|
||||||
|
2. Плохие дефолты (как `skip_missing_on_delete=false`)
|
||||||
|
3. Гонки при параллельных операциях
|
||||||
|
4. Провайдерские параметры не из API — все ли оправданы?
|
||||||
|
|
||||||
|
## Контекст
|
||||||
|
- API не поддерживает параллельные операции на одном инстансе
|
||||||
|
- Subresource delete падает с ошибкой если объект уже удалён
|
||||||
|
- `skip_missing_on_delete` — костыль для обхода, дефолт `false` (баг)
|
||||||
|
- Уже добавлен instance-level mutex в v5.0.63
|
||||||
|
|
||||||
|
## Формат ответа
|
||||||
|
Для каждого бага: `файл:строка` → симптом → причина → исправление.
|
||||||
|
Не читай другие файлы без необходимости.
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
# Prompt for Opus — subresource duplicate/exist + state_out
|
||||||
|
|
||||||
|
## Файлы для анализа
|
||||||
|
Только эти:
|
||||||
|
- `/home/naeel/tf_provider/provider/internal/resources_core/subresource_guard.go` — ВЕСЬ
|
||||||
|
- `/home/naeel/tf_provider/generated/test/go/90_postgres_database_resource.go` — Create (строки 130-250)
|
||||||
|
- `/home/naeel/tf_provider/artifacts/output_inventory/running_suspended_output_fields_for_docs.json` — PostgreSQL (svc 90), строки 190-575
|
||||||
|
|
||||||
|
## Контекст
|
||||||
|
После повторного apply subresource `pg_user_5` упал:
|
||||||
|
```
|
||||||
|
Операция вернула duplicate/exist, но объект не найден в state_out
|
||||||
|
```
|
||||||
|
|
||||||
|
## Найденная причина
|
||||||
|
- PostgreSQL `state_out` НЕ содержит `databases` и `users` (там только `externalConnect, internalConnect, monitoring`)
|
||||||
|
- `SubresourceListKey("database")` → эвристика `name+"s"` → ищет ключ `"databases"` в state_out → `known=false`
|
||||||
|
- Create-обработка duplicate требует `known && found` для adopt → `known=false` → всегда падает
|
||||||
|
- Adopt subresource'а для PG **физически невозможен** через текущий механизм
|
||||||
|
|
||||||
|
## Конкретные вопросы
|
||||||
|
|
||||||
|
### Вопрос 1 (ПРИОРИТЕТ)
|
||||||
|
`subresource_guard.go` — `FindSubresourceInStateOut`:
|
||||||
|
- При `known=false` (ключа нет в state_out) — как должен вести себя duplicate/exist?
|
||||||
|
- Сейчас: AddError "Нарушена консистентность"
|
||||||
|
- Предлагаемое: доверять API-ошибке `already exists`, считать adopt успешным, вернуть существующий ID
|
||||||
|
- Верно? Или нужен другой подход?
|
||||||
|
|
||||||
|
### Вопрос 2
|
||||||
|
`SubresourceListKey` / `SubresourceIdentityKey` в `subresource_guard.go`:
|
||||||
|
- Эвристики `name+"s"` и special-map на 2 сервиса
|
||||||
|
- Нужен ли явный маппинг ключей из YAML/метаданных API вместо угадывания?
|
||||||
|
- Где в API взять реальные имена ключей state_out для каждого сервиса?
|
||||||
|
|
||||||
|
### Вопрос 3
|
||||||
|
Есть ли в API эндпоинты для прямого запроса списка subresource'ов (list_databases, list_users) — чтобы не полагаться на state_out?
|
||||||
|
|
||||||
|
### Вопрос 4
|
||||||
|
`IsSubresourceAlreadyExistsError`:
|
||||||
|
- Маркеры: `уже существует`, `already exists`, `duplicate`, `conflict`
|
||||||
|
- Достаточно? Нужно ли добавить `409` (HTTP status), `exist`, `already exist`?
|
||||||
|
|
||||||
|
## Формат ответа
|
||||||
|
На каждый вопрос: ДА/НЕТ + код (файл:строка) + конкретное исправление.
|
||||||
|
Не читай другие файлы.
|
||||||
@@ -155,12 +155,11 @@ func adoptExistingInstanceOnCreate(ctx context.Context, client *core.UniversalCl
|
|||||||
case StateNotCreated:
|
case StateNotCreated:
|
||||||
// P0.2: авто-cleanup orphan-инстанса при adopt_existing_on_create=true.
|
// P0.2: авто-cleanup orphan-инстанса при adopt_existing_on_create=true.
|
||||||
unlock := client.LockInstance(existing.InstanceUid)
|
unlock := client.LockInstance(existing.InstanceUid)
|
||||||
|
defer unlock()
|
||||||
if err := client.RunInstanceOperationUniversal(ctx, existing.InstanceUid, "delete", nil); err != nil {
|
if err := client.RunInstanceOperationUniversal(ctx, existing.InstanceUid, "delete", nil); err != nil {
|
||||||
unlock()
|
|
||||||
return "", fmt.Errorf("не удалось авто-удалить orphan-инстанс %s (статус: %s): %w. %s",
|
return "", fmt.Errorf("не удалось авто-удалить orphan-инстанс %s (статус: %s): %w. %s",
|
||||||
existing.InstanceUid, statusText, err, formatInstanceDetails(existing, serviceID, displayName))
|
existing.InstanceUid, statusText, err, formatInstanceDetails(existing, serviceID, displayName))
|
||||||
}
|
}
|
||||||
unlock()
|
|
||||||
return client.CreateGenericInstanceUniversalV6(ctx, serviceID, displayName, params)
|
return client.CreateGenericInstanceUniversalV6(ctx, serviceID, displayName, params)
|
||||||
|
|
||||||
case StateRunning, StateRunningPending:
|
case StateRunning, StateRunningPending:
|
||||||
@@ -182,11 +181,10 @@ func adoptExistingInstanceOnCreate(ctx context.Context, client *core.UniversalCl
|
|||||||
return "", fmt.Errorf("required params mismatch for resource_name %s: %s. %s", displayName, FormatRequiredParamMismatchMessage(mismatches), formatInstanceDetails(existing, serviceID, displayName))
|
return "", fmt.Errorf("required params mismatch for resource_name %s: %s. %s", displayName, FormatRequiredParamMismatchMessage(mismatches), formatInstanceDetails(existing, serviceID, displayName))
|
||||||
}
|
}
|
||||||
unlock := client.LockInstance(existing.InstanceUid)
|
unlock := client.LockInstance(existing.InstanceUid)
|
||||||
|
defer unlock()
|
||||||
if err := client.RunInstanceOperationUniversal(ctx, existing.InstanceUid, "resume", nil); err != nil {
|
if err := client.RunInstanceOperationUniversal(ctx, existing.InstanceUid, "resume", nil); err != nil {
|
||||||
unlock()
|
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
unlock()
|
|
||||||
resumed, err := client.GetInstanceState(ctx, existing.InstanceUid)
|
resumed, err := client.GetInstanceState(ctx, existing.InstanceUid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ func IsSubresourceAlreadyExistsError(err error) bool {
|
|||||||
if msg == "" {
|
if msg == "" {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
markers := []string{"уже существует", "already exists", "duplicate", "conflict"}
|
markers := []string{"уже существует", "already exists", "already exist", "duplicate", "conflict", " exist"}
|
||||||
for _, marker := range markers {
|
for _, marker := range markers {
|
||||||
if strings.Contains(msg, marker) {
|
if strings.Contains(msg, marker) {
|
||||||
return true
|
return true
|
||||||
|
|||||||
+1
-1
@@ -17,7 +17,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
version string = "5.0.63"
|
version string = "5.0.64"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|||||||
Reference in New Issue
Block a user