fix(modifiers): ImportState заполняет Required-атрибуты
Было: ImportState ставил только id + organization/nsxt_uid, оставляя Required-атрибуты (vip_configure / ip_space_name) в null — Terraform не мог свести импорт с конфигом. - nsxt_snat: importIpSpaceName(live) — live-значение, иначе канон no-needed. - org_ip_allocation: importVipConfigure(live) — канонический JSON из live, иначе []. - Чистые хелперы + тесты TestImportVipConfigure/TestImportIpSpaceName. Проверено: go build ./... OK; go test ./internal/resources_core/... -short -> PASS. Terraform import проверяется владельцем.
This commit is contained in:
@@ -225,6 +225,24 @@ func (r *NsxtSnatResource) ImportState(ctx context.Context, req resource.ImportS
|
||||
uid := strings.TrimSpace(req.ID)
|
||||
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), uid)...)
|
||||
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("nsxt_uid"), uid)...)
|
||||
|
||||
// Required-атрибут ip_space_name нельзя оставить null после импорта: пользователь
|
||||
// обязан указать его в конфиге. Заполняем live-значением; если SNAT не выставлен —
|
||||
// каноном «выключено» (no-needed).
|
||||
var live map[string]string
|
||||
if r.client != nil {
|
||||
live, _ = r.client.GetInstanceStateParams(ctx, uid)
|
||||
}
|
||||
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("ip_space_name"), importIpSpaceName(live))...)
|
||||
}
|
||||
|
||||
// importIpSpaceName выбирает значение ip_space_name для состояния после импорта:
|
||||
// live-значение, если есть; иначе канон «SNAT выключен» (no-needed).
|
||||
func importIpSpaceName(live map[string]string) string {
|
||||
if v := strings.TrimSpace(live["ipSpaceName"]); v != "" {
|
||||
return v
|
||||
}
|
||||
return noNeededIpSpace
|
||||
}
|
||||
|
||||
// setSnat отправляет modify только с ipSpaceName. Остальные параметры операции
|
||||
|
||||
@@ -299,6 +299,25 @@ func (r *OrgIpAllocationResource) ImportState(ctx context.Context, req resource.
|
||||
uid := strings.TrimSpace(req.ID)
|
||||
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), uid)...)
|
||||
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("organization"), uid)...)
|
||||
|
||||
// Required-атрибут vip_configure нельзя оставить null после импорта.
|
||||
// Заполняем live-значением; если аллокации нет — пустым массивом.
|
||||
var live map[string]string
|
||||
if r.client != nil {
|
||||
live, _ = r.client.GetInstanceStateParams(ctx, uid)
|
||||
}
|
||||
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("vip_configure"), importVipConfigure(live))...)
|
||||
}
|
||||
|
||||
// importVipConfigure выбирает значение vip_configure для состояния после импорта:
|
||||
// канонический JSON из live (если есть непустые элементы), иначе пустой массив.
|
||||
func importVipConfigure(live map[string]string) string {
|
||||
if raw, ok := live["vIPConfigure"]; ok {
|
||||
if items, err := parseVipConfigure(raw); err == nil && len(items) > 0 {
|
||||
return formatVipConfigure(items)
|
||||
}
|
||||
}
|
||||
return "[]"
|
||||
}
|
||||
|
||||
// resolveOrganizationUID принимает имя организации из ЛК или её UUID и возвращает UUID.
|
||||
|
||||
@@ -51,6 +51,48 @@ func TestFormatVipConfigure_Canonical(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// importVipConfigure/importIpSpaceName — выбор значения Required-атрибутов при импорте
|
||||
// (B7: Required не должен оставаться null после import).
|
||||
func TestImportVipConfigure(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
live map[string]string
|
||||
want string
|
||||
}{
|
||||
{"live-аллокация", map[string]string{"vIPConfigure": `[{"name":"internet-ipv4-v1","count":"3"}]`}, `[{"name":"internet-ipv4-v1","count":"3"}]`},
|
||||
{"нет ключа", map[string]string{}, "[]"},
|
||||
{"nil", nil, "[]"},
|
||||
{"свежая орга [{}]", map[string]string{"vIPConfigure": `[{}]`}, "[]"},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if got := importVipConfigure(tc.live); got != tc.want {
|
||||
t.Fatalf("получено %q, ожидалось %q", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestImportIpSpaceName(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
live map[string]string
|
||||
want string
|
||||
}{
|
||||
{"live-значение", map[string]string{"ipSpaceName": "internet-ipv4-v1"}, "internet-ipv4-v1"},
|
||||
{"нет ключа", map[string]string{}, "no-needed"},
|
||||
{"nil", nil, "no-needed"},
|
||||
{"только пробелы", map[string]string{"ipSpaceName": " "}, "no-needed"},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if got := importIpSpaceName(tc.live); got != tc.want {
|
||||
t.Fatalf("получено %q, ожидалось %q", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Сравнение смысловое: `jsonencode` сортирует ключи по алфавиту (count раньше name),
|
||||
// но для нас это то же самое значение — переписывать state нельзя (иначе ложный дрейф).
|
||||
func TestVipAllocationsEqual_OrderInsensitive(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user