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.
|
// RunInstanceOperationUniversalByCode runs an operation using params keyed by code.
|
||||||
// It resolves param codes to IDs via operation manifest, applies defaults, validates, and runs.
|
// 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 {
|
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)
|
state, err := c.GetInstanceState(ctx, instanceUid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -49,6 +60,12 @@ func (c *UniversalClient) RunInstanceOperationUniversalByCode(ctx context.Contex
|
|||||||
return err
|
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)
|
codeToParam := make(map[string]universalCfsParam)
|
||||||
for _, p := range cfsParams {
|
for _, p := range cfsParams {
|
||||||
if key := strings.ToLower(strings.TrimSpace(p.Code)); key != "" {
|
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)
|
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) {
|
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 {
|
if existing.OperationIsInProgress || existing.OperationIsPending {
|
||||||
|
|||||||
Reference in New Issue
Block a user