From 3c0157a1af403d5f5350680acc4fb8db776dc03f Mon Sep 17 00:00:00 2001 From: Repinoid Date: Mon, 21 Sep 2026 21:48:54 +0300 Subject: [PATCH] =?UTF-8?q?fix(generator):=20read-back=20=D0=BF=D0=B0?= =?UTF-8?q?=D1=80=D0=B0=D0=BC=D0=B5=D1=82=D1=80=D1=8B=20=D0=B1=D0=B5=D0=B7?= =?UTF-8?q?=20Default=20->=20Optional+Computed=20(=D1=83=D0=BD=D0=B8=D0=B2?= =?UTF-8?q?=D0=B5=D1=80=D1=81=D0=B0=D0=BB=D1=8C=D0=BD=D0=BE=D0=B5=20=D0=BF?= =?UTF-8?q?=D1=80=D0=B0=D0=B2=D0=B8=D0=BB=D0=BE=20ShouldBeOptionalComputed?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../internal/helpers/helpers.go | 31 ++++++++++ .../internal/templates/instance.go | 2 +- .../internal/writers/writers.go | 62 ++++++++++--------- 3 files changed, 65 insertions(+), 30 deletions(-) diff --git a/TOOLS/resource-generator/internal/helpers/helpers.go b/TOOLS/resource-generator/internal/helpers/helpers.go index 9dd247b..d75a513 100644 --- a/TOOLS/resource-generator/internal/helpers/helpers.go +++ b/TOOLS/resource-generator/internal/helpers/helpers.go @@ -74,6 +74,37 @@ func ParamDefaultExpr(p types.Param) string { } } +// ShouldBeOptionalComputed — обязан ли параметр быть объявлен в схеме как +// `Optional: true, Computed: true` (при отсутствии явного Default). +// +// ОБЩИЙ ИНВАРИАНТ ПРОВАЙДЕРА (не привязан к конкретному сервису или параметру): +// каждый скалярный параметр, который провайдер читает ОБРАТНО из state_params +// инстанса и записывает в state (см. RefreshResourceState + InputField в шаблоне +// instance.go), обязан быть Optional+Computed, если он не Required и без Default. +// +// ПОЧЕМУ ИНАЧЕ ЛОМАЕТСЯ: +// - Пользователь не задал опциональный параметр → в плане он = null. +// - Платформа подставляет своё значение (например дефолтный QoS-профиль +// "QoS-100Mbit" для vc_nsxt) → провайдер кладёт его в state при apply. +// - Terraform видит расхождение plan(null) ≠ state(значение) и падает: +// "Provider produced inconsistent result after apply: .qos_profile: +// was null, but now cty.StringVal("QoS-100Mbit")". +// +// ПОЧЕМУ Computed ЭТО ЧИНИТ: +// - С `Computed: true` незаданный параметр в плане = unknown (не null), +// и провайдер имеет право заполнить его значением из API — это штатная +// семантика Optional+Computed, а не обходной путь. +// +// УСЛОВИЯ, ПРИ КОТОРЫХ Computed НЕ НУЖЕН: +// - Required: значение всегда задано пользователем → план уже известен; +// - RefSvcId != 0: refSvc-параметры НЕ читаются обратно в модель — они +// резолвятся только для API-вызова, а в state остаётся ровно то, что +// написал пользователь (иначе получим plan(display name) ≠ state(UUID)); +// - Default != "": атрибут уже объявлен Computed+Default, значение известно. +func ShouldBeOptionalComputed(p types.Param) bool { + return !p.Required && p.RefSvcId == 0 && p.Default == "" +} + // ParamFormat возвращает вызов Format* для форматирования значения параметра. func ParamFormat(p types.Param, varName string) string { switch strings.ToLower(p.Type) { diff --git a/TOOLS/resource-generator/internal/templates/instance.go b/TOOLS/resource-generator/internal/templates/instance.go index 50dd4de..5652da2 100644 --- a/TOOLS/resource-generator/internal/templates/instance.go +++ b/TOOLS/resource-generator/internal/templates/instance.go @@ -106,7 +106,7 @@ func (r *{{ToCamel .Name}}Resource) Schema(ctx context.Context, req resource.Sch {{- else }} "{{ToSnake .Code}}": schema.{{if eq (ParamType .) "types.Bool"}}Bool{{else if eq (ParamType .) "types.Int64"}}Int64{{else}}String{{end}}Attribute{ {{- if and .Required (eq (ParamDefaultExpr .) "") (eq .RefSvcId 0) }}Required: true,{{else}}Optional: true,{{end}} - {{- if ne (ParamDefaultExpr .) "" }}Computed: true, Default: {{ParamDefaultExpr .}},{{- else if .IsJson }}Computed: true,{{- end }} + {{- if ne (ParamDefaultExpr .) "" }}Computed: true, Default: {{ParamDefaultExpr .}},{{- else if or .IsJson (ShouldBeOptionalComputed .) }}Computed: true,{{- end }} {{- if ne (ParamDescription .) "" }}MarkdownDescription: {{ParamDescription .}},{{end}} {{- if .Sensitive }}Sensitive: true,{{end}} {{- if .IsJson }}PlanModifiers: []planmodifier.String{resources_core.JsonNormalize()},{{- end }} diff --git a/TOOLS/resource-generator/internal/writers/writers.go b/TOOLS/resource-generator/internal/writers/writers.go index 68b67f7..68261ae 100644 --- a/TOOLS/resource-generator/internal/writers/writers.go +++ b/TOOLS/resource-generator/internal/writers/writers.go @@ -27,28 +27,32 @@ func WriteInstanceResource(outDir string, svc types.GenResource) error { filePath := filepath.Join(outDir, fileName) tpl, err := template.New("resource").Funcs(template.FuncMap{ - "ToCamel": helpers.ToCamel, - "ToSnake": helpers.ToSnake, - "ParamType": helpers.ParamType, - "ParamDefaultExpr": helpers.ParamDefaultExpr, - "ParamFormat": helpers.ParamFormat, - "ParamParse": helpers.ParamParse, - "ParamDescription": helpers.ParamDescription, - "OutputType": helpers.OutputParamType, - "OutputIsMap": helpers.OutputParamIsMap, - "OutputIsList": helpers.OutputParamIsList, - "OutputSensitive": helpers.OutputParamSensitive, - "IsNested": helpers.IsNested, - "IsNestedList": helpers.IsNestedList, - "NestedModelName": helpers.NestedModelName, - "NestedTfType": helpers.NestedTfType, - "NestedSchemaType": helpers.NestedSchemaType, - "NestedSchemaBlock": helpers.NestedSchemaBlock, - "NestedSchemaEnd": helpers.NestedSchemaEnd, - "NestedJSONExpr": helpers.NestedJSONExpr, - "SubSchemaType": helpers.SubSchemaType, - "SubDefaultExpr": helpers.SubDefaultExpr, - "bt": func() string { return "`" }, + "ToCamel": helpers.ToCamel, + "ToSnake": helpers.ToSnake, + "ParamType": helpers.ParamType, + "ParamDefaultExpr": helpers.ParamDefaultExpr, + // ShouldBeOptionalComputed — единое правило: параметр, который провайдер + // читает обратно из state_params, обязан быть Optional+Computed, если он + // не Required и без Default. См. подробное объяснение в helpers.go. + "ShouldBeOptionalComputed": helpers.ShouldBeOptionalComputed, + "ParamFormat": helpers.ParamFormat, + "ParamParse": helpers.ParamParse, + "ParamDescription": helpers.ParamDescription, + "OutputType": helpers.OutputParamType, + "OutputIsMap": helpers.OutputParamIsMap, + "OutputIsList": helpers.OutputParamIsList, + "OutputSensitive": helpers.OutputParamSensitive, + "IsNested": helpers.IsNested, + "IsNestedList": helpers.IsNestedList, + "NestedModelName": helpers.NestedModelName, + "NestedTfType": helpers.NestedTfType, + "NestedSchemaType": helpers.NestedSchemaType, + "NestedSchemaBlock": helpers.NestedSchemaBlock, + "NestedSchemaEnd": helpers.NestedSchemaEnd, + "NestedJSONExpr": helpers.NestedJSONExpr, + "SubSchemaType": helpers.SubSchemaType, + "SubDefaultExpr": helpers.SubDefaultExpr, + "bt": func() string { return "`" }, }).Parse(templates.Instance) if err != nil { return err @@ -142,13 +146,13 @@ func WriteModifierResource(outDir string, modifier types.GenModifier) error { filePath := filepath.Join(outDir, fileName) tpl, err := template.New("modifier").Funcs(template.FuncMap{ - "ToCamel": helpers.ToCamel, - "ToSnake": helpers.ToSnake, - "ParamType": helpers.ParamType, - "ParamDefaultExpr": helpers.ParamDefaultExpr, - "ParamFormat": helpers.ParamFormat, - "ParamDescription": helpers.ParamDescription, - "bt": func() string { return "`" }, + "ToCamel": helpers.ToCamel, + "ToSnake": helpers.ToSnake, + "ParamType": helpers.ParamType, + "ParamDefaultExpr": helpers.ParamDefaultExpr, + "ParamFormat": helpers.ParamFormat, + "ParamDescription": helpers.ParamDescription, + "bt": func() string { return "`" }, }).Parse(templates.Modifier) if err != nil { return err