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: