diff --git a/provider/internal/resources_core/nsxt_snat_resource.go b/provider/internal/resources_core/nsxt_snat_resource.go index 497b630..e096694 100644 --- a/provider/internal/resources_core/nsxt_snat_resource.go +++ b/provider/internal/resources_core/nsxt_snat_resource.go @@ -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. Остальные параметры операции diff --git a/provider/internal/resources_core/org_ip_allocation_resource.go b/provider/internal/resources_core/org_ip_allocation_resource.go index db93e69..0533869 100644 --- a/provider/internal/resources_core/org_ip_allocation_resource.go +++ b/provider/internal/resources_core/org_ip_allocation_resource.go @@ -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. diff --git a/provider/internal/resources_core/org_ip_allocation_test.go b/provider/internal/resources_core/org_ip_allocation_test.go index 296d554..a9ffbb4 100644 --- a/provider/internal/resources_core/org_ip_allocation_test.go +++ b/provider/internal/resources_core/org_ip_allocation_test.go @@ -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) {