fix(generator): read-back параметры без Default -> Optional+Computed (универсальное правило ShouldBeOptionalComputed)
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user