Fix generator rebuild and release pipeline

This commit is contained in:
Repinoid
2026-09-21 18:08:26 +03:00
parent 1ec6a0fedc
commit 7ecd2aaf44
10 changed files with 131 additions and 139 deletions
+1
View File
@@ -114,5 +114,6 @@ universal_rebuild/service_params_gen
terraform-provider-mycloud terraform-provider-mycloud
artifacts/api-meta/*/errors.log artifacts/api-meta/*/errors.log
TOOLS/bin/ TOOLS/bin/
TOOLS/resource-generator/bin/
TOOLS/docs-generator/bin/ TOOLS/docs-generator/bin/
docs/30_registry/resources/ docs/30_registry/resources/
+1 -1
View File
@@ -10,4 +10,4 @@ vdc_cpu_allocated = 8
vdc_cpu_guaranteed = 0 vdc_cpu_guaranteed = 0
vdc_mem_allocated = 32 vdc_mem_allocated = 32
vdc_storage_config = "[{\"name\":\"fast\",\"size\":200}]" vdc_storage_config = "[{\"name\":\"SATA\",\"size\":\"200\"}]"
+8 -4
View File
@@ -56,16 +56,20 @@ export TOKEN_FILE=/home/naeel/terra/08-33-41.token
## Step 2: Generate Go resources and docs ## 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: Example:
```bash ```bash
./02_generate_resources_and_docs.sh ./TOOLS/scripts/02_generate_resources_and_docs_v2.sh --profile TOOLS/config/dev
``` ```
Outputs: Outputs:
- Go files in `universal_rebuild/internal/resources_gen` - Go files in `generated/<stand>/go`
- Docs in `docs/30_registry/resources` - Docs in `generated/<stand>/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 ## Step 3: Build and upload provider
+19
View File
@@ -24,6 +24,25 @@ cd resource-generator && go build -o ../bin/resource-generator .
Структура: `main.go` + `internal/{helpers,loader,params,templates,types,writers}`. Структура: `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 ## docs-generator
`resources_yaml/*.yaml` → Markdown-документация в `docs/30_registry/resources/` `resources_yaml/*.yaml` → Markdown-документация в `docs/30_registry/resources/`
Binary file not shown.
@@ -133,7 +133,10 @@ func LoadSpecs(dir string) ([]types.GenResource, []types.GenSubresource, []types
gr.NeedsFmtImport = true gr.NeedsFmtImport = true
} }
gr.NeedsJsonPlanMod = params.AnalyzeJsonPlanMod(gr.SchemaParams) 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) gr.NeedsFmtImport = params.HasNestedParams(gr.SchemaParams)
// --- Action processing (ДО append, чтобы HasRedeploy попал в слайс) --- // --- Action processing (ДО append, чтобы HasRedeploy попал в слайс) ---
@@ -222,12 +222,15 @@ func (r *{{ToCamel .Name}}Resource) ModifyPlan(ctx context.Context, req resource
{{- if .HasRefSvcParams }} {{- if .HasRefSvcParams }}
{{- range .SchemaParams }} {{- range .SchemaParams }}
{{- if and (gt .RefSvcId 0) (eq (ParamType .) "types.String") }} {{- 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() { 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 { if err != nil {
resp.Diagnostics.AddWarning("Failed to resolve {{ToSnake .Code}}", err.Error()) 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 }} {{- end }}
@@ -236,9 +239,13 @@ func (r *{{ToCamel .Name}}Resource) ModifyPlan(ctx context.Context, req resource
params := map[int]string{ params := map[int]string{
{{- range .CreateParams }} {{- range .CreateParams }}
{{- if not (IsNested .) }} {{- if not (IsNested .) }}
{{- if and (gt .RefSvcId 0) (eq (ParamType .) "types.String") }}
{{.ID}}: resolved{{ToCamel .Code}},
{{- else }}
{{.ID}}: {{ParamFormat . (printf "config.%s" (ToCamel .Code))}}, {{.ID}}: {{ParamFormat . (printf "config.%s" (ToCamel .Code))}},
{{- end }} {{- end }}
{{- end }} {{- end }}
{{- end }}
} }
{{- range .CreateParams }} {{- range .CreateParams }}
{{- if (IsNested .) }} {{- if (IsNested .) }}
@@ -274,45 +281,22 @@ func (r *{{ToCamel .Name}}Resource) Create(ctx context.Context, req resource.Cre
{{- end }} {{- end }}
{{- if .HasRefSvcParams }} {{- if .HasRefSvcParams }}
{{- range .SchemaParams }} {{- 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-запроса.
// ПРАВИЛО TERRAFORM (официальная документация): // config не перезаписываем: пользовательский display name или UUID должен
// «If an attribute value is configured, it is NEVER valid to change that // пройти в state ровно в том виде, в котором его передал Terraform.
// value in the plan.» — то есть plan ОБЯЗАН равняться config (= тому что resolved{{ToCamel .Code}} := data.{{ToCamel .Code}}.ValueString()
// написал пользователь). Менять 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 !data.{{ToCamel .Code}}.IsNull() && !data.{{ToCamel .Code}}.IsUnknown() { 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 { if err != nil {
resp.Diagnostics.AddError("Ошибка клиента", err.Error()) resp.Diagnostics.AddError("Ошибка клиента", err.Error())
return return
} }
if resolved{{ToCamel .Code}} != "" && resolved{{ToCamel .Code}} != data.{{ToCamel .Code}}.ValueString() {
data.{{ToCamel .Code}} = types.StringValue(resolved{{ToCamel .Code}})
}
} }
{{- end }} {{- end }}
{{- end }} {{- end }}
{{- end }} {{- end }}
resourceName := data.ResourceName.ValueString() resourceName := data.ResourceName.ValueString()
desiredDomain := "" desiredDomain := ""
{{- if .HasDomainParam }} {{- if .HasDomainParam }}
@@ -363,7 +347,7 @@ func (r *{{ToCamel .Name}}Resource) Create(ctx context.Context, req resource.Cre
{{- end }} {{- end }}
}, []resources_core.InputField{ }, []resources_core.InputField{
{{- range .SchemaParams }} {{- range .SchemaParams }}
{{- if or (le .RefSvcId 0) (ne .RefSvcId 12) }} {{- if eq .RefSvcId 0 }}
{Code: "{{.Code}}", Field: "{{ToCamel .Code}}", Type: "{{.Type}}"}, {Code: "{{.Code}}", Field: "{{ToCamel .Code}}", Type: "{{.Type}}"},
{{- end }} {{- end }}
{{- end }} {{- end }}
@@ -372,34 +356,6 @@ func (r *{{ToCamel .Name}}Resource) Create(ctx context.Context, req resource.Cre
if resp.Diagnostics.HasError() { if resp.Diagnostics.HasError() {
return 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)...) 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 }} {{- end }}
}, []resources_core.InputField{ }, []resources_core.InputField{
{{- range .SchemaParams }} {{- range .SchemaParams }}
{{- if or (le .RefSvcId 0) (ne .RefSvcId 12) }} {{- if eq .RefSvcId 0 }}
{Code: "{{.Code}}", Field: "{{ToCamel .Code}}", Type: "{{.Type}}"}, {Code: "{{.Code}}", Field: "{{ToCamel .Code}}", Type: "{{.Type}}"},
{{- end }} {{- end }}
{{- end }} {{- end }}
@@ -440,34 +396,6 @@ func (r *{{ToCamel .Name}}Resource) Read(ctx context.Context, req resource.ReadR
if resp.Diagnostics.HasError() { if resp.Diagnostics.HasError() {
return 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)...) 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 }} {{- end }}
}, []resources_core.InputField{ }, []resources_core.InputField{
{{- range .SchemaParams }} {{- range .SchemaParams }}
{{- if or (le .RefSvcId 0) (ne .RefSvcId 12) }} {{- if eq .RefSvcId 0 }}
{Code: "{{.Code}}", Field: "{{ToCamel .Code}}", Type: "{{.Type}}"}, {Code: "{{.Code}}", Field: "{{ToCamel .Code}}", Type: "{{.Type}}"},
{{- end }} {{- end }}
{{- end }} {{- end }}
@@ -606,33 +534,6 @@ func (r *{{ToCamel .Name}}Resource) Update(ctx context.Context, req resource.Upd
if resp.Diagnostics.HasError() { if resp.Diagnostics.HasError() {
return 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)...) resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
} }
@@ -101,15 +101,17 @@ func (r *{{ToCamel (printf "%s_%s" .ServiceName .SubName)}}Resource) Create(ctx
{{- if .HasRefSvcParams }} {{- if .HasRefSvcParams }}
{{- range .SchemaParams }} {{- range .SchemaParams }}
{{- if and (gt .RefSvcId 0) (eq (ParamType .) "types.String") }} {{- 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() { 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 { if err != nil {
resp.Diagnostics.AddError("Ошибка клиента", err.Error()) resp.Diagnostics.AddError("Ошибка клиента", err.Error())
return return
} }
if resolved{{ToCamel .Code}} != "" && resolved{{ToCamel .Code}} != plan.{{ToCamel .Code}}.ValueString() {
plan.{{ToCamel .Code}} = types.StringValue(resolved{{ToCamel .Code}})
}
} }
{{- end }} {{- end }}
{{- end }} {{- end }}
@@ -166,8 +168,12 @@ func (r *{{ToCamel (printf "%s_%s" .ServiceName .SubName)}}Resource) Create(ctx
params := resources_core.CompactParams(map[string]string{ params := resources_core.CompactParams(map[string]string{
{{- range .CreateParams }} {{- range .CreateParams }}
{{- if and (gt .RefSvcId 0) (eq (ParamType .) "types.String") }}
"{{.Code}}": resolved{{ToCamel .Code}},
{{- else }}
"{{.Code}}": {{ParamFormat . (printf "plan.%s" (ToCamel .Code))}}, "{{.Code}}": {{ParamFormat . (printf "plan.%s" (ToCamel .Code))}},
{{- end }} {{- end }}
{{- end }}
}) })
operationTimeout := "" operationTimeout := ""
@@ -249,15 +255,16 @@ func (r *{{ToCamel (printf "%s_%s" .ServiceName .SubName)}}Resource) Update(ctx
{{- if .HasRefSvcParams }} {{- if .HasRefSvcParams }}
{{- range .SchemaParams }} {{- range .SchemaParams }}
{{- if and (gt .RefSvcId 0) (eq (ParamType .) "types.String") }} {{- 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() { 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 { if err != nil {
resp.Diagnostics.AddError("Ошибка клиента", err.Error()) resp.Diagnostics.AddError("Ошибка клиента", err.Error())
return return
} }
if resolved{{ToCamel .Code}} != "" && resolved{{ToCamel .Code}} != plan.{{ToCamel .Code}}.ValueString() {
plan.{{ToCamel .Code}} = types.StringValue(resolved{{ToCamel .Code}})
}
} }
{{- end }} {{- end }}
{{- end }} {{- end }}
@@ -284,8 +291,12 @@ func (r *{{ToCamel (printf "%s_%s" .ServiceName .SubName)}}Resource) Update(ctx
params := resources_core.CompactParams(map[string]string{ params := resources_core.CompactParams(map[string]string{
{{- range .ModifyParams }} {{- range .ModifyParams }}
{{- if and (gt .RefSvcId 0) (eq (ParamType .) "types.String") }}
"{{.Code}}": resolved{{ToCamel .Code}},
{{- else }}
"{{.Code}}": {{ParamFormat . (printf "plan.%s" (ToCamel .Code))}}, "{{.Code}}": {{ParamFormat . (printf "plan.%s" (ToCamel .Code))}},
{{- end }} {{- end }}
{{- end }}
}) })
idParams := map[string]string{ idParams := map[string]string{
@@ -3,6 +3,15 @@ set -euo pipefail
# Canonical resources+docs generator (template format). # Canonical resources+docs generator (template format).
# Restores instruction-style docs structure (manual/example/params_create/params_modify/outputs/ops/params landing). # 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)" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="${ROOT_DIR:-$(cd "${SCRIPT_DIR}/../.." && 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" RESOURCE_GENERATOR_SRC="${ROOT_DIR}/TOOLS/resource-generator"
DOCS_GENERATOR_BIN="${ROOT_DIR}/TOOLS/bin/docs-generator" DOCS_GENERATOR_BIN="${ROOT_DIR}/TOOLS/bin/docs-generator"
DOCS_GENERATOR_SRC="${ROOT_DIR}/TOOLS/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 # Это убирает зависимость от mtime и исключает запуск устаревшего бинаря,
if [[ ! -x "$DOCS_GENERATOR_BIN" ]] || find "$DOCS_GENERATOR_SRC" -type f -newer "$DOCS_GENERATOR_BIN" -print -quit | grep -q .; then # который может не знать про новые kinds/spec-ветки в YAML.
(cd "$DOCS_GENERATOR_SRC" && go build -o "$DOCS_GENERATOR_BIN" .) (cd "$RESOURCE_GENERATOR_SRC" && go build -o "$RESOURCE_GENERATOR_BIN" .)
fi (cd "$DOCS_GENERATOR_SRC" && go build -o "$DOCS_GENERATOR_BIN" .)
NUBES_RESOURCES_DIR="$RESOURCES_YAML_DIR" \ NUBES_RESOURCES_DIR="$RESOURCES_YAML_DIR" \
NUBES_RESOURCES_GEN_DIR="$TMP_GEN_DIR" \ NUBES_RESOURCES_GEN_DIR="$TMP_GEN_DIR" \
+44
View File
@@ -109,3 +109,47 @@
- Следующий этап — уже не правка логики, а публикация и стендовая проверка при необходимости. - Следующий этап — уже не правка логики, а публикация и стендовая проверка при необходимости.
- DEV-релиз `2.0.3` успешно собран и загружен в registry `nubes-dev` через `TOOLS/scripts/03_build_and_upload_provider.sh --profile TOOLS/config/dev`. - DEV-релиз `2.0.3` успешно собран и загружен в registry `nubes-dev` через `TOOLS/scripts/03_build_and_upload_provider.sh --profile TOOLS/config/dev`.
- Перед этим уже был подготовлен короткий запрос на ревью для Opus и получен ответ, который подтвердил направление правки. - Перед этим уже был подготовлен короткий запрос на ревью для 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-артефакт не путал следующий запуск.