Compare commits
3
Commits
5fd64b68d0
...
03fff05117
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
03fff05117 | ||
|
|
621280a530 | ||
|
|
c9d73450b0 |
@@ -6,6 +6,9 @@
|
||||
.terraform.lock.hcl
|
||||
|
||||
# === Generated files (NOT code — regenerate from API) ===
|
||||
# ВАЖНО: provider/internal/provider/operation_timeouts.json — НЕ артефакт.
|
||||
# Это дефолтный конфиг таймаутов для go build/go test (см. operation_timeouts_embed.go),
|
||||
# поэтому он намеренно отслеживается git. Профильные значения — в TOOLS/config/<profile>/.
|
||||
provider/resources_yaml/
|
||||
provider/internal/resources_gen/
|
||||
|
||||
|
||||
@@ -14,6 +14,14 @@
|
||||
| Режим «заморозки» в конфиге стенда: `keep_on_destroy=true` (эдж/SNAT/квота IP), adopt для эджа, явный `suspend_on_destroy` у кластера | `DEV_STAND/FullPipe/{edge.tf,modifiers.tf,shturval.tf}` | `40aef87` |
|
||||
| Релиз dev-провайдера `2.0.22` (три платформы + SHA256SUMS/подпись, залито в реестр) | `VERSIONS.md` | `c29df21` |
|
||||
|
||||
## Проверка цикла на живом стенде
|
||||
|
||||
- `terraform destroy` (провайдер `2.0.22`): `0 added, 0 changed, 5 destroyed`, ошибок нет.
|
||||
Кластер и vDC ушли в `suspend`, эдж остался `running` с `ipSpaceName=internet-ipv4-v1`, квота IP — `count=3`,
|
||||
state пуст. Предупреждения: «заморожен, а не удалён» ×2 (кластер, vDC), «оставлен как есть» (эдж),
|
||||
«Аллокация IP не снималась» (квота), «SNAT не выключался».
|
||||
- Обратный ход (`apply` → adopt + `resume`) — следующий шаг, запускает пользователь.
|
||||
|
||||
Бэкап перед правкой: `TMP/backup_2026-09-24/shturval.tf.before-adopt`.
|
||||
|
||||
## Итоги диагностики кластера `shturval-dev-00`
|
||||
|
||||
@@ -216,6 +216,32 @@ SNAT останется включённым (Delete при `keep=true` печа
|
||||
|
||||
---
|
||||
|
||||
## 5.2. Проверено вживую: `destroy` = заморозка (24.09, вечер)
|
||||
|
||||
`terraform destroy` на `DEV_STAND/FullPipe` (провайдер `2.0.22`) — «Apply complete! Resources: 0 added, 0 changed, 5 destroyed», ошибок нет. Предупреждения вывода:
|
||||
|
||||
- `SNAT не выключался` — `keep_on_destroy = true`: `ipSpaceName` шлюза оставлен без изменений (NSXT-логика, `nsxt_snat_resource.go`);
|
||||
- `Ресурс оставлен как есть, а не удалён` — эдж (`vc_nsxt`, service_id=22) не менялся в облаке;
|
||||
- `Аллокация IP не снималась` — квота внешних IP организации оставлена без изменений;
|
||||
- `Ресурс заморожен, а не удалён` (×2) — vDC (`vc_vdc`, 21) и кластер (`k8s_sthutrval_cluster`, 150) переведены в `suspend`.
|
||||
|
||||
Состояние после destroy (проверено kubectl + API ЛК):
|
||||
|
||||
| Объект | Статус |
|
||||
|---|---|
|
||||
| `terraform state list` | пусто (все 5 ресурсов убраны из стейта) |
|
||||
| Кластер `94627ff4…` | `suspended`, `isSuspended=true`, не удалён |
|
||||
| vDC `d0937335…` | `suspended`, `isSuspended=true`, не удалён |
|
||||
| Эдж `2c37fed1…` | `running`, `ipSpaceName=internet-ipv4-v1` (SNAT включён) |
|
||||
| Орга `57eeacd1…` | `running`, `vIPConfigure=[{name:internet-ipv4-v1,count:3}]` (квота не тронута) |
|
||||
| Кластерный API `.146:6443` | TCP принимается эджем, но k8s не отвечает (`connection reset by peer`) — ВМ кластера спят |
|
||||
| Ingress `.148:443` | открыт (эдж/AVI живут) |
|
||||
|
||||
Осталось проверить обратный ход: `terraform apply` должен усыновить те же инстансы (`adopt_existing_on_create=true`)
|
||||
и разморозить их (`resume`) — запускает пользователь.
|
||||
|
||||
---
|
||||
|
||||
## 6. Мои ошибки в этой сессии (обязательно к фиксации)
|
||||
|
||||
1. Сказал, что apply «либо даст ошибку, либо создаст дубль кластера» — **неверно**: будет hard error
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -2,5 +2,15 @@ package provider
|
||||
|
||||
import _ "embed"
|
||||
|
||||
// operation_timeouts.json — ДЕФОЛТНЫЙ конфиг таймаутов операций для сборки из
|
||||
// исходников (без него падает go build / go test в provider/).
|
||||
//
|
||||
// Это НЕ сгенерированный артефакт и он СОЗНАТЕЛЬНО лежит в git:
|
||||
// при релизе скрипт TOOLS/scripts/03_build_and_upload_provider.sh копирует
|
||||
// провайдер во временный каталог и ПОДМЕНЯЕТ там этот файл профильным
|
||||
// (TOOLS/config/<profile>/operation_timeouts.json) — рабочее дерево не меняется.
|
||||
//
|
||||
// Менять значения для релиза нужно в TOOLS/config/<profile>/, а не здесь.
|
||||
//
|
||||
//go:embed operation_timeouts.json
|
||||
var operationTimeoutsConfigEmbedded []byte
|
||||
|
||||
@@ -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