diff --git a/TOOLS/config/dev/profile.env b/TOOLS/config/dev/profile.env index 1e0eb75..6b1d377 100644 --- a/TOOLS/config/dev/profile.env +++ b/TOOLS/config/dev/profile.env @@ -4,7 +4,7 @@ TOKEN_FILE="secrets/dev.token" # Release versions # Version -VERSION="2.0.13" +VERSION="2.0.14" NAMESPACE="nubes-dev" PROVIDER_NAME="nubes" diff --git a/provider/internal/core/operation_cfs.go b/provider/internal/core/operation_cfs.go index eb8840b..53b422b 100644 --- a/provider/internal/core/operation_cfs.go +++ b/provider/internal/core/operation_cfs.go @@ -4,8 +4,49 @@ import ( "context" "encoding/json" "fmt" + "strings" ) +// instanceLiveParams возвращает ТЕКУЩИЕ params инстанса (GET /instances/{uid} → state.params), +// keyed by lower(code). Это единственный достоверный источник live-значений. +// +// ВАЖНО: paramValue из ответа GET /instanceOperations/{opUid}?fields=cfsParams — это +// дефолт ФОРМЫ операции, а НЕ состояние инстанса. Пример из HAR/edge_.har: +// для needEnableAVI в cfsParams paramValue="false", тогда как live-значение инстанса +// needEnableAVI=true. Досылка paramValue как live стирала ALB (reset-to-default). +func (c *UniversalClient) instanceLiveParams(ctx context.Context, instanceUid string) map[string]string { + live := map[string]string{} + params, err := c.GetInstanceStateParams(ctx, instanceUid) + if err != nil { + return live + } + for k, v := range params { + key := strings.ToLower(strings.TrimSpace(k)) + if key == "" { + continue + } + live[key] = v + } + return live +} + +// lookupLiveParam ищет live-значение параметра инстанса по возможным ключам cfsParam. +func lookupLiveParam(live map[string]string, p universalCfsParam) (string, bool) { + if len(live) == 0 { + return "", false + } + for _, cand := range []string{p.Code, p.SvcOperationCfsParam, p.Name, p.Label} { + key := strings.ToLower(strings.TrimSpace(cand)) + if key == "" { + continue + } + if v, ok := live[key]; ok && strings.TrimSpace(v) != "" { + return v, true + } + } + return "", false +} + // fetchOperationCfsParams возвращает cfsParams операции. При неудаче живого // GET /instanceOperations/{opUid}?fields=cfsParams (backend 500 в // getResourceRealmConfig на проблемных инстансах) берёт схему из diff --git a/provider/internal/core/operation_run.go b/provider/internal/core/operation_run.go index 9255d47..ad350cf 100644 --- a/provider/internal/core/operation_run.go +++ b/provider/internal/core/operation_run.go @@ -135,16 +135,21 @@ func (c *UniversalClient) RunInstanceOperationUniversalWithDefaults(ctx context. sent[paramId] = true } + live := c.instanceLiveParams(ctx, instanceUid) for _, param := range cfsParams { if sent[param.SvcOperationCfsParamId] { continue } - val := "" - if param.ParamValue != nil { - val = *param.ParamValue - } else if param.DefaultValue != nil { - val = *param.DefaultValue + // Приоритет: live state.params инстанса → paramValue операции → defaultValue. + // paramValue из cfsParams — дефолт формы, не состояние инстанса (см. operation_cfs.go). + val, hasLive := lookupLiveParam(live, param) + if !hasLive { + if param.ParamValue != nil { + val = *param.ParamValue + } else if param.DefaultValue != nil { + val = *param.DefaultValue + } } val = normalizeUniversalValueV6(val, param) diff --git a/provider/internal/core/operation_run_bycode.go b/provider/internal/core/operation_run_bycode.go index 9b7cd3e..3fa04d2 100644 --- a/provider/internal/core/operation_run_bycode.go +++ b/provider/internal/core/operation_run_bycode.go @@ -105,26 +105,31 @@ func (c *UniversalClient) runInstanceOperationByCode(ctx context.Context, instan sent[paramId] = true } + live := c.instanceLiveParams(ctx, instanceUid) for _, param := range cfsParams { if sent[param.SvcOperationCfsParamId] { continue } - // Дозаполняем ВСЕ незаданные параметры их текущим live-значением - // (ParamValue из GET ?fields=cfsParams), а не только required. - // Бэкенд для modify трактует пропущенный/null как reset-to-default, - // из-за чего частичный payload затирал create-поля (см. prompt_for_opus_modifier_null_bug.md). - // Поведение выровнено под RunInstanceOperationUniversalWithDefaults. - // НО: если ни live-значения, ни дефолта НЕТ — пропускаем (не шлём синтетический + // Дозаполняем ВСЕ незаданные параметры, чтобы бэкенд modify не трактовал + // пропущенный/null как reset-to-default (иначе частичный payload затирает + // create-поля, см. prompt_for_opus_modifier_null_bug.md). + // + // ПРИОРИТЕТ ИСТОЧНИКА: live state.params инстанса → paramValue операции → defaultValue. + // КРИТИЧНО: paramValue из ?fields=cfsParams — дефолт ФОРМЫ операции, не состояние + // инстанса (HAR/edge_.har: needEnableAVI paramValue="false" при live=true → стирало ALB). + // Если ни live, ни paramValue, ни defaultValue НЕТ — пропускаем (не шлём синтетический // "0"/"false"/"[]", который может нарушить constraint "integer > 0"). - if (param.ParamValue == nil || strings.TrimSpace(*param.ParamValue) == "") && - (param.DefaultValue == nil || strings.TrimSpace(*param.DefaultValue) == "") { - continue - } - val := "" - if param.ParamValue != nil { - val = *param.ParamValue - } else if param.DefaultValue != nil { - val = *param.DefaultValue + val, hasLive := lookupLiveParam(live, param) + if !hasLive { + if (param.ParamValue == nil || strings.TrimSpace(*param.ParamValue) == "") && + (param.DefaultValue == nil || strings.TrimSpace(*param.DefaultValue) == "") { + continue + } + if param.ParamValue != nil { + val = *param.ParamValue + } else if param.DefaultValue != nil { + val = *param.DefaultValue + } } val = normalizeUniversalValueV6(val, param)