diff --git a/HISTORY/OPUS/3006_2_scripts_questions.md b/HISTORY/OPUS/3006_2_scripts_questions.md new file mode 100644 index 0000000..66eacd7 --- /dev/null +++ b/HISTORY/OPUS/3006_2_scripts_questions.md @@ -0,0 +1,174 @@ +# Вопросы к Opus 4.8: Анализ генерационных скриптов и промежуточного кода + +> **Контекст:** Ты уже проанализировал ПРОВАЙДЕР (client.go, crud.go, gen_v2). Теперь нужен анализ СКРИПТОВ ГЕНЕРАЦИИ — как API-данные превращаются в YAML, Go-код и документацию. +> **Режим:** Plan (только чтение). Все пути относительно `/home/naeel/tf_provider/`. + +--- + +## Вопрос 1: Bash-скрипты — полный аудит + +**Файлы:** +- `devops/01_generate_yamls.sh` — API → YAML +- `devops/02_generate_resources_and_docs_v2.sh` — YAML → Go + Docs +- `devops/03_build_and_upload_provider.sh` — сборка + S3 +- `devops/04_build_and_publish_docs.sh` — публикация документации + +**Задача:** прочитай ВСЕ 4 скрипта полностью. Для каждого проверь: + +1. **Обработка ошибок:** + - Что происходит при `set -e` — на каком шаге скрипт упадёт? + - Есть ли `trap` для очистки временных файлов? + - Что если API вернул 500? Пустой ответ? Битый JSON? + - Что если `services_list.txt` пустой? + +2. **Зависимости:** + - Какие внешние инструменты нужны (go, python3, mc, gpg, mkdocs)? + - Проверяются ли они перед запуском? + - Что если инструмент отсутствует — понятная ли ошибка? + +3. **Идемпотентность:** + - Можно ли перезапустить скрипт при обрыве? + - Чистит ли он предыдущий мусор перед генерацией? + - Что с параллельным запуском двух экземпляров? + +4. **User-Agent / DDoS-Guard:** + - `01_generate_yamls.sh` вызывает python `urllib` для получения имён сервисов (строка ~170). Есть ли там User-Agent? + - Как Go-бинари (`service_spec_gen`) получают токен? Через env? + +5. **Профили (test/prod/dev):** + - Как скрипты определяют какой профиль использовать? + - Что если `--profile` не указан? Есть ли защита от запуска без профиля? + - Где хранятся сгенерированные артефакты для каждого профиля? + +--- + +## Вопрос 2: service_spec_gen — API → YAML (ключевой генератор) + +**Файл:** `universal_rebuild/tools/service_spec_gen/generate_service_spec.go` + +Мы уже добавили User-Agent, но проверь остальное: + +1. **HTTP-клиент:** + - Есть ли retry при ошибках API (429, 503, network)? + - Какой таймаут? `http.Client{Timeout: 30s}` — достаточно ли? + - Используется ли `http.DefaultClient` или свой? + +2. **Парсинг ответа API:** + - Структуры `serviceInfo`, `serviceOperationInfo`, `operationInfo` — все ли поля API маппятся? + - Есть ли поля которые API возвращает, но генератор игнорирует (потеря данных)? + - Что если API вернёт неожиданный формат — будет `panic` или ошибка? + +3. **Классификация операций (`classifyOperation`):** + - Как определяется `kind` (instance/subresource/action)? + - Правило: `create/modify/delete/suspend/resume` → instance, `create_*/modify_*/delete_*` → subresource, остальное → action. Все ли кейсы покрыты? + - Что с новыми префиксами, которые могут появиться (например, `restart_*`)? + +4. **Сбор параметров:** + - Поля `ParamSpec` — все ли constraints из API сохраняются (`maxLength`, `minLength`, `regex`, `uniqueScope`, `dependsOn`)? + - `normalizeDefault` — правильно ли обрабатывает null/числа/строки? + - `normalizeValueList` — преобразует `[]interface{}` в `[]string`. Что если элемент не строка? + +5. **Выходной YAML:** + - `yaml.Marshal` — сохраняет ли все поля корректно (omitempty, вложенные структуры)? + - Имя файла: `{id}_{name}.yaml`. Что если имя содержит спецсимволы? + +--- + +## Вопрос 3: gen_v2 — YAML → Go (уже проанализирован, но проверь детали) + +**Файл:** `universal_rebuild/tools/gen_v2/generate_resources_v2.go` + +Мы уже добавили `validateSpec()`. Проверь: + +1. **Обработка ошибок генерации:** + - `format.Source` — что при ошибке форматирования? Пишет битый код или паникует? + - `writeInstanceResource` / `writeSubresource` / `writeActionResource` — обрабатывают ли ошибки `template.Execute`? + +2. **Вычисление CreateOnly:** + - `computeCreateOnly` = параметр в create но не в modify → ForceNew. Что если modify-операция существует но не содержит этот параметр потому что он неизменяем? Это корректно? + +3. **Subresource identity:** + - Если нет modify → все параметры ForceNew. Если есть modify → как определяется identity? + - `buildSubresourceForceNewCodes` — правильная ли логика? + +4. **JSON-параметры:** + - `data_type: json` → `IsJson=true` + `JsonNormalize` plan-modifier. Достаточно ли этого для всех JSON-типов (map, array, object)? + +--- + +## Вопрос 4: docs_template_gen_v2 — генерация документации + +**Файлы:** `universal_rebuild/tools/docs_template_gen_v2/main.go` + +1. **Что именно генерируется?** + - Какие страницы/файлы создаются? + - Формат выхода — Markdown для mkdocs? + - Все ли поля из YAML используются (MAN, params, outputs, lifecycle)? + +2. **Покрытие:** + - Генерируется ли документация для subresource и action ресурсов? + - Есть ли index/overview страницы? + - Примеры (HCL examples) — откуда берутся? + +3. **Ошибки:** + - Что при битом YAML? Пустых операциях? + - Флаг `-exclude clickhouse` — почему хардкод? + +--- + +## Вопрос 5: service_ops_gen и service_params_gen — нужны ли они? + +**Файлы:** +- `universal_rebuild/tools/service_ops_gen/generate_service_ops.go` +- `universal_rebuild/tools/service_params_gen/generate_service_params.go` + +Эти тулы выглядят как **устаревшие** (в коде написано APPEND-ONLY). `service_spec_gen` заменил их (генерирует унифицированный YAML вместо отдельных ops/params YAML). + +1. Используются ли они где-то в скриптах? +2. Можно ли их удалить? +3. Есть ли `gen/` (v1) — он ещё нужен? + +--- + +## Вопрос 6: embed.go и resources_yaml + +**Файлы:** +- `universal_rebuild/resources_yaml/embed.go` +- `devops/profiles/*/generated/resources_yaml/embed.go` + +1. `embed.go` использует `//go:embed *.yaml` — встраивает YAML в бинарник. Зачем? +2. Где в коде провайдера используется `resources_yaml.Files`? +3. Почему `embed.go` создаётся bash-скриптом, а не лежит в репо? + +--- + +## Вопрос 7: Профили и CI + +**Файлы:** +- `devops/profiles/test/`, `prod/`, `dev/` +- `devops/ci/pipeline.yaml` +- `.github/workflows/` + +1. Как CI-пайплайн связан с профилями? +2. Есть ли автоматический прогон генерации в CI? +3. Как происходит деплой — через operator в K8s или вручную? +4. `devops/10_yaml_stability_run.sh` — зачем несколько скриптов стабильности (10-13)? + +--- + +## Вопрос 8: Практическая проверка + +Мы сегодня попробовали сгенерировать YAML для postgres (service_id=90) и обнаружили что API добавил 2 новые операции (`backup`, `reconcile`) которых нет в старом сгенерированном коде. + +**Задача:** проверь ВСЕ сервисы из `services_list.txt` на наличие YAML в `devops/profiles/test/generated/resources_yaml/`. Какие сервисы есть в списке но отсутствуют в сгенерированных YAML? Какие YAML есть но сервис исключён из списка? + +--- + +## Формат ответа + +На каждый вопрос: **что прочитал → проблема → предложение → приоритет (P0/P1/P2)**. + +Особое внимание: +- Bash-скриптам (01_generate_yamls.sh — самый критичный) +- HTTP-клиентам в Go-генераторах (UA, retry, таймауты) +- Целостности данных (не теряем ли поля API при генерации YAML) diff --git a/universal_rebuild/internal/resources_gen/90_postgres_backup_resource.go b/universal_rebuild/internal/resources_gen/90_postgres_backup_resource.go new file mode 100644 index 0000000..83c0223 --- /dev/null +++ b/universal_rebuild/internal/resources_gen/90_postgres_backup_resource.go @@ -0,0 +1,330 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: postgres +// Subresource: backup + +var _ resource.Resource = &PostgresBackupResource{} + +// PostgresBackupResource manages a subresource via operations. +type PostgresBackupResource struct { + client *core.UniversalClient +} + +type PostgresBackupModel struct { + ID types.String `tfsdk:"id"` + PostgresID types.String `tfsdk:"postgres_id"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + SkipMissingOnDelete types.Bool `tfsdk:"skip_missing_on_delete"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` +} + +func NewPostgresBackupResource() resource.Resource { + return &PostgresBackupResource{} +} + +func (r *PostgresBackupResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_postgres_backup" +} + +func (r *PostgresBackupResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, + "postgres_id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + }, + "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)}, + "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."}, + } + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *PostgresBackupResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan PostgresBackupModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + instanceUID := strings.TrimSpace(plan.PostgresID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + adoptExistingOnCreate := !plan.AdoptExistingOnCreate.IsNull() && !plan.AdoptExistingOnCreate.IsUnknown() && plan.AdoptExistingOnCreate.ValueBool() + + idParams := map[string]string{} + identityCodes := []string{} + listKey, idKey := resources_core.ResolveSubresourceStateKeys("backup", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + if adoptExistingOnCreate { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "backup", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Объект уже есть, выполняется усыновление: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + resp.Diagnostics.AddError("Подресурс уже существует", "Найден объект с таким именем: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{}) + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + createErr := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_backup", params, operationTimeout) + if createErr != nil && resources_core.IsSubresourceTransientDependencyError(createErr) { + createErr = resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "create_backup", params, operationTimeout) + } + if createErr != nil { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "backup", idParams)) + resp.Diagnostics.AddWarning("Подресурс подтверждён в state_out", "Операция create вернула ошибку, но объект найден в state_out и принят в state: "+targetValue) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + if adoptExistingOnCreate && resources_core.IsSubresourceAlreadyExistsError(createErr) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "backup", idParams)) + resp.Diagnostics.AddWarning("Подресурс уже существует", "Операция вернула duplicate/exist, объект подтверждён в state_out, выполняется усыновление") + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + } + resp.Diagnostics.AddError("Нарушена консистентность", "Операция вернула duplicate/exist, но объект не найден в state_out") + return + } + resp.Diagnostics.AddError("Ошибка клиента", createErr.Error()) + return + } + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция create завершилась успешно, но объект не найден в state_out: "+targetValue) + return + } + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "backup", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *PostgresBackupResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state PostgresBackupModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *PostgresBackupResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan PostgresBackupModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + if "" == "" { + var state PostgresBackupModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if plan.ID.IsNull() || plan.ID.IsUnknown() { + plan.ID = state.ID + } + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + return + } + + instanceUID := strings.TrimSpace(plan.PostgresID.ValueString()) + if instanceUID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра") + return + } + + params := resources_core.CompactParams(map[string]string{}) + + idParams := map[string]string{} + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "", params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = types.StringValue(resources_core.BuildSubresourceID(instanceUID, "backup", idParams)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *PostgresBackupResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state PostgresBackupModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + if "" == "" { + return + } + + instanceUID := strings.TrimSpace(state.PostgresID.ValueString()) + if instanceUID == "" { + return + } + skipMissingOnDelete := !state.SkipMissingOnDelete.IsNull() && !state.SkipMissingOnDelete.IsUnknown() && state.SkipMissingOnDelete.ValueBool() + + idParams := map[string]string{} + identityCodes := []string{} + listKey, idKey := resources_core.ResolveSubresourceStateKeys("backup", identityCodes) + targetValue := strings.TrimSpace(idParams[idKey]) + if targetValue == "" { + for _, code := range identityCodes { + candidate := strings.TrimSpace(idParams[code]) + if candidate == "" { + continue + } + targetValue = candidate + idKey = code + break + } + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && !found { + if skipMissingOnDelete { + resp.Diagnostics.AddWarning("Подресурс не найден", "Объект отсутствует и будет пропущен: "+targetValue) + return + } + resp.Diagnostics.AddError("Подресурс не найден", "Объект отсутствует: "+targetValue) + return + } + } + + params := resources_core.CompactParams(map[string]string{}) + + operationTimeout := "" + if !state.OperationTimeout.IsNull() && !state.OperationTimeout.IsUnknown() { + operationTimeout = state.OperationTimeout.ValueString() + } + if !state.LogLevel.IsNull() && !state.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, state.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "", params, operationTimeout); err != nil { + if resources_core.IsSubresourceMissingError(err) { + if targetValue != "" { + found, known, checkErr := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if checkErr != nil { + resp.Diagnostics.AddError("Ошибка клиента", checkErr.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete вернула not found, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } + if skipMissingOnDelete { + resp.Diagnostics.AddWarning("Подресурс не найден", "Операция delete вернула not found, объект отсутствует в state_out, удаление пропущено") + return + } + } + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + if targetValue != "" { + found, known, err := resources_core.FindSubresourceInStateOut(ctx, r.client, instanceUID, listKey, idKey, targetValue) + if err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + if known && found { + resp.Diagnostics.AddError("Нарушена консистентность", "Операция delete завершилась успешно, но объект всё ещё присутствует в state_out: "+targetValue) + return + } + } +} + +func (r *PostgresBackupResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +} diff --git a/universal_rebuild/internal/resources_gen/90_postgres_reconcile_action.go b/universal_rebuild/internal/resources_gen/90_postgres_reconcile_action.go new file mode 100644 index 0000000..5a9e4f6 --- /dev/null +++ b/universal_rebuild/internal/resources_gen/90_postgres_reconcile_action.go @@ -0,0 +1,136 @@ +package resources_gen + +import ( + "context" + "strings" + + "terraform-provider-nubes/internal/core" + "terraform-provider-nubes/internal/resources_core" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Code generated by tools/gen_v2. DO NOT EDIT. +// Service: postgres +// Action: reconcile + +var _ resource.Resource = &PostgresReconcileResource{} + +// PostgresReconcileResource triggers a one-time action. +type PostgresReconcileResource struct { + client *core.UniversalClient +} + +type PostgresReconcileModel struct { + ID types.String `tfsdk:"id"` + PostgresID types.String `tfsdk:"postgres_id"` + RunID types.String `tfsdk:"run_id"` + AdoptExistingOnCreate types.Bool `tfsdk:"adopt_existing_on_create"` + OperationTimeout types.String `tfsdk:"operation_timeout"` + LogLevel types.String `tfsdk:"log_level"` +} + +func NewPostgresReconcileResource() resource.Resource { + return &PostgresReconcileResource{} +} + +func (r *PostgresReconcileResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_postgres_reconcile" +} + +func (r *PostgresReconcileResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + attrs := map[string]schema.Attribute{ + "id": schema.StringAttribute{Computed: true}, + "postgres_id": schema.StringAttribute{Required: true}, + "run_id": schema.StringAttribute{Required: true}, + "adopt_existing_on_create": schema.BoolAttribute{Optional: true, Computed: true, Default: booldefault.StaticBool(false)}, + "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."}, + } + resp.Schema = schema.Schema{Attributes: attrs} +} + +func (r *PostgresReconcileResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan PostgresReconcileModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + instanceUID := strings.TrimSpace(plan.PostgresID.ValueString()) + runID := strings.TrimSpace(plan.RunID.ValueString()) + if instanceUID == "" || runID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра или run_id") + return + } + + params := map[string]string{} + + operationTimeout := "" + if !plan.OperationTimeout.IsNull() && !plan.OperationTimeout.IsUnknown() { + operationTimeout = plan.OperationTimeout.ValueString() + } + if !plan.LogLevel.IsNull() && !plan.LogLevel.IsUnknown() { + ctx = core.CtxWithLogLevel(ctx, plan.LogLevel.ValueString()) + } + if err := resources_core.RunOperationByCodeWithTimeout(ctx, r.client, instanceUID, "reconcile", params, operationTimeout); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = types.StringValue(resources_core.BuildActionID(instanceUID, "reconcile", runID)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *PostgresReconcileResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state PostgresReconcileModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *PostgresReconcileResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan PostgresReconcileModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + instanceUID := strings.TrimSpace(plan.PostgresID.ValueString()) + runID := strings.TrimSpace(plan.RunID.ValueString()) + if instanceUID == "" || runID == "" { + resp.Diagnostics.AddError("Ошибка клиента", "отсутствует идентификатор экземпляра или run_id") + return + } + + params := map[string]string{} + + if err := resources_core.RunOperationByCode(ctx, r.client, instanceUID, "reconcile", params); err != nil { + resp.Diagnostics.AddError("Ошибка клиента", err.Error()) + return + } + + plan.ID = types.StringValue(resources_core.BuildActionID(instanceUID, "reconcile", runID)) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *PostgresReconcileResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + // No-op: removing the resource does not trigger a remote action. +} + +func (r *PostgresReconcileResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*core.UniversalClient) + if !ok { + resp.Diagnostics.AddError("Error", "Invalid client type") + return + } + r.client = client +}