From 4b34cc7e631f3586412d13380243546199027df4 Mon Sep 17 00:00:00 2001 From: Repinoid Date: Mon, 21 Sep 2026 21:48:54 +0300 Subject: [PATCH] =?UTF-8?q?fix(core):=20=D0=B3=D0=B0=D1=80=D0=B0=D0=BD?= =?UTF-8?q?=D1=82=D0=B8=D1=8F=20known=20=D0=B4=D0=BB=D1=8F=20read-back=20?= =?UTF-8?q?=D0=BF=D0=BE=D0=BB=D0=B5=D0=B9=20(unknown=20->=20null),=20?= =?UTF-8?q?=D1=87=D1=82=D0=BE=D0=B1=D1=8B=20Computed=20=D0=B1=D0=B5=D0=B7?= =?UTF-8?q?=20Default=20=D0=BD=D0=B5=20=D0=BB=D0=BE=D0=BC=D0=B0=D0=BB=20ap?= =?UTF-8?q?ply?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../internal/resources_core/state_refresh.go | 61 +++++++++++++++++-- 1 file changed, 57 insertions(+), 4 deletions(-) diff --git a/provider/internal/resources_core/state_refresh.go b/provider/internal/resources_core/state_refresh.go index 679697a..fdef223 100644 --- a/provider/internal/resources_core/state_refresh.go +++ b/provider/internal/resources_core/state_refresh.go @@ -114,10 +114,8 @@ func RefreshResourceState[T any](ctx context.Context, client *core.UniversalClie if code == "" { continue } - value, ok := paramsMap[code] - if !ok { - continue - } + // Поле модели достаём ДО проверки наличия значения в API: оно нужно + // и в ветке "API не вернул код" (см. схлопывание unknown → null ниже). fieldName := strings.TrimSpace(input.Field) if fieldName == "" { fieldName = toCamel(code) @@ -127,6 +125,25 @@ func RefreshResourceState[T any](ctx context.Context, client *core.UniversalClie continue } + value, ok := paramsMap[code] + if !ok { + // ИНВАРИАНТ: Computed-атрибут обязан быть KNOWN после apply/read. + // + // Параметры, которые провайдер читает обратно, объявлены в схеме как + // Optional+Computed (см. helpers.ShouldBeOptionalComputed). Если + // пользователь такой параметр не задал, в плане он = unknown, и именно + // провайдер обязан проставить конкретное значение. Когда платформа + // не вернула код в state_params, единственное корректное конкретное + // значение — null. + // + // Если оставить unknown, Terraform упадёт с + // "Provider produced invalid result object after apply: ... was unknown". + if inputFieldUnknown(fv) { + setInputFieldNull(fv) + } + continue + } + if strings.EqualFold(code, "jsonEnv") && fv.Type() == typeString { // Preserve planned json_env when API returns equivalent JSON with different ordering. if planned, ok := fv.Interface().(types.String); ok && !planned.IsNull() && !planned.IsUnknown() { @@ -156,6 +173,42 @@ func RefreshResourceState[T any](ctx context.Context, client *core.UniversalClie return state, diags } +// inputFieldUnknown сообщает, находится ли поле модели в состоянии unknown. +// +// Зачем: соблюдение инварианта «Computed-атрибут обязан быть known после +// apply/read». Если платформа не вернула значение в state_params, unknown +// оставлять нельзя — его надо схлопнуть в null (см. setInputFieldNull). +func inputFieldUnknown(fv reflect.Value) bool { + switch fv.Type() { + case typeString: + v, ok := fv.Interface().(types.String) + return ok && v.IsUnknown() + case typeBool: + v, ok := fv.Interface().(types.Bool) + return ok && v.IsUnknown() + case typeInt64: + v, ok := fv.Interface().(types.Int64) + return ok && v.IsUnknown() + } + return false +} + +// setInputFieldNull записывает в поле модели типизированный null. +// +// Зачем: null — это конкретное (known) значение, в отличие от unknown. Именно +// null приводит состояние Terraform в консистентный вид, когда платформа не +// сообщила значение для read-back параметра. +func setInputFieldNull(fv reflect.Value) { + switch fv.Type() { + case typeString: + fv.Set(reflect.ValueOf(types.StringNull())) + case typeBool: + fv.Set(reflect.ValueOf(types.BoolNull())) + case typeInt64: + fv.Set(reflect.ValueOf(types.Int64Null())) + } +} + func setFieldValue(field reflect.Value, value interface{}) { switch field.Type() { case typeString: