feat(core): modifierDesiredEqualsCurrent + RunInstanceOperationUniversalByIdempotent + RunOperationByCodeIdempotent
This commit is contained in:
@@ -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 != "" {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user