diff --git a/.gitignore b/.gitignore index 34ce865..3d13260 100644 --- a/.gitignore +++ b/.gitignore @@ -114,5 +114,6 @@ universal_rebuild/service_params_gen terraform-provider-mycloud artifacts/api-meta/*/errors.log TOOLS/bin/ +TOOLS/resource-generator/bin/ TOOLS/docs-generator/bin/ docs/30_registry/resources/ diff --git a/DEV_STAND/FullPipe/terraform.tfvars.example b/DEV_STAND/FullPipe/terraform.tfvars.example index ef1a073..a938d21 100644 --- a/DEV_STAND/FullPipe/terraform.tfvars.example +++ b/DEV_STAND/FullPipe/terraform.tfvars.example @@ -10,4 +10,4 @@ vdc_cpu_allocated = 8 vdc_cpu_guaranteed = 0 vdc_mem_allocated = 32 -vdc_storage_config = "[{\"name\":\"fast\",\"size\":200}]" +vdc_storage_config = "[{\"name\":\"SATA\",\"size\":\"200\"}]" diff --git a/README.md b/README.md index 6537ca9..ca59bb7 100644 --- a/README.md +++ b/README.md @@ -56,16 +56,20 @@ export TOKEN_FILE=/home/naeel/terra/08-33-41.token ## Step 2: Generate Go resources and docs -Script: `02_generate_resources_and_docs.sh` +Script: `TOOLS/scripts/02_generate_resources_and_docs_v2.sh` Example: ```bash -./02_generate_resources_and_docs.sh +./TOOLS/scripts/02_generate_resources_and_docs_v2.sh --profile TOOLS/config/dev ``` Outputs: -- Go files in `universal_rebuild/internal/resources_gen` -- Docs in `docs/30_registry/resources` +- Go files in `generated//go` +- Docs in `generated//docs` + +Important: +- The v2 script always rebuilds `resource-generator` and `docs-generator` from source before running. +- Do not invoke stale binaries from `TOOLS/resource-generator/bin/` or `TOOLS/docs-generator/bin/` directly. ## Step 3: Build and upload provider diff --git a/TOOLS/README.md b/TOOLS/README.md index e606e7f..cbe9a14 100644 --- a/TOOLS/README.md +++ b/TOOLS/README.md @@ -24,6 +24,25 @@ cd resource-generator && go build -o ../bin/resource-generator . Структура: `main.go` + `internal/{helpers,loader,params,templates,types,writers}`. +### Как запускать правильно + +Не запускайте `TOOLS/resource-generator/bin/resource-generator` вручную и не полагайтесь на старый бинарник из `TOOLS/resource-generator/bin/`. + +Используйте канонический скрипт из корня репозитория, он **всегда** пересобирает генераторы из текущих исходников перед запуском: + +```bash +./TOOLS/scripts/02_generate_resources_and_docs_v2.sh --profile TOOLS/config/dev +``` + +Для других стендов подставляйте нужный профиль: + +```bash +./TOOLS/scripts/02_generate_resources_and_docs_v2.sh --profile TOOLS/config/test +./TOOLS/scripts/02_generate_resources_and_docs_v2.sh --profile TOOLS/config/prod +``` + +Это устраняет случайный запуск устаревшего бинаря и гарантирует, что новые `kind` из YAML, включая `modifier`, будут обработаны текущим кодом генератора. + ## docs-generator `resources_yaml/*.yaml` → Markdown-документация в `docs/30_registry/resources/` diff --git a/TOOLS/resource-generator/bin/resource-generator b/TOOLS/resource-generator/bin/resource-generator deleted file mode 100755 index 3fa210c..0000000 Binary files a/TOOLS/resource-generator/bin/resource-generator and /dev/null differ diff --git a/TOOLS/resource-generator/internal/loader/loader.go b/TOOLS/resource-generator/internal/loader/loader.go index f914bb7..e298621 100644 --- a/TOOLS/resource-generator/internal/loader/loader.go +++ b/TOOLS/resource-generator/internal/loader/loader.go @@ -133,7 +133,10 @@ func LoadSpecs(dir string) ([]types.GenResource, []types.GenSubresource, []types gr.NeedsFmtImport = true } gr.NeedsJsonPlanMod = params.AnalyzeJsonPlanMod(gr.SchemaParams) - gr.NeedsStringsImport = params.HasRestoreCasingParams(gr.SchemaParams) || params.AnalyzeNeedsStrings(gr.ModifyParams) || params.AnalyzeNeedsStrings(gr.CreateOnlyRequiredParams) || params.AnalyzeNeedsStrings(gr.CreateOnlyParams) + // Строковый import нужен только для обычных строковых сравнений и + // обработок в шаблоне. Старый restore-хук для refSvc больше не + // генерируется, поэтому отдельный флаг под него не нужен. + gr.NeedsStringsImport = params.AnalyzeNeedsStrings(gr.ModifyParams) || params.AnalyzeNeedsStrings(gr.CreateOnlyRequiredParams) || params.AnalyzeNeedsStrings(gr.CreateOnlyParams) gr.NeedsFmtImport = params.HasNestedParams(gr.SchemaParams) // --- Action processing (ДО append, чтобы HasRedeploy попал в слайс) --- diff --git a/TOOLS/resource-generator/internal/templates/instance.go b/TOOLS/resource-generator/internal/templates/instance.go index d154826..654a0e1 100644 --- a/TOOLS/resource-generator/internal/templates/instance.go +++ b/TOOLS/resource-generator/internal/templates/instance.go @@ -222,12 +222,15 @@ func (r *{{ToCamel .Name}}Resource) ModifyPlan(ctx context.Context, req resource {{- if .HasRefSvcParams }} {{- range .SchemaParams }} {{- if and (gt .RefSvcId 0) (eq (ParamType .) "types.String") }} + // refSvc-поле резолвим только для API-запроса. + // config не перезаписываем: пользовательский display name или UUID должен + // пройти в state ровно в том виде, в котором его передал Terraform. + resolved{{ToCamel .Code}} := config.{{ToCamel .Code}}.ValueString() if !config.{{ToCamel .Code}}.IsNull() && !config.{{ToCamel .Code}}.IsUnknown() { - resolved{{ToCamel .Code}}, err := r.client.ResolveRefSvcParamValue(ctx, {{.RefSvcId}}, config.{{ToCamel .Code}}.ValueString()) + var err error + resolved{{ToCamel .Code}}, err = r.client.ResolveRefSvcParamValue(ctx, {{.RefSvcId}}, config.{{ToCamel .Code}}.ValueString()) if err != nil { resp.Diagnostics.AddWarning("Failed to resolve {{ToSnake .Code}}", err.Error()) - } else if resolved{{ToCamel .Code}} != "" && resolved{{ToCamel .Code}} != config.{{ToCamel .Code}}.ValueString() { - config.{{ToCamel .Code}} = types.StringValue(resolved{{ToCamel .Code}}) } } {{- end }} @@ -236,9 +239,13 @@ func (r *{{ToCamel .Name}}Resource) ModifyPlan(ctx context.Context, req resource params := map[int]string{ {{- range .CreateParams }} {{- if not (IsNested .) }} + {{- if and (gt .RefSvcId 0) (eq (ParamType .) "types.String") }} + {{.ID}}: resolved{{ToCamel .Code}}, + {{- else }} {{.ID}}: {{ParamFormat . (printf "config.%s" (ToCamel .Code))}}, {{- end }} {{- end }} + {{- end }} } {{- range .CreateParams }} {{- if (IsNested .) }} @@ -274,45 +281,22 @@ func (r *{{ToCamel .Name}}Resource) Create(ctx context.Context, req resource.Cre {{- end }} {{- if .HasRefSvcParams }} {{- range .SchemaParams }} - {{- if and (gt .RefSvcId 0) (ne .RefSvcId 12) (eq (ParamType .) "types.String") }} - // ══════════════════════════════════════════════════════════════════════════ - // ПРАВИЛО TERRAFORM (официальная документация): - // «If an attribute value is configured, it is NEVER valid to change that - // value in the plan.» — то есть plan ОБЯЗАН равняться config (= тому что - // написал пользователь). Менять plan запрещено на уровне фреймворка. - // - // ПРОБЛЕМА: API нашего облака возвращает UUID в нижнем регистре. - // Пользователь пишет: vapp_uid = "6214BA32-..." (верхний или смешанный). - // После apply API вернул: "6214ba32-..." → state != plan → Terraform кричит: - // «Provider produced inconsistent result after apply». - // - // РЕШЕНИЕ: корректировать STATE под PLAN, а не наоборот. - // Шаг 1 (здесь): сохраняем оригинальное значение из plan ДО того как - // ResolveRefSvcParamValue переведёт UUID в нижний регистр (нужен для API). - // Шаг 2 (ниже, после RefreshResourceState): восстанавливаем оригинальный - // регистр в state через strings.EqualFold (сравниваем без учёта регистра). - // ══════════════════════════════════════════════════════════════════════════ - original{{ToCamel .Code}} := data.{{ToCamel .Code}} - {{- end }} - {{- end }} - {{- end }} - {{- if .HasRefSvcParams }} - {{- range .SchemaParams }} - {{- if and (gt .RefSvcId 0) (ne .RefSvcId 12) (eq (ParamType .) "types.String") }} + {{- if and (gt .RefSvcId 0) (eq (ParamType .) "types.String") }} + // refSvc-поле резолвим только для API-запроса. + // config не перезаписываем: пользовательский display name или UUID должен + // пройти в state ровно в том виде, в котором его передал Terraform. + resolved{{ToCamel .Code}} := data.{{ToCamel .Code}}.ValueString() if !data.{{ToCamel .Code}}.IsNull() && !data.{{ToCamel .Code}}.IsUnknown() { - resolved{{ToCamel .Code}}, err := r.client.ResolveRefSvcParamValue(ctx, {{.RefSvcId}}, data.{{ToCamel .Code}}.ValueString()) + var err error + resolved{{ToCamel .Code}}, err = r.client.ResolveRefSvcParamValue(ctx, {{.RefSvcId}}, data.{{ToCamel .Code}}.ValueString()) if err != nil { resp.Diagnostics.AddError("Ошибка клиента", err.Error()) return } - if resolved{{ToCamel .Code}} != "" && resolved{{ToCamel .Code}} != data.{{ToCamel .Code}}.ValueString() { - data.{{ToCamel .Code}} = types.StringValue(resolved{{ToCamel .Code}}) - } } {{- end }} {{- end }} {{- end }} - resourceName := data.ResourceName.ValueString() desiredDomain := "" {{- if .HasDomainParam }} @@ -363,7 +347,7 @@ func (r *{{ToCamel .Name}}Resource) Create(ctx context.Context, req resource.Cre {{- end }} }, []resources_core.InputField{ {{- range .SchemaParams }} - {{- if or (le .RefSvcId 0) (ne .RefSvcId 12) }} + {{- if eq .RefSvcId 0 }} {Code: "{{.Code}}", Field: "{{ToCamel .Code}}", Type: "{{.Type}}"}, {{- end }} {{- end }} @@ -372,34 +356,6 @@ func (r *{{ToCamel .Name}}Resource) Create(ctx context.Context, req resource.Cre if resp.Diagnostics.HasError() { return } - {{- if .HasRefSvcParams }} - {{- range .SchemaParams }} - {{- if and (gt .RefSvcId 0) (ne .RefSvcId 12) (eq (ParamType .) "types.String") }} - if !state.{{ToCamel .Code}}.IsNull() && !state.{{ToCamel .Code}}.IsUnknown() { - resolved{{ToCamel .Code}}, err := r.client.ResolveRefSvcParamValue(ctx, {{.RefSvcId}}, state.{{ToCamel .Code}}.ValueString()) - if err != nil { - resp.Diagnostics.AddWarning("Failed to resolve {{ToSnake .Code}}", err.Error()) - } else if resolved{{ToCamel .Code}} != "" && resolved{{ToCamel .Code}} != state.{{ToCamel .Code}}.ValueString() { - state.{{ToCamel .Code}} = types.StringValue(resolved{{ToCamel .Code}}) - } - } - {{- end }} - {{- end }} - // Restore user-provided casing в state (Create). - // EqualFold = «совпадают ли значения без учёта регистра?» - // Если да — значит API вернул «туже» строку, только в другом регистре. - // Заменяем state на original (то что было в plan/config пользователя). - // Итог: plan=="6214BA32-..." и state=="6214BA32-..." → нет diff → нет taint. - {{- range .SchemaParams }} - {{- if and (gt .RefSvcId 0) (ne .RefSvcId 12) (eq (ParamType .) "types.String") }} - if !original{{ToCamel .Code}}.IsNull() && !original{{ToCamel .Code}}.IsUnknown() && !state.{{ToCamel .Code}}.IsNull() && !state.{{ToCamel .Code}}.IsUnknown() { - if strings.EqualFold(state.{{ToCamel .Code}}.ValueString(), original{{ToCamel .Code}}.ValueString()) { - state.{{ToCamel .Code}} = original{{ToCamel .Code}} - } - } - {{- end }} - {{- end }} - {{- end }} resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) } @@ -431,7 +387,7 @@ func (r *{{ToCamel .Name}}Resource) Read(ctx context.Context, req resource.ReadR {{- end }} }, []resources_core.InputField{ {{- range .SchemaParams }} - {{- if or (le .RefSvcId 0) (ne .RefSvcId 12) }} + {{- if eq .RefSvcId 0 }} {Code: "{{.Code}}", Field: "{{ToCamel .Code}}", Type: "{{.Type}}"}, {{- end }} {{- end }} @@ -440,34 +396,6 @@ func (r *{{ToCamel .Name}}Resource) Read(ctx context.Context, req resource.ReadR if resp.Diagnostics.HasError() { return } - {{- if .HasRefSvcParams }} - {{- range .SchemaParams }} - {{- if and (gt .RefSvcId 0) (ne .RefSvcId 12) (eq (ParamType .) "types.String") }} - if !newState.{{ToCamel .Code}}.IsNull() && !newState.{{ToCamel .Code}}.IsUnknown() { - resolved{{ToCamel .Code}}, err := r.client.ResolveRefSvcParamValue(ctx, {{.RefSvcId}}, newState.{{ToCamel .Code}}.ValueString()) - if err != nil { - resp.Diagnostics.AddWarning("Failed to resolve {{ToSnake .Code}}", err.Error()) - } else if resolved{{ToCamel .Code}} != "" && resolved{{ToCamel .Code}} != newState.{{ToCamel .Code}}.ValueString() { - newState.{{ToCamel .Code}} = types.StringValue(resolved{{ToCamel .Code}}) - } - } - {{- end }} - {{- end }} - // Restore user-provided casing в state (Read). - // При чтении у нас нет plan — но предыдущий state уже хранит значение - // в регистре пользователя (после первого Create оно было восстановлено). - // Берём prior state (переменная state) как эталон регистра. - // Если API вернул то же UUID только строчными буквами — восстанавливаем. - {{- range .SchemaParams }} - {{- if and (gt .RefSvcId 0) (ne .RefSvcId 12) (eq (ParamType .) "types.String") }} - if !state.{{ToCamel .Code}}.IsNull() && !state.{{ToCamel .Code}}.IsUnknown() && !newState.{{ToCamel .Code}}.IsNull() && !newState.{{ToCamel .Code}}.IsUnknown() { - if strings.EqualFold(newState.{{ToCamel .Code}}.ValueString(), state.{{ToCamel .Code}}.ValueString()) { - newState.{{ToCamel .Code}} = state.{{ToCamel .Code}} - } - } - {{- end }} - {{- end }} - {{- end }} resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...) } @@ -597,7 +525,7 @@ func (r *{{ToCamel .Name}}Resource) Update(ctx context.Context, req resource.Upd {{- end }} }, []resources_core.InputField{ {{- range .SchemaParams }} - {{- if or (le .RefSvcId 0) (ne .RefSvcId 12) }} + {{- if eq .RefSvcId 0 }} {Code: "{{.Code}}", Field: "{{ToCamel .Code}}", Type: "{{.Type}}"}, {{- end }} {{- end }} @@ -606,33 +534,6 @@ func (r *{{ToCamel .Name}}Resource) Update(ctx context.Context, req resource.Upd if resp.Diagnostics.HasError() { return } - {{- if .HasRefSvcParams }} - {{- range .SchemaParams }} - {{- if and (gt .RefSvcId 0) (ne .RefSvcId 12) (eq (ParamType .) "types.String") }} - if !state.{{ToCamel .Code}}.IsNull() && !state.{{ToCamel .Code}}.IsUnknown() { - resolved{{ToCamel .Code}}, err := r.client.ResolveRefSvcParamValue(ctx, {{.RefSvcId}}, state.{{ToCamel .Code}}.ValueString()) - if err != nil { - resp.Diagnostics.AddWarning("Failed to resolve {{ToSnake .Code}}", err.Error()) - } else if resolved{{ToCamel .Code}} != "" && resolved{{ToCamel .Code}} != state.{{ToCamel .Code}}.ValueString() { - state.{{ToCamel .Code}} = types.StringValue(resolved{{ToCamel .Code}}) - } - } - {{- end }} - {{- end }} - // Restore user-provided casing в state (Update). - // После API-вызова modify state содержит значения в lower-case от API. - // Plan == config == то что написал пользователь (регистр неизменён). - // EqualFold: если UUID совпадает без учёта регистра — берём из plan. - {{- range .SchemaParams }} - {{- if and (gt .RefSvcId 0) (ne .RefSvcId 12) (eq (ParamType .) "types.String") }} - if !plan.{{ToCamel .Code}}.IsNull() && !plan.{{ToCamel .Code}}.IsUnknown() && !state.{{ToCamel .Code}}.IsNull() && !state.{{ToCamel .Code}}.IsUnknown() { - if strings.EqualFold(state.{{ToCamel .Code}}.ValueString(), plan.{{ToCamel .Code}}.ValueString()) { - state.{{ToCamel .Code}} = plan.{{ToCamel .Code}} - } - } - {{- end }} - {{- end }} - {{- end }} resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) } diff --git a/TOOLS/resource-generator/internal/templates/subresource.go b/TOOLS/resource-generator/internal/templates/subresource.go index 9f2b101..497fd25 100644 --- a/TOOLS/resource-generator/internal/templates/subresource.go +++ b/TOOLS/resource-generator/internal/templates/subresource.go @@ -101,15 +101,17 @@ func (r *{{ToCamel (printf "%s_%s" .ServiceName .SubName)}}Resource) Create(ctx {{- if .HasRefSvcParams }} {{- range .SchemaParams }} {{- if and (gt .RefSvcId 0) (eq (ParamType .) "types.String") }} + // Подресурсный refSvc-параметр резолвим только в локальную переменную. + // Сам plan не трогаем: UUID нужен лишь для backend-вызова, а в state + // Terraform должен видеть исходное пользовательское значение. + resolved{{ToCamel .Code}} := plan.{{ToCamel .Code}}.ValueString() if !plan.{{ToCamel .Code}}.IsNull() && !plan.{{ToCamel .Code}}.IsUnknown() { - resolved{{ToCamel .Code}}, err := r.client.ResolveRefSvcParamValue(ctx, {{.RefSvcId}}, plan.{{ToCamel .Code}}.ValueString()) + var err error + resolved{{ToCamel .Code}}, err = r.client.ResolveRefSvcParamValue(ctx, {{.RefSvcId}}, plan.{{ToCamel .Code}}.ValueString()) if err != nil { resp.Diagnostics.AddError("Ошибка клиента", err.Error()) return } - if resolved{{ToCamel .Code}} != "" && resolved{{ToCamel .Code}} != plan.{{ToCamel .Code}}.ValueString() { - plan.{{ToCamel .Code}} = types.StringValue(resolved{{ToCamel .Code}}) - } } {{- end }} {{- end }} @@ -166,8 +168,12 @@ func (r *{{ToCamel (printf "%s_%s" .ServiceName .SubName)}}Resource) Create(ctx params := resources_core.CompactParams(map[string]string{ {{- range .CreateParams }} + {{- if and (gt .RefSvcId 0) (eq (ParamType .) "types.String") }} + "{{.Code}}": resolved{{ToCamel .Code}}, + {{- else }} "{{.Code}}": {{ParamFormat . (printf "plan.%s" (ToCamel .Code))}}, {{- end }} + {{- end }} }) operationTimeout := "" @@ -249,15 +255,16 @@ func (r *{{ToCamel (printf "%s_%s" .ServiceName .SubName)}}Resource) Update(ctx {{- if .HasRefSvcParams }} {{- range .SchemaParams }} {{- if and (gt .RefSvcId 0) (eq (ParamType .) "types.String") }} + // Update для подресурса следует тому же правилу, что и Create: + // резолвим значение только в локальную переменную и не мутируем plan. + resolved{{ToCamel .Code}} := plan.{{ToCamel .Code}}.ValueString() if !plan.{{ToCamel .Code}}.IsNull() && !plan.{{ToCamel .Code}}.IsUnknown() { - resolved{{ToCamel .Code}}, err := r.client.ResolveRefSvcParamValue(ctx, {{.RefSvcId}}, plan.{{ToCamel .Code}}.ValueString()) + var err error + resolved{{ToCamel .Code}}, err = r.client.ResolveRefSvcParamValue(ctx, {{.RefSvcId}}, plan.{{ToCamel .Code}}.ValueString()) if err != nil { resp.Diagnostics.AddError("Ошибка клиента", err.Error()) return } - if resolved{{ToCamel .Code}} != "" && resolved{{ToCamel .Code}} != plan.{{ToCamel .Code}}.ValueString() { - plan.{{ToCamel .Code}} = types.StringValue(resolved{{ToCamel .Code}}) - } } {{- end }} {{- end }} @@ -284,8 +291,12 @@ func (r *{{ToCamel (printf "%s_%s" .ServiceName .SubName)}}Resource) Update(ctx params := resources_core.CompactParams(map[string]string{ {{- range .ModifyParams }} + {{- if and (gt .RefSvcId 0) (eq (ParamType .) "types.String") }} + "{{.Code}}": resolved{{ToCamel .Code}}, + {{- else }} "{{.Code}}": {{ParamFormat . (printf "plan.%s" (ToCamel .Code))}}, {{- end }} + {{- end }} }) idParams := map[string]string{ diff --git a/TOOLS/scripts/02_generate_resources_and_docs_v2.sh b/TOOLS/scripts/02_generate_resources_and_docs_v2.sh index 49d2b6e..f6e83c3 100755 --- a/TOOLS/scripts/02_generate_resources_and_docs_v2.sh +++ b/TOOLS/scripts/02_generate_resources_and_docs_v2.sh @@ -3,6 +3,15 @@ set -euo pipefail # Canonical resources+docs generator (template format). # Restores instruction-style docs structure (manual/example/params_create/params_modify/outputs/ops/params landing). +# +# Usage: +# ./TOOLS/scripts/02_generate_resources_and_docs_v2.sh --profile TOOLS/config/dev +# ./TOOLS/scripts/02_generate_resources_and_docs_v2.sh --profile TOOLS/config/test +# ./TOOLS/scripts/02_generate_resources_and_docs_v2.sh --profile TOOLS/config/prod +# +# The script always rebuilds resource-generator and docs-generator from source +# before running. This prevents stale binaries from hiding support for new YAML +# kinds such as modifier. SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ROOT_DIR="${ROOT_DIR:-$(cd "${SCRIPT_DIR}/../.." && pwd)}" @@ -97,12 +106,12 @@ RESOURCE_GENERATOR_BIN="${ROOT_DIR}/TOOLS/bin/resource-generator" RESOURCE_GENERATOR_SRC="${ROOT_DIR}/TOOLS/resource-generator" DOCS_GENERATOR_BIN="${ROOT_DIR}/TOOLS/bin/docs-generator" DOCS_GENERATOR_SRC="${ROOT_DIR}/TOOLS/docs-generator" -if [[ ! -x "$RESOURCE_GENERATOR_BIN" ]] || find "$RESOURCE_GENERATOR_SRC" -type f -newer "$RESOURCE_GENERATOR_BIN" -print -quit | grep -q .; then - (cd "$RESOURCE_GENERATOR_SRC" && go build -o "$RESOURCE_GENERATOR_BIN" .) -fi -if [[ ! -x "$DOCS_GENERATOR_BIN" ]] || find "$DOCS_GENERATOR_SRC" -type f -newer "$DOCS_GENERATOR_BIN" -print -quit | grep -q .; then - (cd "$DOCS_GENERATOR_SRC" && go build -o "$DOCS_GENERATOR_BIN" .) -fi + +# Всегда пересобираем генераторы из текущих исходников. +# Это убирает зависимость от mtime и исключает запуск устаревшего бинаря, +# который может не знать про новые kinds/spec-ветки в YAML. +(cd "$RESOURCE_GENERATOR_SRC" && go build -o "$RESOURCE_GENERATOR_BIN" .) +(cd "$DOCS_GENERATOR_SRC" && go build -o "$DOCS_GENERATOR_BIN" .) NUBES_RESOURCES_DIR="$RESOURCES_YAML_DIR" \ NUBES_RESOURCES_GEN_DIR="$TMP_GEN_DIR" \ diff --git a/docs/CHAT_RESUME_2026-09-21.md b/docs/CHAT_RESUME_2026-09-21.md index a14d72f..ed4118f 100644 --- a/docs/CHAT_RESUME_2026-09-21.md +++ b/docs/CHAT_RESUME_2026-09-21.md @@ -109,3 +109,47 @@ - Следующий этап — уже не правка логики, а публикация и стендовая проверка при необходимости. - DEV-релиз `2.0.3` успешно собран и загружен в registry `nubes-dev` через `TOOLS/scripts/03_build_and_upload_provider.sh --profile TOOLS/config/dev`. - Перед этим уже был подготовлен короткий запрос на ревью для Opus и получен ответ, который подтвердил направление правки. + +--- + +## 8. Отдельный диалог про `organization_uid`, refSvcId и универсальное поведение + +### 8.1. Что стало проблемой +- В `vc_vdc` поле `organization_uid` можно передавать как display name (`kontora`), так и как UUID организации. +- В коде `provider/internal/resources_gen/21_vc_vdc_resource.go` это поле сейчас резолвится через `ResolveRefSvcParamValue(...)` в UUID. +- После `apply` Terraform видит расхождение: в конфиге было имя, в state оказался UUID, и появляется ошибка `Provider produced inconsistent result after apply`. +- Параллельно в этом же ресурсе остаются ручные `EqualFold`-хаки, которые пытаются сохранить старое значение, но не решают кейс "имя vs UUID". + +### 8.2. Почему это сравнивали с S3 +- Для `nubes_s3_bucket` похожее поведение уже работает: ref-поле `s3_user_uid` проходит через общий механизм refSvc-резолва и state-refresh. +- В S3 есть симметричный путь: UUID можно принимать на вход, а состояние при чтении синхронизируется через общий mapping-слой. +- Поэтому S3 не падает на inconsistency, а VDC падает из-за локальных restore-хаков и разного поведения на create/read/update. + +### 8.3. Что выяснили по коду +- Ключевой участок VDC: + - [provider/internal/resources_gen/21_vc_vdc_resource.go](provider/internal/resources_gen/21_vc_vdc_resource.go#L197-L202) + - [provider/internal/resources_gen/21_vc_vdc_resource.go](provider/internal/resources_gen/21_vc_vdc_resource.go#L323-L338) + - [provider/internal/resources_gen/21_vc_vdc_resource.go](provider/internal/resources_gen/21_vc_vdc_resource.go#L388-L403) + - [provider/internal/resources_gen/21_vc_vdc_resource.go](provider/internal/resources_gen/21_vc_vdc_resource.go#L511-L525) +- В S3 аналогичный слой устроен аккуратнее: + - [provider/internal/resources_gen/13_s3bucket_resource.go](provider/internal/resources_gen/13_s3bucket_resource.go#L149-L159) + - [provider/internal/resources_core/state_refresh.go](provider/internal/resources_core/state_refresh.go#L82-L96) + - [provider/internal/resources_core/params_ref_mapping.go](provider/internal/resources_core/params_ref_mapping.go#L124-L147) + +### 8.4. Что решил сделать дальше +- Пользователю нужен не частный фикс только для VDC, а универсальная схема для всех refSvcId-полей. +- Была сформулирована задача для Opus: определить, какой канон выбрать для state, где делать name→UUID и UUID→display_name, и как убрать ручные `EqualFold`-хаки без поломки S3 и других уже рабочих ресурсов. +- Отдельно зафиксировано требование: ответ Opus нужен короткий, но сам вопрос должен быть подробным и однозначным. + +### 8.5. Важный вывод на сейчас +- Универсальное решение пока не внедрено. +- Текущий безопасный путь — сначала получить короткий архитектурный ответ от Opus, а уже потом править генератор и пересобирать ресурсы. + +--- + +## 9. Детерминированная пересборка генератора + +- После отдельного разбора `kind: modifier` выяснилось, что падение генерации было эксплуатационным: запускался устаревший бинарник `resource-generator`, а не текущие исходники. +- В `TOOLS/scripts/02_generate_resources_and_docs_v2.sh` убран `mtime`-гард через `find ... -newer`; генераторы теперь всегда собираются заново перед прогоном. +- Это сделано специально, чтобы старый бинарник больше не мог скрыть поддержку новых `kind`-веток в YAML-спеках. +- Дополнительно `TOOLS/resource-generator/bin/` добавлен в ignore, чтобы локальный stale-артефакт не путал следующий запуск.