fix(uuid-case): нормализация UUID внутри JSON — adopt suspended-инстанса больше не падает на регистре (jsonutil + JsonNormalize), тесты
This commit is contained in:
@@ -8,11 +8,28 @@ package jsonutil
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// uuidAnywhereRegex — UUID-подобная подстрока внутри произвольного текста
|
||||
// (например, внутри JSON-строки). Регистр не учитывается.
|
||||
var uuidAnywhereRegex = regexp.MustCompile(`(?i)[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}`)
|
||||
|
||||
// LowercaseUUIDsInText приводит все UUID-подобные подстроки к lowercase.
|
||||
//
|
||||
// Зачем: облако отдаёт один и тот же UUID то в lowercase (vdcUid), то в UPPERCASE
|
||||
// (nsxtUid у кластера Штурвала), а пользователь может написать в config любой
|
||||
// регистр. Сравнение plan vs actual обязано игнорировать регистр UUID.
|
||||
func LowercaseUUIDsInText(value string) string {
|
||||
if !uuidAnywhereRegex.MatchString(value) {
|
||||
return value
|
||||
}
|
||||
return uuidAnywhereRegex.ReplaceAllStringFunc(value, strings.ToLower)
|
||||
}
|
||||
|
||||
// JSONStringsEquivalent compares JSON strings ignoring whitespace and map key order.
|
||||
func JSONStringsEquivalent(a, b string) bool {
|
||||
na, oka := normalizeJSONIfPossible(a)
|
||||
@@ -107,7 +124,7 @@ func normalizeJSONScalarsToStrings(v interface{}) interface{} {
|
||||
}
|
||||
return out
|
||||
case string:
|
||||
return val
|
||||
return LowercaseUUIDsInText(val)
|
||||
case float64:
|
||||
return strconv.FormatFloat(val, 'g', -1, 64)
|
||||
case bool:
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
package jsonutil
|
||||
|
||||
import "testing"
|
||||
|
||||
// FIX(uuid-case) регрессия: облако отдаёт один и тот же UUID то в lowercase (vdcUid),
|
||||
// то в UPPERCASE (nsxtUid у кластера Штурвала). Сравнение plan vs actual должно
|
||||
// игнорировать регистр UUID — и для одиночного значения, и внутри JSON.
|
||||
func TestJSONStringsEquivalentUUIDCase(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
a string
|
||||
b string
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "uuid внутри JSON: разный регистр — эквивалентно",
|
||||
a: `{"nsxtUid":"2c37fed1-e8f8-4a84-8434-7851c7c8b5d6","clusterName":"shturval-dev-00"}`,
|
||||
b: `{"clusterName":"shturval-dev-00","nsxtUid":"2C37FED1-E8F8-4A84-8434-7851C7C8B5D6"}`,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "вложенный JSON и смешанный регистр",
|
||||
a: `{"a":{"b":["2C37FED1-E8F8-4A84-8434-7851C7C8B5D6"]}}`,
|
||||
b: `{"a":{"b":["2c37fed1-e8f8-4a84-8434-7851c7c8b5d6"]}}`,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "разные UUID — не эквивалентно",
|
||||
a: `{"nsxtUid":"2c37fed1-e8f8-4a84-8434-7851c7c8b5d6"}`,
|
||||
b: `{"nsxtUid":"86a01033-adac-424f-8296-aa10a001d6ed"}`,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "разные значения не-UUID — не эквивалентно",
|
||||
a: `{"clusterName":"shturval-dev-00"}`,
|
||||
b: `{"clusterName":"shturval-dev-01"}`,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "числа и bool остаются различимыми",
|
||||
a: `{"exVip":true,"count":3}`,
|
||||
b: `{"exVip":true,"count":"3"}`,
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if got := JSONStringsEquivalent(tc.a, tc.b); got != tc.want {
|
||||
t.Fatalf("JSONStringsEquivalent(%s, %s) = %v, want %v", tc.a, tc.b, got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLowercaseUUIDsInText(t *testing.T) {
|
||||
in := `{"nsxtUid":"2C37FED1-E8F8-4A84-8434-7851C7C8B5D6","name":"internet-ipv4-v1"}`
|
||||
want := `{"nsxtUid":"2c37fed1-e8f8-4a84-8434-7851c7c8b5d6","name":"internet-ipv4-v1"}`
|
||||
if got := LowercaseUUIDsInText(in); got != want {
|
||||
t.Fatalf("LowercaseUUIDsInText() = %s, want %s", got, want)
|
||||
}
|
||||
|
||||
// Текст без UUID не меняется (в т.ч. имя ipSpace с дефисами и цифрами).
|
||||
plain := `{"ipSpaceName":"internet-ipv4-v1","qosProfile":"QoS-100Mbit"}`
|
||||
if got := LowercaseUUIDsInText(plain); got != plain {
|
||||
t.Fatalf("LowercaseUUIDsInText() изменил текст без UUID: %s", got)
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,8 @@ import (
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
|
||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||
|
||||
"terraform-provider-nubes/internal/core/jsonutil"
|
||||
)
|
||||
|
||||
// jsonNormalizePlanModifier реализует planmodifier.String.
|
||||
@@ -49,5 +51,7 @@ func (m jsonNormalizePlanModifier) PlanModifyString(_ context.Context, req planm
|
||||
// Невалидный JSON — оставляем значение без изменений, не прерываем планирование.
|
||||
return
|
||||
}
|
||||
resp.PlanValue = types.StringValue(buf.String())
|
||||
// FIX(uuid-case): UUID-подстроки — к lowercase, чтобы план не расходился с
|
||||
// состоянием только из-за регистра UUID (облако отдаёт их в разных регистрах).
|
||||
resp.PlanValue = types.StringValue(jsonutil.LowercaseUUIDsInText(buf.String()))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package resources_core
|
||||
|
||||
import "testing"
|
||||
|
||||
// FIX(uuid-case): регрессия today's bug — adopt приостановленного инстанса Штурвала
|
||||
// падал с "required params mismatch ... startupConfiguration", потому что UUID
|
||||
// внутри JSON сравнивался с учётом регистра (plan — lowercase из config,
|
||||
// actual — UPPERCASE от API). Теперь UUID внутри JSON эквивалентны.
|
||||
func TestParamsEquivalentUUIDCaseInsideJSON(t *testing.T) {
|
||||
plan := `{"nsxtUid":"2c37fed1-e8f8-4a84-8434-7851c7c8b5d6","vdcUid":"d0937335-276b-475b-baa4-d8e6d16bad51","clusterName":"shturval-dev-00","exVip":true}`
|
||||
actual := `{"clusterName":"shturval-dev-00","exVip":true,"nsxtUid":"2C37FED1-E8F8-4A84-8434-7851C7C8B5D6","vdcUid":"d0937335-276b-475b-baa4-d8e6d16bad51"}`
|
||||
|
||||
if !paramsEquivalent(plan, actual) {
|
||||
t.Fatalf("paramsEquivalent() = false, ожидалось true: регистр UUID внутри JSON не должен влиять")
|
||||
}
|
||||
|
||||
// Реально другой nsxtUid — по-прежнему не эквивалентно.
|
||||
other := `{"clusterName":"shturval-dev-00","exVip":true,"nsxtUid":"86a01033-adac-424f-8296-aa10a001d6ed","vdcUid":"d0937335-276b-475b-baa4-d8e6d16bad51"}`
|
||||
if paramsEquivalent(plan, other) {
|
||||
t.Fatalf("paramsEquivalent() = true для другого nsxtUid — неверно")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user