feat(core): modifierDesiredEqualsCurrent + RunInstanceOperationUniversalByIdempotent + RunOperationByCodeIdempotent

This commit is contained in:
Repinoid
2026-09-22 21:13:56 +03:00
parent 17a803836a
commit 25e988886d
3 changed files with 105 additions and 0 deletions
@@ -0,0 +1,68 @@
package core
import (
"strings"
"terraform-provider-nubes/internal/core/jsonutil"
)
// modifierDesiredEqualsCurrent сравнивает желаемые значения полей модификатора
// (keyed by code) с живыми значениями из cfsParams (ParamValue).
//
// Возвращает true, если ВСЕ поля совпали (можно пропустить run при idempotency).
// Семантика сравнения:
// - bool/int/string: нормализуются через normalizeUniversalValueV6 и сравниваются строками;
// - map-fixed: JSON-сравнение (порядок ключей не значим);
// - array-map-fixed: JSON-сравнение сырых значений (normalize не строит дефолт для массивов).
func (c *UniversalClient) modifierDesiredEqualsCurrent(desired map[string]string, cfsParams []universalCfsParam) bool {
if len(desired) == 0 {
return false
}
codeToParam := make(map[string]universalCfsParam)
for _, p := range cfsParams {
if key := strings.ToLower(strings.TrimSpace(p.Code)); key != "" {
codeToParam[key] = p
}
if key := strings.ToLower(strings.TrimSpace(p.SvcOperationCfsParam)); key != "" {
codeToParam[key] = p
}
}
for code, wanted := range desired {
param, ok := codeToParam[strings.ToLower(strings.TrimSpace(code))]
if !ok {
// Код не найден в схеме — не можем сравнить, считаем «не равно».
return false
}
current := ""
if param.ParamValue != nil {
current = *param.ParamValue
}
dataType := strings.ToLower(strings.TrimSpace(param.DataType))
if strings.HasPrefix(dataType, "array") {
// array-map-fixed: сравнивать сырые значения как JSON.
if !jsonutil.JSONStringsEquivalent(wanted, current) {
return false
}
continue
}
if strings.HasPrefix(dataType, "map") || strings.Contains(dataType, "json") {
if !jsonutil.JSONStringsEquivalent(wanted, current) {
return false
}
continue
}
// Скаляры: нормализуем обе стороны.
nw := normalizeUniversalValueV6(wanted, param)
nc := normalizeUniversalValueV6(current, param)
if nw != nc {
return false
}
}
return true
}
@@ -9,6 +9,17 @@ import (
// RunInstanceOperationUniversalByCode runs an operation using params keyed by code.
// It resolves param codes to IDs via operation manifest, applies defaults, validates, and runs.
func (c *UniversalClient) RunInstanceOperationUniversalByCode(ctx context.Context, instanceUid string, action string, params map[string]string) error {
return c.runInstanceOperationByCode(ctx, instanceUid, action, params, false)
}
// RunInstanceOperationUniversalByIdempotent — то же, но с idempotency pre-check:
// перед run сверяет desired==current и, если ВСЕ поля совпали, пропускает run.
// Применяется для модификаторов с idempotency: check_before_run.
func (c *UniversalClient) RunInstanceOperationUniversalByIdempotent(ctx context.Context, instanceUid string, action string, params map[string]string) error {
return c.runInstanceOperationByCode(ctx, instanceUid, action, params, true)
}
func (c *UniversalClient) runInstanceOperationByCode(ctx context.Context, instanceUid string, action string, params map[string]string, idempotent bool) error {
state, err := c.GetInstanceState(ctx, instanceUid)
if err != nil {
return err
@@ -49,6 +60,12 @@ func (c *UniversalClient) RunInstanceOperationUniversalByCode(ctx context.Contex
return err
}
// Idempotency pre-check: если все desired уже равны live-значениям — пропускаем run.
// desired = явно заданные пользователем коды (params, keyed by code), БЕЗ досылки.
if idempotent && c.modifierDesiredEqualsCurrent(params, cfsParams) {
return nil
}
codeToParam := make(map[string]universalCfsParam)
for _, p := range cfsParams {
if key := strings.ToLower(strings.TrimSpace(p.Code)); key != "" {
+20
View File
@@ -139,6 +139,26 @@ func RunOperationByCodeWithTimeout(ctx context.Context, client *core.UniversalCl
return client.RunInstanceOperationUniversalByCode(ctxWithTimeout, instanceID, operation, params)
}
// RunOperationByCodeIdempotent — то же, что RunOperationByCodeWithTimeout, но
// с idempotency pre-check (desired==current → skip run). Для модификаторов
// с idempotency: check_before_run.
func RunOperationByCodeIdempotent(ctx context.Context, client *core.UniversalClient, instanceID string, operation string, params map[string]string, operationTimeout string) error {
if strings.TrimSpace(instanceID) == "" {
return fmt.Errorf("missing instance id for operation")
}
if client == nil {
return fmt.Errorf("missing client for operation")
}
unlock := client.LockInstance(instanceID)
defer unlock()
ctxWithTimeout, err := core.WithOperationTimeout(ctx, operationTimeout)
if err != nil {
return err
}
return client.RunInstanceOperationUniversalByIdempotent(ctxWithTimeout, instanceID, operation, params)
}
func adoptExistingInstanceOnCreate(ctx context.Context, client *core.UniversalClient, serviceID int, displayName string, resumeIfExists bool, params map[int]string, existing *core.InstanceStateResponse) (string, error) {
// Проверка: операция уже выполняется
if existing.OperationIsInProgress || existing.OperationIsPending {