fix(core): гарантия known для read-back полей (unknown -> null), чтобы Computed без Default не ломал apply

This commit is contained in:
Repinoid
2026-09-21 21:48:54 +03:00
parent 3c0157a1af
commit 4b34cc7e63
@@ -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: