diff --git a/assets/pango/example/main.go b/assets/pango/example/main.go index 3a7df687..78ac7dfa 100644 --- a/assets/pango/example/main.go +++ b/assets/pango/example/main.go @@ -292,10 +292,9 @@ func checkEthernetLayer3Static(c *pango.Client, ctx context.Context) { location = ethernet.NewNgfwLocation() } - var importLocation []ethernet.ImportLocation api := ethernet.NewService(c) - reply, err := api.Create(ctx, *location, importLocation, entry) + reply, err := api.Create(ctx, *location, entry) if err != nil { log.Printf("Failed to create ethernet: %s", err) return @@ -329,10 +328,9 @@ func checkEthernetLayer3Dhcp(c *pango.Client, ctx context.Context) { location = ethernet.NewNgfwLocation() } - var importLocation []ethernet.ImportLocation api := ethernet.NewService(c) - reply, err := api.Create(ctx, *location, importLocation, entry) + reply, err := api.Create(ctx, *location, entry) if err != nil { log.Printf("Failed to create ethernet: %s", err) return @@ -355,10 +353,9 @@ func checkEthernetHa(c *pango.Client, ctx context.Context) { location = ethernet.NewNgfwLocation() } - var importLocation []ethernet.ImportLocation api := ethernet.NewService(c) - reply, err := api.Create(ctx, *location, importLocation, entry) + reply, err := api.Create(ctx, *location, entry) if err != nil { log.Printf("Failed to create ethernet: %s", err) return @@ -570,12 +567,11 @@ func checkVrZoneWithEthernet(c *pango.Client, ctx context.Context) { ethernetLocation = ethernet.NewNgfwLocation() } - var importLocation []ethernet.ImportLocation api := ethernet.NewService(c) interfacesToDelete := []string{"ethernet1/2", "ethernet1/3"} for _, iface := range interfacesToDelete { - err = api.Delete(ctx, *ethernetLocation, importLocation, iface) + err = api.Delete(ctx, *ethernetLocation, iface) if err != nil { log.Printf("Failed to delete ethernet: %s", err) return diff --git a/assets/terraform/internal/manager/entry_import.go b/assets/terraform/internal/manager/entry_import.go index 7d5d8a65..84324ecd 100644 --- a/assets/terraform/internal/manager/entry_import.go +++ b/assets/terraform/internal/manager/entry_import.go @@ -10,17 +10,17 @@ import ( "github.com/PaloAltoNetworks/pango/xmlapi" ) -type SDKImportableEntryService[E EntryObject, L EntryLocation, IL ImportLocation] interface { +type SDKImportableEntryService[E EntryObject, L EntryLocation] interface { CreateWithXpath(context.Context, string, E) error ReadWithXpath(context.Context, string, string) (E, error) List(context.Context, L, string, string, string) ([]E, error) UpdateWithXpath(context.Context, string, E, string) error - Delete(context.Context, L, []IL, ...string) error - ImportToLocations(context.Context, L, []IL, string) error - UnimportFromLocations(context.Context, L, []IL, []string) error + Delete(context.Context, L, ...string) error + ImportToLocation(context.Context, L, string, string) error + UnimportFromLocation(context.Context, L, string, string) error } -type ImportableEntryObjectManager[E EntryObject, L EntryLocation, IL ImportLocation, IS SDKImportableEntryService[E, L, IL]] struct { +type ImportableEntryObjectManager[E EntryObject, L EntryLocation, IS SDKImportableEntryService[E, L]] struct { batchSize int service IS client SDKClient @@ -28,8 +28,8 @@ type ImportableEntryObjectManager[E EntryObject, L EntryLocation, IL ImportLocat matcher func(E, E) bool } -func NewImportableEntryObjectManager[E EntryObject, L EntryLocation, IL ImportLocation, IS SDKImportableEntryService[E, L, IL]](client SDKClient, service IS, batchSize int, specifier func(E) (any, error), matcher func(E, E) bool) *ImportableEntryObjectManager[E, L, IL, IS] { - return &ImportableEntryObjectManager[E, L, IL, IS]{ +func NewImportableEntryObjectManager[E EntryObject, L EntryLocation, IS SDKImportableEntryService[E, L]](client SDKClient, service IS, batchSize int, specifier func(E) (any, error), matcher func(E, E) bool) *ImportableEntryObjectManager[E, L, IS] { + return &ImportableEntryObjectManager[E, L, IS]{ batchSize: batchSize, service: service, client: client, @@ -38,11 +38,11 @@ func NewImportableEntryObjectManager[E EntryObject, L EntryLocation, IL ImportLo } } -func (o *ImportableEntryObjectManager[E, L, IL, IS]) ReadMany(ctx context.Context, location L, entries []E) ([]E, error) { +func (o *ImportableEntryObjectManager[E, L, IS]) ReadMany(ctx context.Context, location L, entries []E) ([]E, error) { return nil, &Error{err: ErrInternal, message: "called ReadMany on an importable singular resource"} } -func (o *ImportableEntryObjectManager[E, L, IL, IS]) Read(ctx context.Context, location L, components []string, name string) (E, error) { +func (o *ImportableEntryObjectManager[E, L, IS]) Read(ctx context.Context, location L, components []string, name string) (E, error) { xpath, err := location.XpathWithComponents(o.client.Versioning(), append(components, util.AsEntryXpath(name))...) if err != nil { return *new(E), err @@ -59,7 +59,7 @@ func (o *ImportableEntryObjectManager[E, L, IL, IS]) Read(ctx context.Context, l return object, nil } -func (o *ImportableEntryObjectManager[E, L, IL, IS]) Create(ctx context.Context, location L, components []string, entry E) (E, error) { +func (o *ImportableEntryObjectManager[E, L, IS]) Create(ctx context.Context, location L, components []string, entry E) (E, error) { name := entry.EntryName() _, err := o.Read(ctx, location, components, name) @@ -84,7 +84,7 @@ func (o *ImportableEntryObjectManager[E, L, IL, IS]) Create(ctx context.Context, return o.Read(ctx, location, components, name) } -func (o *ImportableEntryObjectManager[E, L, IL, IS]) Update(ctx context.Context, location L, components []string, entry E, name string) (E, error) { +func (o *ImportableEntryObjectManager[E, L, IS]) Update(ctx context.Context, location L, components []string, entry E, name string) (E, error) { xpath, err := location.XpathWithComponents(o.client.Versioning(), append(components, util.AsEntryXpath(entry.EntryName()))...) if err != nil { return *new(E), &Error{err: err, message: "error during Update call"} @@ -98,7 +98,7 @@ func (o *ImportableEntryObjectManager[E, L, IL, IS]) Update(ctx context.Context, return o.service.ReadWithXpath(ctx, util.AsXpath(xpath), "get") } -func (o *ImportableEntryObjectManager[E, L, IL, IS]) Delete(ctx context.Context, location L, importLocations []IL, components []string, names []string) error { +func (o *ImportableEntryObjectManager[E, L, IS]) Delete(ctx context.Context, location L, components []string, names []string) error { deletes := xmlapi.NewChunkedMultiConfig(o.batchSize, len(names)) for _, elt := range names { @@ -123,10 +123,10 @@ func (o *ImportableEntryObjectManager[E, L, IL, IS]) Delete(ctx context.Context, return nil } -func (o *ImportableEntryObjectManager[E, L, IL, IS]) ImportToLocations(ctx context.Context, location L, importLocs []IL, entry string) error { - return o.service.ImportToLocations(ctx, location, importLocs, entry) +func (o *ImportableEntryObjectManager[E, L, IS]) ImportToLocation(ctx context.Context, location L, vsys string, entry string) error { + return o.service.ImportToLocation(ctx, location, vsys, entry) } -func (o *ImportableEntryObjectManager[E, L, IL, IS]) UnimportFromLocations(ctx context.Context, location L, importLocs []IL, entry string) error { - return o.service.UnimportFromLocations(ctx, location, importLocs, []string{entry}) +func (o *ImportableEntryObjectManager[E, L, IS]) UnimportFromLocation(ctx context.Context, location L, vsys string, entry string) error { + return o.service.UnimportFromLocation(ctx, location, vsys, entry) } diff --git a/assets/terraform/internal/manager/manager.go b/assets/terraform/internal/manager/manager.go index 268e8c34..cfeb459b 100644 --- a/assets/terraform/internal/manager/manager.go +++ b/assets/terraform/internal/manager/manager.go @@ -7,7 +7,6 @@ import ( "net/http" "net/url" - "github.com/PaloAltoNetworks/pango/util" "github.com/PaloAltoNetworks/pango/version" "github.com/PaloAltoNetworks/pango/xmlapi" ) @@ -57,9 +56,3 @@ type SDKClient interface { ChunkedMultiConfig(context.Context, *xmlapi.MultiConfig, bool, url.Values) ([]xmlapi.ChunkedMultiConfigResponse, error) MultiConfig(context.Context, *xmlapi.MultiConfig, bool, url.Values) ([]byte, *http.Response, *xmlapi.MultiConfigResponse, error) } - -type ImportLocation interface { - XpathForLocation(version.Number, util.ILocation) ([]string, error) - MarshalPangoXML([]string) (string, error) - UnmarshalPangoXML([]byte) ([]string, error) -} diff --git a/assets/terraform/test/resource_aggregate_interface_test.go b/assets/terraform/test/resource_aggregate_interface_test.go index 76fcb660..d46c25f5 100644 --- a/assets/terraform/test/resource_aggregate_interface_test.go +++ b/assets/terraform/test/resource_aggregate_interface_test.go @@ -2,6 +2,7 @@ package provider_test import ( "fmt" + "regexp" "testing" "github.com/hashicorp/terraform-plugin-testing/config" @@ -1221,3 +1222,289 @@ resource "panos_aggregate_interface" "iface" { } } ` + +// Test 1: TestAccAggregateInterface_Layer3_TemplateVsys1WithZone +func TestAccAggregateInterface_Layer3_TemplateVsys1WithZone(t *testing.T) { + t.Parallel() + + nameSuffix := acctest.RandStringFromCharSet(6, acctest.CharSetAlphaNum) + prefix := fmt.Sprintf("test-acc-%s", nameSuffix) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccAggregateInterfaceLayer3TemplateVsys1WithZone, + ConfigVariables: map[string]config.Variable{ + "prefix": config.StringVariable(prefix), + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue( + "panos_aggregate_interface.test", + tfjsonpath.New("name"), + knownvalue.StringExact("ae1"), + ), + statecheck.ExpectKnownValue( + "panos_zone.test", + tfjsonpath.New("name"), + knownvalue.StringExact(fmt.Sprintf("%s-zone", prefix)), + ), + statecheck.ExpectKnownValue( + "panos_zone.test", + tfjsonpath.New("network").AtMapKey("layer3"), + knownvalue.ListExact([]knownvalue.Check{ + knownvalue.StringExact("ae1"), + }), + ), + // Verify actual import in PAN-OS + ExpectVsysImportExists( + "panos_aggregate_interface.test", + "vsys1", + ImportTypeInterface, + ), + }, + }, + }, + }) +} + +const testAccAggregateInterfaceLayer3TemplateVsys1WithZone = ` +variable "prefix" { type = string } + +resource "panos_template" "test" { + location = { panorama = {} } + name = var.prefix +} + +resource "panos_aggregate_interface" "test" { + depends_on = [panos_template.test] + + location = { + template = { + name = panos_template.test.name + vsys = "vsys1" + } + } + + name = "ae1" + layer3 = {} +} + +resource "panos_zone" "test" { + depends_on = [panos_aggregate_interface.test] + + location = { + template = { + name = panos_template.test.name + } + } + + name = "${var.prefix}-zone" + enable_user_identification = true + + network = { + layer3 = ["ae1"] + } +} +` + +// Test 2: TestAccAggregateInterface_Layer3_TemplateVsysNullNoZone +func TestAccAggregateInterface_Layer3_TemplateVsysNullNoZone(t *testing.T) { + t.Parallel() + + nameSuffix := acctest.RandStringFromCharSet(6, acctest.CharSetAlphaNum) + prefix := fmt.Sprintf("test-acc-%s", nameSuffix) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccAggregateInterfaceLayer3TemplateVsysNullNoZone, + ConfigVariables: map[string]config.Variable{ + "prefix": config.StringVariable(prefix), + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue( + "panos_aggregate_interface.test", + tfjsonpath.New("name"), + knownvalue.StringExact("ae1"), + ), + // Verify NOT imported to vsys1 + ExpectVsysImportAbsent( + "panos_aggregate_interface.test", + "vsys1", + ImportTypeInterface, + ), + }, + }, + { + Config: testAccAggregateInterfaceLayer3TemplateVsysNullNoZoneStep2, + ConfigVariables: map[string]config.Variable{ + "prefix": config.StringVariable(prefix), + }, + ExpectError: regexp.MustCompile("Error in create"), + }, + }, + }) +} + +const testAccAggregateInterfaceLayer3TemplateVsysNullNoZone = ` +variable "prefix" { type = string } + +resource "panos_template" "test" { + location = { panorama = {} } + name = var.prefix +} + +resource "panos_aggregate_interface" "test" { + depends_on = [panos_template.test] + + location = { + template = { + name = panos_template.test.name + vsys = null + } + } + + name = "ae1" + layer3 = {} +} +` + +const testAccAggregateInterfaceLayer3TemplateVsysNullNoZoneStep2 = ` +variable "prefix" { type = string } + +resource "panos_template" "test" { + location = { panorama = {} } + name = var.prefix +} + +resource "panos_aggregate_interface" "test" { + depends_on = [panos_template.test] + + location = { + template = { + name = panos_template.test.name + vsys = null + } + } + + name = "ae1" + layer3 = {} +} + +resource "panos_zone" "test" { + depends_on = [panos_aggregate_interface.test] + + location = { + template = { + name = panos_template.test.name + } + } + + name = "${var.prefix}-zone" + + network = { + layer3 = ["ae1"] + } +} +` + +// Test 3: TestAccAggregateInterface_Layer3_TemplateVsysEmptyNoZone +func TestAccAggregateInterface_Layer3_TemplateVsysEmptyNoZone(t *testing.T) { + t.Parallel() + + nameSuffix := acctest.RandStringFromCharSet(6, acctest.CharSetAlphaNum) + prefix := fmt.Sprintf("test-acc-%s", nameSuffix) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccAggregateInterfaceLayer3TemplateVsysEmptyNoZone, + ConfigVariables: map[string]config.Variable{ + "prefix": config.StringVariable(prefix), + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue( + "panos_aggregate_interface.test", + tfjsonpath.New("name"), + knownvalue.StringExact("ae1"), + ), + }, + }, + { + Config: testAccAggregateInterfaceLayer3TemplateVsysEmptyNoZoneWithZone, + ConfigVariables: map[string]config.Variable{ + "prefix": config.StringVariable(prefix), + }, + ExpectError: regexp.MustCompile("Error in create"), + }, + }, + }) +} + +const testAccAggregateInterfaceLayer3TemplateVsysEmptyNoZone = ` +variable "prefix" { type = string } + +resource "panos_template" "test" { + location = { panorama = {} } + name = var.prefix +} + +resource "panos_aggregate_interface" "test" { + depends_on = [panos_template.test] + + location = { + template = { + name = panos_template.test.name + vsys = "" + } + } + + name = "ae1" + layer3 = {} +} +` + +const testAccAggregateInterfaceLayer3TemplateVsysEmptyNoZoneWithZone = ` +variable "prefix" { type = string } + +resource "panos_template" "test" { + location = { panorama = {} } + name = var.prefix +} + +resource "panos_aggregate_interface" "test" { + depends_on = [panos_template.test] + + location = { + template = { + name = panos_template.test.name + vsys = "" + } + } + + name = "ae1" + layer3 = {} +} + +resource "panos_zone" "test" { + depends_on = [panos_aggregate_interface.test] + + location = { + template = { + name = panos_template.test.name + } + } + + name = "${var.prefix}-zone" + + network = { + layer3 = ["ae1"] + } +} +` diff --git a/assets/terraform/test/resource_aggregate_layer3_subinterface_test.go b/assets/terraform/test/resource_aggregate_layer3_subinterface_test.go index 9b237b3c..a58be8da 100644 --- a/assets/terraform/test/resource_aggregate_layer3_subinterface_test.go +++ b/assets/terraform/test/resource_aggregate_layer3_subinterface_test.go @@ -1548,6 +1548,52 @@ resource "panos_aggregate_layer3_subinterface" "example" { } ` +const aggregateLayer3Subinterface_Import_Zone_Layer3 = ` +variable "prefix" { type = string } + +resource "panos_template" "example" { + location = { panorama = {} } + name = "${var.prefix}-tmpl" +} + +resource "panos_aggregate_interface" "parent" { + location = { + template = { + vsys = "vsys1" + name = panos_template.example.name + } + } + name = "ae1" + layer3 = {} +} + +resource "panos_aggregate_layer3_subinterface" "example" { + location = { template = { name = panos_template.example.name, vsys = "vsys1" } } + + parent = panos_aggregate_interface.parent.name + name = "ae1.1" + tag = 1 + + comment = "Test aggregate layer3 subinterface with zone import" + + ip = [{ + name = "192.0.2.1/24" + }] +} + +resource "panos_zone" "zone" { + depends_on = [panos_aggregate_layer3_subinterface.example] + + location = { template = { name = panos_template.example.name } } + + name = "${var.prefix}-zone" + + network = { + layer3 = ["ae1.1"] + } +} +` + func TestAccAggregateLayer3Subinterface_SdwanLinkSettings_UpstreamNat_StaticIp_Address(t *testing.T) { t.Parallel() t.Skip("missing required resource: sdwan profile") @@ -1619,3 +1665,246 @@ func TestAccAggregateLayer3Subinterface_SdwanLinkSettings_UpstreamNat_StaticIp_A }, }) } + + +func TestAccAggregateLayer3Subinterface_Import_Zone_Layer3(t *testing.T) { + t.Parallel() + + nameSuffix := acctest.RandStringFromCharSet(6, acctest.CharSetAlphaNum) + prefix := fmt.Sprintf("test-acc-%s", nameSuffix) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProviders, + Steps: []resource.TestStep{ + { + Config: aggregateLayer3Subinterface_Import_Zone_Layer3, + ConfigVariables: map[string]config.Variable{ + "prefix": config.StringVariable(prefix), + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue( + "panos_aggregate_layer3_subinterface.example", + tfjsonpath.New("name"), + knownvalue.StringExact("ae1.1"), + ), + statecheck.ExpectKnownValue( + "panos_aggregate_layer3_subinterface.example", + tfjsonpath.New("tag"), + knownvalue.Int64Exact(1), + ), + statecheck.ExpectKnownValue( + "panos_aggregate_layer3_subinterface.example", + tfjsonpath.New("comment"), + knownvalue.StringExact("Test aggregate layer3 subinterface with zone import"), + ), + statecheck.ExpectKnownValue( + "panos_aggregate_layer3_subinterface.example", + tfjsonpath.New("ip").AtSliceIndex(0).AtMapKey("name"), + knownvalue.StringExact("192.0.2.1/24"), + ), + statecheck.ExpectKnownValue( + "panos_zone.zone", + tfjsonpath.New("name"), + knownvalue.StringExact(fmt.Sprintf("%s-zone", prefix)), + ), + statecheck.ExpectKnownValue( + "panos_zone.zone", + tfjsonpath.New("network").AtMapKey("layer3"), + knownvalue.ListExact([]knownvalue.Check{ + knownvalue.StringExact("ae1.1"), + }), + ), + }, + }, + }, + }) +} + +const aggregateLayer3Subinterface_Ipv6_DhcpClient = ` +variable "prefix" { type = string } + +resource "panos_template" "example" { + location = { panorama = {} } + name = "${var.prefix}-tmpl" +} + +resource "panos_aggregate_interface" "parent" { + location = { + template = { + vsys = "vsys1" + name = panos_template.example.name + } + } + name = "ae1" + layer3 = {} +} + +resource "panos_aggregate_layer3_subinterface" "example" { + location = { template = { name = panos_template.example.name, vsys = "vsys1" } } + + parent = panos_aggregate_interface.parent.name + name = "ae1.1" + tag = 1 + + comment = "Test aggregate layer3 subinterface with IPv6 DHCP client (11.0.2+)" + + ipv6 = { + enabled = true + dhcp_client = { + enable = true + accept_ra_route = true + default_route_metric = 10 + preference = "high" + neighbor_discovery = { + dad_attempts = 1 + enable_dad = true + enable_ndp_monitor = true + ns_interval = 1000 + reachable_time = 30000 + } + prefix_delegation = { + enable = { + yes = { + pfx_pool_name = "test-pool" + prefix_len = 64 + prefix_len_hint = true + } + } + } + v6_options = { + duid_type = "duid-type-llt" + enable = { + yes = { + non_temp_addr = true + temp_addr = false + } + } + rapid_commit = true + } + } + } +} +` + +func TestAccAggregateLayer3Subinterface_Ipv6_DhcpClient(t *testing.T) { + t.Parallel() + + nameSuffix := acctest.RandStringFromCharSet(6, acctest.CharSetAlphaNum) + prefix := fmt.Sprintf("test-acc-%s", nameSuffix) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProviders, + Steps: []resource.TestStep{ + { + Config: aggregateLayer3Subinterface_Ipv6_DhcpClient, + ConfigVariables: map[string]config.Variable{ + "prefix": config.StringVariable(prefix), + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue( + "panos_aggregate_layer3_subinterface.example", + tfjsonpath.New("name"), + knownvalue.StringExact("ae1.1"), + ), + statecheck.ExpectKnownValue( + "panos_aggregate_layer3_subinterface.example", + tfjsonpath.New("tag"), + knownvalue.Int64Exact(1), + ), + statecheck.ExpectKnownValue( + "panos_aggregate_layer3_subinterface.example", + tfjsonpath.New("comment"), + knownvalue.StringExact("Test aggregate layer3 subinterface with IPv6 DHCP client (11.0.2+)"), + ), + statecheck.ExpectKnownValue( + "panos_aggregate_layer3_subinterface.example", + tfjsonpath.New("ipv6").AtMapKey("enabled"), + knownvalue.Bool(true), + ), + statecheck.ExpectKnownValue( + "panos_aggregate_layer3_subinterface.example", + tfjsonpath.New("ipv6").AtMapKey("dhcp_client").AtMapKey("enable"), + knownvalue.Bool(true), + ), + statecheck.ExpectKnownValue( + "panos_aggregate_layer3_subinterface.example", + tfjsonpath.New("ipv6").AtMapKey("dhcp_client").AtMapKey("accept_ra_route"), + knownvalue.Bool(true), + ), + statecheck.ExpectKnownValue( + "panos_aggregate_layer3_subinterface.example", + tfjsonpath.New("ipv6").AtMapKey("dhcp_client").AtMapKey("default_route_metric"), + knownvalue.Int64Exact(10), + ), + statecheck.ExpectKnownValue( + "panos_aggregate_layer3_subinterface.example", + tfjsonpath.New("ipv6").AtMapKey("dhcp_client").AtMapKey("preference"), + knownvalue.StringExact("high"), + ), + statecheck.ExpectKnownValue( + "panos_aggregate_layer3_subinterface.example", + tfjsonpath.New("ipv6").AtMapKey("dhcp_client").AtMapKey("neighbor_discovery").AtMapKey("dad_attempts"), + knownvalue.Int64Exact(1), + ), + statecheck.ExpectKnownValue( + "panos_aggregate_layer3_subinterface.example", + tfjsonpath.New("ipv6").AtMapKey("dhcp_client").AtMapKey("neighbor_discovery").AtMapKey("enable_dad"), + knownvalue.Bool(true), + ), + statecheck.ExpectKnownValue( + "panos_aggregate_layer3_subinterface.example", + tfjsonpath.New("ipv6").AtMapKey("dhcp_client").AtMapKey("neighbor_discovery").AtMapKey("enable_ndp_monitor"), + knownvalue.Bool(true), + ), + statecheck.ExpectKnownValue( + "panos_aggregate_layer3_subinterface.example", + tfjsonpath.New("ipv6").AtMapKey("dhcp_client").AtMapKey("neighbor_discovery").AtMapKey("ns_interval"), + knownvalue.Int64Exact(1000), + ), + statecheck.ExpectKnownValue( + "panos_aggregate_layer3_subinterface.example", + tfjsonpath.New("ipv6").AtMapKey("dhcp_client").AtMapKey("neighbor_discovery").AtMapKey("reachable_time"), + knownvalue.Int64Exact(30000), + ), + statecheck.ExpectKnownValue( + "panos_aggregate_layer3_subinterface.example", + tfjsonpath.New("ipv6").AtMapKey("dhcp_client").AtMapKey("prefix_delegation").AtMapKey("enable").AtMapKey("yes").AtMapKey("pfx_pool_name"), + knownvalue.StringExact("test-pool"), + ), + statecheck.ExpectKnownValue( + "panos_aggregate_layer3_subinterface.example", + tfjsonpath.New("ipv6").AtMapKey("dhcp_client").AtMapKey("prefix_delegation").AtMapKey("enable").AtMapKey("yes").AtMapKey("prefix_len"), + knownvalue.Int64Exact(64), + ), + statecheck.ExpectKnownValue( + "panos_aggregate_layer3_subinterface.example", + tfjsonpath.New("ipv6").AtMapKey("dhcp_client").AtMapKey("prefix_delegation").AtMapKey("enable").AtMapKey("yes").AtMapKey("prefix_len_hint"), + knownvalue.Bool(true), + ), + statecheck.ExpectKnownValue( + "panos_aggregate_layer3_subinterface.example", + tfjsonpath.New("ipv6").AtMapKey("dhcp_client").AtMapKey("v6_options").AtMapKey("duid_type"), + knownvalue.StringExact("duid-type-llt"), + ), + statecheck.ExpectKnownValue( + "panos_aggregate_layer3_subinterface.example", + tfjsonpath.New("ipv6").AtMapKey("dhcp_client").AtMapKey("v6_options").AtMapKey("enable").AtMapKey("yes").AtMapKey("non_temp_addr"), + knownvalue.Bool(true), + ), + statecheck.ExpectKnownValue( + "panos_aggregate_layer3_subinterface.example", + tfjsonpath.New("ipv6").AtMapKey("dhcp_client").AtMapKey("v6_options").AtMapKey("enable").AtMapKey("yes").AtMapKey("temp_addr"), + knownvalue.Bool(false), + ), + statecheck.ExpectKnownValue( + "panos_aggregate_layer3_subinterface.example", + tfjsonpath.New("ipv6").AtMapKey("dhcp_client").AtMapKey("v6_options").AtMapKey("rapid_commit"), + knownvalue.Bool(true), + ), + }, + }, + }, + }) +} diff --git a/assets/terraform/test/resource_ethernet_interface_test.go b/assets/terraform/test/resource_ethernet_interface_test.go index 386d3396..f1fe02e2 100644 --- a/assets/terraform/test/resource_ethernet_interface_test.go +++ b/assets/terraform/test/resource_ethernet_interface_test.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "os" + "regexp" "testing" "github.com/hashicorp/terraform-plugin-testing/config" @@ -275,3 +276,298 @@ func makePanosEthernetInterface_Layer3(label string) string { return fmt.Sprintf(configTpl, label) } + +const testAccEthernetInterfaceLayer3TemplateVsys1WithZone = ` +variable "prefix" { type = string } + +resource "panos_template" "test" { + location = { panorama = {} } + name = var.prefix +} + +resource "panos_ethernet_interface" "test" { + depends_on = [panos_template.test] + + location = { + template = { + name = panos_template.test.name + vsys = "vsys1" + } + } + + name = "ethernet1/1" + layer3 = {} +} + +resource "panos_zone" "test" { + depends_on = [panos_ethernet_interface.test] + + location = { + template = { + name = panos_template.test.name + } + } + + name = "${var.prefix}-zone" + enable_user_identification = true + + network = { + layer3 = ["ethernet1/1"] + } +} +` + +func TestAccEthernetInterface_Layer3_TemplateVsys1WithZone(t *testing.T) { + t.Parallel() + + nameSuffix := acctest.RandStringFromCharSet(6, acctest.CharSetAlphaNum) + prefix := fmt.Sprintf("test-acc-%s", nameSuffix) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccEthernetInterfaceLayer3TemplateVsys1WithZone, + ConfigVariables: map[string]config.Variable{ + "prefix": config.StringVariable(prefix), + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue( + "panos_ethernet_interface.test", + tfjsonpath.New("name"), + knownvalue.StringExact("ethernet1/1"), + ), + statecheck.ExpectKnownValue( + "panos_zone.test", + tfjsonpath.New("name"), + knownvalue.StringExact(fmt.Sprintf("%s-zone", prefix)), + ), + statecheck.ExpectKnownValue( + "panos_zone.test", + tfjsonpath.New("network").AtMapKey("layer3"), + knownvalue.ListExact([]knownvalue.Check{ + knownvalue.StringExact("ethernet1/1"), + }), + ), + // Verify actual import in PAN-OS + ExpectVsysImportExists( + "panos_ethernet_interface.test", + "vsys1", + ImportTypeInterface, + ), + }, + }, + }, + }) +} + +const testAccEthernetInterfaceLayer3TemplateVsysNullWithZone = ` +variable "prefix" { type = string } + +resource "panos_template" "test" { + location = { panorama = {} } + name = var.prefix +} + +resource "panos_ethernet_interface" "test" { + depends_on = [panos_template.test] + + location = { + template = { + name = panos_template.test.name + vsys = null + } + } + + name = "ethernet1/1" + layer3 = {} +} +` + +const testAccEthernetInterfaceLayer3TemplateVsysNullWithZoneStep2 = ` +variable "prefix" { type = string } + +resource "panos_template" "test" { + location = { panorama = {} } + name = var.prefix +} + +resource "panos_ethernet_interface" "test" { + depends_on = [panos_template.test] + + location = { + template = { + name = panos_template.test.name + vsys = null + } + } + + name = "ethernet1/1" + layer3 = {} +} + +resource "panos_zone" "test" { + depends_on = [panos_ethernet_interface.test] + + location = { + template = { + name = panos_template.test.name + } + } + + name = "${var.prefix}-zone" + + network = { + layer3 = ["ethernet1/1"] + } +} +` + +func TestAccEthernetInterface_Layer3_TemplateVsysNullWithZone(t *testing.T) { + t.Parallel() + + nameSuffix := acctest.RandStringFromCharSet(6, acctest.CharSetAlphaNum) + prefix := fmt.Sprintf("test-acc-%s", nameSuffix) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccEthernetInterfaceLayer3TemplateVsysNullWithZone, + ConfigVariables: map[string]config.Variable{ + "prefix": config.StringVariable(prefix), + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue( + "panos_ethernet_interface.test", + tfjsonpath.New("name"), + knownvalue.StringExact("ethernet1/1"), + ), + // Verify IS imported to vsys1 (due to default_value: vsys1 in spec) + ExpectVsysImportExists( + "panos_ethernet_interface.test", + "vsys1", + ImportTypeInterface, + ), + }, + }, + { + Config: testAccEthernetInterfaceLayer3TemplateVsysNullWithZoneStep2, + ConfigVariables: map[string]config.Variable{ + "prefix": config.StringVariable(prefix), + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue( + "panos_zone.test", + tfjsonpath.New("name"), + knownvalue.StringExact(fmt.Sprintf("%s-zone", prefix)), + ), + }, + }, + }, + }) +} + +const testAccEthernetInterfaceLayer3TemplateVsysEmptyNoZone = ` +variable "prefix" { type = string } + +resource "panos_template" "test" { + location = { panorama = {} } + name = var.prefix +} + +resource "panos_ethernet_interface" "test" { + depends_on = [panos_template.test] + + location = { + template = { + name = panos_template.test.name + vsys = "" + } + } + + name = "ethernet1/1" + layer3 = {} +} +` + +const testAccEthernetInterfaceLayer3TemplateVsysEmptyNoZoneWithZone = ` +variable "prefix" { type = string } + +resource "panos_template" "test" { + location = { panorama = {} } + name = var.prefix +} + +resource "panos_ethernet_interface" "test" { + depends_on = [panos_template.test] + + location = { + template = { + name = panos_template.test.name + vsys = "" + } + } + + name = "ethernet1/1" + layer3 = {} +} + +resource "panos_zone" "test" { + depends_on = [panos_ethernet_interface.test] + + location = { + template = { + name = panos_template.test.name + } + } + + name = "${var.prefix}-zone" + + network = { + layer3 = ["ethernet1/1"] + } +} +` + +func TestAccEthernetInterface_Layer3_TemplateVsysEmptyNoZone(t *testing.T) { + t.Parallel() + + nameSuffix := acctest.RandStringFromCharSet(6, acctest.CharSetAlphaNum) + prefix := fmt.Sprintf("test-acc-%s", nameSuffix) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccEthernetInterfaceLayer3TemplateVsysEmptyNoZone, + ConfigVariables: map[string]config.Variable{ + "prefix": config.StringVariable(prefix), + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue( + "panos_ethernet_interface.test", + tfjsonpath.New("name"), + knownvalue.StringExact("ethernet1/1"), + ), + // Verify NOT imported to vsys1 + ExpectVsysImportAbsent( + "panos_ethernet_interface.test", + "vsys1", + ImportTypeInterface, + ), + }, + }, + { + Config: testAccEthernetInterfaceLayer3TemplateVsysEmptyNoZoneWithZone, + ConfigVariables: map[string]config.Variable{ + "prefix": config.StringVariable(prefix), + }, + ExpectError: regexp.MustCompile("Error in create"), + }, + }, + }) +} diff --git a/assets/terraform/test/resource_security_policy_rules_test.go b/assets/terraform/test/resource_security_policy_rules_test.go index 31f351ce..2177cef1 100644 --- a/assets/terraform/test/resource_security_policy_rules_test.go +++ b/assets/terraform/test/resource_security_policy_rules_test.go @@ -5,7 +5,6 @@ import ( "encoding/base64" "encoding/json" "fmt" - "regexp" "strings" "testing" @@ -33,7 +32,7 @@ resource "panos_security_policy_rules" "rules" { location = { device_group = { name = panos_device_group.example.name - ruleset = "pre-ruleset" + rulebase = "pre-rulebase" } } @@ -77,7 +76,10 @@ func TestAccSecurityPolicyRulesImport(t *testing.T) { } } - importStateGenerateIDInvalid := importStateGenerateIDWithPrefixAndRules([]string{"rule-2", "rule-3", "rule-4", "rule-5"}) + // Import with a superset of rules (rule-5 doesn't exist on server). + // ReadMany now handles missing entries gracefully by returning only existing ones, + // so this import succeeds with the 3 existing rules rather than failing. + importStateGenerateIDPartial := importStateGenerateIDWithPrefixAndRules([]string{"rule-2", "rule-3", "rule-4", "rule-5"}) importStateGenerateIDValid := importStateGenerateIDWithPrefixAndRules([]string{"rule-2", "rule-3", "rule-4"}) resource.Test(t, resource.TestCase{ @@ -99,8 +101,7 @@ func TestAccSecurityPolicyRulesImport(t *testing.T) { }, ResourceName: "panos_security_policy_rules.imported", ImportState: true, - ImportStateIdFunc: importStateGenerateIDInvalid, - ExpectError: regexp.MustCompile("Not all entries found on the server"), + ImportStateIdFunc: importStateGenerateIDPartial, }, { Config: configStep2, diff --git a/assets/terraform/test/resource_virtual_router_test.go b/assets/terraform/test/resource_virtual_router_test.go index e47c884c..98f91831 100644 --- a/assets/terraform/test/resource_virtual_router_test.go +++ b/assets/terraform/test/resource_virtual_router_test.go @@ -1,14 +1,17 @@ package provider_test import ( + "context" "fmt" "testing" + "github.com/PaloAltoNetworks/pango/network/virtual_router" "github.com/hashicorp/terraform-plugin-testing/config" "github.com/hashicorp/terraform-plugin-testing/helper/acctest" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/knownvalue" "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/terraform" "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" ) @@ -3956,3 +3959,233 @@ resource "panos_virtual_router" "test" { } } ` + +// TestAccPanosVirtualRouter_VsysImport_Lifecycle verifies that virtual router +// import status correctly follows vsys field changes through: +// empty → vsys1 → vsys2 → empty +func TestAccPanosVirtualRouter_VsysImport_Lifecycle(t *testing.T) { + t.Parallel() + + nameSuffix := acctest.RandStringFromCharSet(6, acctest.CharSetAlphaNum) + templateName := fmt.Sprintf("acc-vr-import-lifecycle-%s", nameSuffix) + routerName := fmt.Sprintf("test-vr-lifecycle-%s", nameSuffix) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProviders, + Steps: []resource.TestStep{ + { + Config: panosVirtualRouter_VsysImport_EmptyVsys_Tmpl, + ConfigVariables: map[string]config.Variable{ + "template_name": config.StringVariable(templateName), + "router_name": config.StringVariable(routerName), + }, + ConfigStateChecks: []statecheck.StateCheck{ + ExpectVsysImportAbsent("panos_virtual_router.test", "vsys1", ImportTypeVirtualRouter), + ExpectVsysImportAbsent("panos_virtual_router.test", "vsys2", ImportTypeVirtualRouter), + }, + }, + { + Config: panosVirtualRouter_VsysImport_Vsys1_Tmpl, + ConfigVariables: map[string]config.Variable{ + "template_name": config.StringVariable(templateName), + "router_name": config.StringVariable(routerName), + }, + ConfigStateChecks: []statecheck.StateCheck{ + ExpectVsysImportExists("panos_virtual_router.test", "vsys1", ImportTypeVirtualRouter), + ExpectVsysImportAbsent("panos_virtual_router.test", "vsys2", ImportTypeVirtualRouter), + }, + }, + { + Config: panosVirtualRouter_VsysImport_Vsys2_Tmpl, + ConfigVariables: map[string]config.Variable{ + "template_name": config.StringVariable(templateName), + "router_name": config.StringVariable(routerName), + }, + ConfigStateChecks: []statecheck.StateCheck{ + ExpectVsysImportAbsent("panos_virtual_router.test", "vsys1", ImportTypeVirtualRouter), + ExpectVsysImportExists("panos_virtual_router.test", "vsys2", ImportTypeVirtualRouter), + }, + }, + { + Config: panosVirtualRouter_VsysImport_EmptyVsys_Tmpl, + ConfigVariables: map[string]config.Variable{ + "template_name": config.StringVariable(templateName), + "router_name": config.StringVariable(routerName), + }, + ConfigStateChecks: []statecheck.StateCheck{ + ExpectVsysImportAbsent("panos_virtual_router.test", "vsys1", ImportTypeVirtualRouter), + ExpectVsysImportAbsent("panos_virtual_router.test", "vsys2", ImportTypeVirtualRouter), + }, + }, + }, + }) +} + +// TestAccPanosVirtualRouter_VsysImport_ManualDrift verifies that Terraform does not +// interfere with manual imports when vsys is empty (not managed by Terraform). +func TestAccPanosVirtualRouter_VsysImport_ManualDrift(t *testing.T) { + t.Parallel() + + nameSuffix := acctest.RandStringFromCharSet(6, acctest.CharSetAlphaNum) + templateName := fmt.Sprintf("acc-vr-drift-%s", nameSuffix) + routerName := fmt.Sprintf("test-vr-drift-%s", nameSuffix) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProviders, + Steps: []resource.TestStep{ + { + // Create with empty vsys - Terraform doesn't manage import + Config: panosVirtualRouter_VsysImport_EmptyVsys_Tmpl, + ConfigVariables: map[string]config.Variable{ + "template_name": config.StringVariable(templateName), + "router_name": config.StringVariable(routerName), + }, + ConfigStateChecks: []statecheck.StateCheck{ + ExpectVsysImportAbsent("panos_virtual_router.test", "vsys1", ImportTypeVirtualRouter), + }, + }, + { + // Manually import outside Terraform + Config: panosVirtualRouter_VsysImport_EmptyVsys_Tmpl, + ConfigVariables: map[string]config.Variable{ + "template_name": config.StringVariable(templateName), + "router_name": config.StringVariable(routerName), + }, + Check: resource.ComposeTestCheckFunc( + manuallyImportVirtualRouterToVsys(templateName, routerName, "vsys1"), + ), + ConfigStateChecks: []statecheck.StateCheck{ + // Verify manual import exists + ExpectVsysImportExists("panos_virtual_router.test", "vsys1", ImportTypeVirtualRouter), + }, + // PlanOnly with ExpectNonEmptyPlan=false would verify no changes in plan + // but that's not directly supported. The re-apply in this step serves + // as verification that Terraform doesn't try to remove the manual import. + }, + { + // Cleanup: manually unimport before destroy + Config: panosVirtualRouter_VsysImport_EmptyVsys_Tmpl, + ConfigVariables: map[string]config.Variable{ + "template_name": config.StringVariable(templateName), + "router_name": config.StringVariable(routerName), + }, + Check: resource.ComposeTestCheckFunc( + manuallyUnimportVirtualRouterFromVsys(templateName, routerName, "vsys1"), + ), + ConfigStateChecks: []statecheck.StateCheck{ + ExpectVsysImportAbsent("panos_virtual_router.test", "vsys1", ImportTypeVirtualRouter), + }, + }, + }, + }) +} + +// manuallyImportVirtualRouterToVsys imports a virtual router to a vsys outside of Terraform +func manuallyImportVirtualRouterToVsys(templateName, routerName, vsysName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + // Build location + location := virtual_router.NewTemplateLocation() + location.Template.Template = templateName + + // Import to specified vsys + svc := virtual_router.NewService(sdkClient) + err := svc.ImportToLocation(context.Background(), *location, vsysName, routerName) + if err != nil { + return fmt.Errorf("manual import of %s to %s failed: %w", routerName, vsysName, err) + } + + return nil + } +} + +// manuallyUnimportVirtualRouterFromVsys unimports a virtual router from a vsys outside of Terraform +func manuallyUnimportVirtualRouterFromVsys(templateName, routerName, vsysName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + // Build location + location := virtual_router.NewTemplateLocation() + location.Template.Template = templateName + + // Unimport from specified vsys + svc := virtual_router.NewService(sdkClient) + err := svc.UnimportFromLocation(context.Background(), *location, vsysName, routerName) + if err != nil { + return fmt.Errorf("manual unimport of %s from %s failed: %w", routerName, vsysName, err) + } + + return nil + } +} + +const panosVirtualRouter_VsysImport_EmptyVsys_Tmpl = ` +variable "template_name" { type = string } +variable "router_name" { type = string } + +resource "panos_template" "template" { + name = var.template_name + location = { + panorama = { + panorama_device = "localhost.localdomain" + } + } +} + +resource "panos_virtual_router" "test" { + location = { + template = { + name = panos_template.template.name + vsys = "" # Empty - no import + } + } + name = var.router_name +} +` + +const panosVirtualRouter_VsysImport_Vsys1_Tmpl = ` +variable "template_name" { type = string } +variable "router_name" { type = string } + +resource "panos_template" "template" { + name = var.template_name + location = { + panorama = { + panorama_device = "localhost.localdomain" + } + } +} + +resource "panos_virtual_router" "test" { + location = { + template = { + name = panos_template.template.name + vsys = "vsys1" + } + } + name = var.router_name +} +` + +const panosVirtualRouter_VsysImport_Vsys2_Tmpl = ` +variable "template_name" { type = string } +variable "router_name" { type = string } + +resource "panos_template" "template" { + name = var.template_name + location = { + panorama = { + panorama_device = "localhost.localdomain" + } + } +} + +resource "panos_virtual_router" "test" { + location = { + template = { + name = panos_template.template.name + vsys = "vsys2" + } + } + name = var.router_name +} +` diff --git a/assets/terraform/test/vsys_import_check_test.go b/assets/terraform/test/vsys_import_check_test.go new file mode 100644 index 00000000..e39b8ba2 --- /dev/null +++ b/assets/terraform/test/vsys_import_check_test.go @@ -0,0 +1,168 @@ +package provider_test + +import ( + "context" + "encoding/xml" + "fmt" + + "github.com/PaloAltoNetworks/pango/errors" + "github.com/PaloAltoNetworks/pango/util" + "github.com/PaloAltoNetworks/pango/xmlapi" + tfjson "github.com/hashicorp/terraform-json" + "github.com/hashicorp/terraform-plugin-testing/statecheck" +) + +type ImportType string + +const ( + ImportTypeInterface ImportType = "interface" + ImportTypeVirtualRouter ImportType = "virtual-router" +) + +type expectVsysImport struct { + ResourceName string // e.g., "panos_virtual_router.test" + VsysName string // e.g., "vsys1" + ImportType ImportType // "interface" or "virtual-router" + ShouldBeImported bool // true = expect import, false = expect no import +} + +func ExpectVsysImportExists(resourceName, vsysName string, importType ImportType) *expectVsysImport { + return &expectVsysImport{ + ResourceName: resourceName, + VsysName: vsysName, + ImportType: importType, + ShouldBeImported: true, + } +} + +func ExpectVsysImportAbsent(resourceName, vsysName string, importType ImportType) *expectVsysImport { + return &expectVsysImport{ + ResourceName: resourceName, + VsysName: vsysName, + ImportType: importType, + ShouldBeImported: false, + } +} + +func (o *expectVsysImport) CheckState(ctx context.Context, req statecheck.CheckStateRequest, resp *statecheck.CheckStateResponse) { + // Find resource in Terraform state + var resource *tfjson.StateResource + for _, r := range req.State.Values.RootModule.Resources { + if r.Address == o.ResourceName { + resource = r + break + } + } + + if resource == nil { + resp.Error = fmt.Errorf("resource %s not found in state", o.ResourceName) + return + } + + // Get the actual resource name (e.g., "ethernet1/1", "vr-1") + resourceId, ok := resource.AttributeValues["name"].(string) + if !ok || resourceId == "" { + resp.Error = fmt.Errorf("resource %s has no name attribute", o.ResourceName) + return + } + + // Extract template name from Terraform state + // location.template.name is nested: location -> template -> name + locationMap, ok := resource.AttributeValues["location"].(map[string]interface{}) + if !ok { + resp.Error = fmt.Errorf("resource %s has no location attribute", o.ResourceName) + return + } + + templateMap, ok := locationMap["template"].(map[string]interface{}) + if !ok { + resp.Error = fmt.Errorf("resource %s has no location.template attribute", o.ResourceName) + return + } + + templateName, ok := templateMap["name"].(string) + if !ok || templateName == "" { + resp.Error = fmt.Errorf("resource %s has no location.template.name attribute", o.ResourceName) + return + } + + // Build XPath for template-based vsys import list + xpath := []string{ + "config", "devices", + util.AsEntryXpath("localhost.localdomain"), + "template", util.AsEntryXpath(templateName), + "config", "devices", + util.AsEntryXpath("localhost.localdomain"), + "vsys", util.AsEntryXpath(o.VsysName), + "import", "network", string(o.ImportType), + } + + // Make API call + cmd := &xmlapi.Config{ + Action: "get", + Xpath: util.AsXpath(xpath), + } + + bytes, _, err := sdkClient.Communicate(ctx, cmd, false, nil) + if err != nil { + // ObjectNotFound means import list is empty - this is acceptable for negative checks + if errors.IsObjectNotFound(err) { + if o.ShouldBeImported { + resp.Error = fmt.Errorf("expected %s %q to be imported to vsys %q, but vsys has no %s imports", + o.ImportType, resourceId, o.VsysName, o.ImportType) + } + // For negative check, ObjectNotFound is success (no imports = not imported) + return + } + resp.Error = fmt.Errorf("failed to check vsys import state: %w", err) + return + } + + // Parse XML response based on import type + var members []string + if o.ImportType == ImportTypeVirtualRouter { + var vrResponse struct { + Members []struct { + Name string `xml:",chardata"` + } `xml:"result>virtual-router>member"` + } + if err := xml.Unmarshal(bytes, &vrResponse); err != nil { + resp.Error = fmt.Errorf("failed to parse virtual-router import response: %w", err) + return + } + for _, m := range vrResponse.Members { + members = append(members, m.Name) + } + } else { + var ifResponse struct { + Members []struct { + Name string `xml:",chardata"` + } `xml:"result>interface>member"` + } + if err := xml.Unmarshal(bytes, &ifResponse); err != nil { + resp.Error = fmt.Errorf("failed to parse interface import response: %w", err) + return + } + for _, m := range ifResponse.Members { + members = append(members, m.Name) + } + } + + // Check if resource is in import list + found := false + for _, member := range members { + if member == resourceId { + found = true + break + } + } + + // Validate based on expectation + if o.ShouldBeImported && !found { + resp.Error = fmt.Errorf("expected %s %q to be imported to vsys %q, but it was not found in import list (found: %v)", + o.ImportType, resourceId, o.VsysName, members) + } else if !o.ShouldBeImported && found { + resp.Error = fmt.Errorf("expected %s %q to NOT be imported to vsys %q, but it was found in import list", + o.ImportType, resourceId, o.VsysName) + } +} diff --git a/pkg/generate/generator.go b/pkg/generate/generator.go index eced8268..1833570a 100644 --- a/pkg/generate/generator.go +++ b/pkg/generate/generator.go @@ -282,7 +282,6 @@ func (c *Creator) parseTemplate(templateName string) (*template.Template, error) return translate.RenderImports(c.Spec, templateTypes...) }, "SupportedMethod": func(method object.GoSdkMethod) bool { return c.Spec.SupportedMethod(method) }, - "RenderEntryImportStructs": func() (string, error) { return translate.RenderEntryImportStructs(c.Spec) }, "RenderLocationFilter": func() (string, error) { return translate.RenderLocationFilter(c.Spec) }, "packageName": translate.PackageName, "locationType": translate.LocationType, diff --git a/pkg/naming/names.go b/pkg/naming/names.go index dab0b8c7..a20527c9 100644 --- a/pkg/naming/names.go +++ b/pkg/naming/names.go @@ -133,6 +133,11 @@ func Underscore(prefix, value, suffix string) string { return builder.String() } +func Dashed(prefix, value, suffix string) string { + underscored := Underscore(prefix, value, suffix) + return strings.ReplaceAll(underscored, "_", "-") +} + // writeRuneAndApplyChangesForUnderscore contains logic to check all conditions for create under_score names and writes rune value. func writeRuneAndApplyChangesForUnderscore(runeValue int32, builder *strings.Builder, isFirstCharacter *bool) { if shouldResetFirstCharacterFlag(runeValue) { diff --git a/pkg/properties/namevariant.go b/pkg/properties/namevariant.go index f54c6819..87320433 100644 --- a/pkg/properties/namevariant.go +++ b/pkg/properties/namevariant.go @@ -9,6 +9,7 @@ import ( type NameVariant struct { Original string Underscore string + Dashed string CamelCase string LowerCamelCase string } @@ -16,6 +17,7 @@ type NameVariant struct { func NewNameVariant(name string) *NameVariant { return &NameVariant{ Original: name, + Dashed: naming.Dashed("", name, ""), Underscore: naming.Underscore("", name, ""), CamelCase: naming.CamelCase("", name, "", true), LowerCamelCase: naming.CamelCase("", name, "", false), diff --git a/pkg/properties/normalized.go b/pkg/properties/normalized.go index 71479dfe..2de1bec0 100644 --- a/pkg/properties/normalized.go +++ b/pkg/properties/normalized.go @@ -15,6 +15,7 @@ import ( "text/template" "github.com/paloaltonetworks/pan-os-codegen/pkg/content" + "github.com/paloaltonetworks/pan-os-codegen/pkg/schema/imports" "github.com/paloaltonetworks/pan-os-codegen/pkg/schema/object" "github.com/paloaltonetworks/pan-os-codegen/pkg/schema/parameter" "github.com/paloaltonetworks/pan-os-codegen/pkg/schema/validator" @@ -31,7 +32,7 @@ type Normalization struct { PanosXpath PanosXpath `json:"panos_xpath" yaml:"panos_xpath"` Locations map[string]*Location `json:"locations" yaml:"locations"` Entry *Entry `json:"entry" yaml:"entry"` - Imports []Import `json:"imports" yaml:"imports"` + Imports imports.Import `json:"imports" yaml:"imports"` Version string `json:"version" yaml:"version"` Spec *Spec `json:"spec" yaml:"spec"` Const map[string]*Const `json:"const" yaml:"const"` @@ -42,27 +43,6 @@ type PanosXpath struct { Variables []object.PanosXpathVariable `yaml:"vars"` } -type Import struct { - Variant *NameVariant - Type *NameVariant - Locations map[string]ImportLocation -} - -type ImportLocation struct { - Name *NameVariant - SpecOrder int - Required bool - XpathElements []string - XpathVariables map[string]ImportXpathVariable -} - -type ImportXpathVariable struct { - Name *NameVariant - SpecOrder int - Description string - Default string -} - type TerraformResourceType string const ( @@ -735,6 +715,7 @@ func schemaToSpec(object object.Object) (*Normalization, error) { PluralDescription: object.TerraformConfig.PluralDescription, }, Locations: make(map[string]*Location), + Imports: object.Imports, GoSdkSkip: object.GoSdkConfig.Skip, GoSdkPath: object.GoSdkConfig.Package, GoSdkSupportedMethods: object.GoSdkConfig.SupportedMethods, @@ -819,51 +800,6 @@ func schemaToSpec(object object.Object) (*Normalization, error) { } - var imports []Import - for _, elt := range object.Imports { - locations := make(map[string]ImportLocation, len(elt.Locations)) - for idx, location := range elt.Locations { - schemaXpathVars := make(map[string]xpathschema.Variable, len(location.Xpath.Variables)) - xpathVars := make(map[string]ImportXpathVariable, len(location.Xpath.Variables)) - for variableIdx, xpathVariable := range location.Xpath.Variables { - schemaXpathVars[xpathVariable.Name] = xpathVariable - - _, found := xpathVars[xpathVariable.Name] - if found { - panic(fmt.Sprintf("Variable duplicate: %s", xpathVariable.Name)) - } - - xpathVars[xpathVariable.Name] = ImportXpathVariable{ - Name: NewNameVariant(xpathVariable.Name), - SpecOrder: variableIdx, - Description: xpathVariable.Description, - Default: xpathVariable.Default, - } - } - - var xpath []string - xpath = append(xpath, location.Xpath.Elements...) - - locations[location.Name] = ImportLocation{ - Name: NewNameVariant(location.Name), - SpecOrder: idx, - Required: location.Required, - XpathVariables: xpathVars, - XpathElements: xpath, - } - } - - imports = append(imports, Import{ - Type: NewNameVariant(elt.Type), - Variant: NewNameVariant(elt.Variant), - Locations: locations, - }) - } - - if len(imports) > 0 { - spec.Imports = imports - } - consts := make(map[string]*Const) for idx, param := range object.Spec.Parameters { specParam, err := schemaParameterToSpecParameter(param) @@ -969,26 +905,6 @@ func (spec *Normalization) OrderedLocations() []*Location { return elements } -func (o *Import) OrderedLocations() []*ImportLocation { - elements := make([]*ImportLocation, len(o.Locations)) - - for _, elt := range o.Locations { - elements[elt.SpecOrder] = &elt - } - - return elements -} - -func (o *ImportLocation) OrderedXpathVariables() []*ImportXpathVariable { - elements := make([]*ImportXpathVariable, len(o.XpathVariables)) - - for _, elt := range o.XpathVariables { - elements[elt.SpecOrder] = &elt - } - - return elements -} - // AddNameVariantsForLocation add name variants for location (under_score and CamelCase). func (spec *Normalization) AddNameVariantsForLocation() error { for key, location := range spec.Locations { diff --git a/pkg/properties/provider_file.go b/pkg/properties/provider_file.go index e4e86245..c2b10b8b 100644 --- a/pkg/properties/provider_file.go +++ b/pkg/properties/provider_file.go @@ -64,6 +64,10 @@ func (o TerraformNameProvider) ActionStructName() string { return fmt.Sprintf("%sAction", o.StructName) } +func (o TerraformNameProvider) IdentityModelStructName() string { + return fmt.Sprintf("%sIdentityModel", o.ResourceStructName) +} + type TerraformSpecFlags uint const ( diff --git a/pkg/schema/imports/import.go b/pkg/schema/imports/import.go index 006a544a..1950937a 100644 --- a/pkg/schema/imports/import.go +++ b/pkg/schema/imports/import.go @@ -1,11 +1,25 @@ package imports -import ( - "github.com/paloaltonetworks/pan-os-codegen/pkg/schema/location" -) - +// Import defines vsys import configuration for PAN-OS resources that need to be +// imported into a vsys (e.g., network interfaces, virtual routers, subinterfaces). type Import struct { - Variant string `yaml:"variant"` - Type string `yaml:"type"` - Locations []location.Location `yaml:"locations"` + // Variants lists PAN-OS interface modes or location types that require vsys import. + // Specific values (e.g., ["layer2", "layer3"]) mean import is only required when + // the resource's location uses one of those modes; a conditional check is generated + // in the Terraform provider for each listed variant. + // + // The special wildcard value ["*"] means import into vsys regardless of interface + // mode — used for resources like tunnel interfaces, virtual routers, and subinterfaces + // that are always importable. When "*" is present, no mode check is generated; + // locationRequiresImport is set unconditionally to true. + // + // If "*" appears alongside specific variants (e.g., ["layer2", "*"]), the wildcard + // is filtered out and only the specific variant checks are generated. + Variants []string `yaml:"variants"` + + // Target is the PAN-OS XML element name for the import target (e.g., "interface"). + Target string `yaml:"target"` + + // DefaultValue is the default vsys value to use for import, if any. + DefaultValue *string `yaml:"default_value,omitempty"` } diff --git a/pkg/schema/imports/import_test.go b/pkg/schema/imports/import_test.go new file mode 100644 index 00000000..3153c1d2 --- /dev/null +++ b/pkg/schema/imports/import_test.go @@ -0,0 +1,106 @@ +package imports_test + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "gopkg.in/yaml.v3" + + "github.com/paloaltonetworks/pan-os-codegen/pkg/schema/imports" +) + +var _ = Describe("Import", func() { + Context("when unmarshalling from YAML", func() { + Context("with default_value present", func() { + It("should parse all fields correctly including default_value", func() { + yamlData := `target: interface +variants: + - layer2 + - layer3 +default_value: vsys1` + + var result imports.Import + err := yaml.Unmarshal([]byte(yamlData), &result) + + Expect(err).ShouldNot(HaveOccurred()) + Expect(result.Target).Should(Equal("interface")) + Expect(result.Variants).Should(Equal([]string{"layer2", "layer3"})) + Expect(result.DefaultValue).ShouldNot(BeNil()) + Expect(*result.DefaultValue).Should(Equal("vsys1")) + }) + }) + + Context("without default_value", func() { + It("should set DefaultValue to nil", func() { + yamlData := `target: interface +variants: + - layer2 + - layer3` + + var result imports.Import + err := yaml.Unmarshal([]byte(yamlData), &result) + + Expect(err).ShouldNot(HaveOccurred()) + Expect(result.Target).Should(Equal("interface")) + Expect(result.Variants).Should(Equal([]string{"layer2", "layer3"})) + Expect(result.DefaultValue).Should(BeNil()) + }) + }) + + Context("with empty default_value", func() { + It("should parse empty string as the default_value", func() { + yamlData := `target: interface +variants: + - "*" +default_value: ""` + + var result imports.Import + err := yaml.Unmarshal([]byte(yamlData), &result) + + Expect(err).ShouldNot(HaveOccurred()) + Expect(result.Target).Should(Equal("interface")) + Expect(result.Variants).Should(Equal([]string{"*"})) + Expect(result.DefaultValue).ShouldNot(BeNil()) + Expect(*result.DefaultValue).Should(Equal("")) + }) + }) + }) + + Context("when marshalling to YAML", func() { + Context("with DefaultValue set", func() { + It("should include default_value in output", func() { + input := imports.Import{ + Target: "interface", + Variants: []string{"layer2", "layer3"}, + DefaultValue: stringPtr("vsys1"), + } + + data, err := yaml.Marshal(input) + + Expect(err).ShouldNot(HaveOccurred()) + output := string(data) + Expect(output).Should(ContainSubstring("default_value: vsys1")) + }) + }) + + Context("with DefaultValue nil", func() { + It("should omit default_value from output due to omitempty tag", func() { + input := imports.Import{ + Target: "interface", + Variants: []string{"layer2", "layer3"}, + DefaultValue: nil, + } + + data, err := yaml.Marshal(input) + + Expect(err).ShouldNot(HaveOccurred()) + output := string(data) + Expect(output).ShouldNot(ContainSubstring("default_value")) + }) + }) + }) +}) + +// Helper function to create a pointer to a string +func stringPtr(s string) *string { + return &s +} diff --git a/pkg/schema/imports/imports_suite_test.go b/pkg/schema/imports/imports_suite_test.go new file mode 100644 index 00000000..2fdf6f56 --- /dev/null +++ b/pkg/schema/imports/imports_suite_test.go @@ -0,0 +1,15 @@ +package imports_test + +import ( + "log/slog" + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestImports(t *testing.T) { + slog.SetDefault(slog.New(slog.NewTextHandler(GinkgoWriter, &slog.HandlerOptions{Level: slog.LevelDebug}))) + RegisterFailHandler(Fail) + RunSpecs(t, "Imports Suite") +} diff --git a/pkg/schema/object/object.go b/pkg/schema/object/object.go index e544d49c..0e6ccbda 100644 --- a/pkg/schema/object/object.go +++ b/pkg/schema/object/object.go @@ -114,7 +114,7 @@ type Object struct { GoSdkConfig *GoSdkConfig `yaml:"go_sdk_config"` Locations []location.Location `yaml:"locations"` Entries []Entry `yaml:"entries"` - Imports []imports.Import `yaml:"imports"` + Imports imports.Import `yaml:"imports"` Spec *Spec `yaml:"spec"` } diff --git a/pkg/translate/import_location.go b/pkg/translate/import_location.go index 6eade444..b78d16f9 100644 --- a/pkg/translate/import_location.go +++ b/pkg/translate/import_location.go @@ -3,7 +3,6 @@ package translate import ( "bytes" "fmt" - "strings" "text/template" "github.com/paloaltonetworks/pan-os-codegen/pkg/properties" @@ -18,116 +17,6 @@ func LocationType(location *properties.Location, pointer bool) string { return fmt.Sprintf("%s%sLocation", prefix, location.Name.CamelCase) } -type importXpathVariableSpec struct { - Name *properties.NameVariant - Description string - Default *string -} - -type importLocationSpec struct { - Name *properties.NameVariant - XpathElements []string - XpathVariables []importXpathVariableSpec - XpathFinalElement string - ResponseXpath string -} - -type importSpec struct { - Variant *properties.NameVariant - Type *properties.NameVariant - Locations []importLocationSpec -} - -func createImportLocationSpecsForLocation(location properties.ImportLocation) importLocationSpec { - var variables []importXpathVariableSpec - variablesByName := make(map[string]importXpathVariableSpec, len(location.XpathVariables)) - - for _, elt := range location.OrderedXpathVariables() { - var defaultValue *string - if elt.Default != "" { - defaultValue = &elt.Default - } - variableSpec := importXpathVariableSpec{ - Name: elt.Name, - Description: elt.Description, - Default: defaultValue, - } - - variables = append(variables, variableSpec) - variablesByName[elt.Name.Underscore] = variableSpec - } - - var elements []string - for _, elt := range location.XpathElements { - if strings.HasPrefix(elt, "$") { - variableName := elt[1:] - asEntryXpath := fmt.Sprintf("util.AsEntryXpath(o.%s)", variablesByName[variableName].Name.LowerCamelCase) - elements = append(elements, asEntryXpath) - } else { - elements = append(elements, fmt.Sprintf("\"%s\"", elt)) - } - } - - xpathFinalElement := location.XpathElements[len(location.XpathElements)-1] - responseXpath := fmt.Sprintf("result>%s>member", xpathFinalElement) - - return importLocationSpec{ - Name: location.Name, - XpathElements: elements, - XpathVariables: variables, - XpathFinalElement: xpathFinalElement, - ResponseXpath: responseXpath, - } -} - -func createImportSpecsForNormalization(spec *properties.Normalization) []importSpec { - var specs []importSpec - - for _, imp := range spec.Imports { - var locations []importLocationSpec - for _, location := range imp.OrderedLocations() { - locations = append(locations, createImportLocationSpecsForLocation(*location)) - } - - specs = append(specs, importSpec{ - Variant: imp.Variant, - Type: imp.Type, - Locations: locations, - }) - } - - return specs -} - -// RenderEntryImportStructs generates import location structs for a normalization spec. -func RenderEntryImportStructs(spec *properties.Normalization) (string, error) { - type renderContext struct { - Specs []importSpec - } - - tmplContent, err := loadTemplate("partials/import_location_struct.tmpl") - if err != nil { - return "", err - } - - tmpl, err := template.New("render-entry-import-structs").Parse(tmplContent) - if err != nil { - return "", err - } - - data := renderContext{ - Specs: createImportSpecsForNormalization(spec), - } - - var buf bytes.Buffer - err = tmpl.Execute(&buf, data) - if err != nil { - return "", err - } - - return buf.String(), nil -} - type locationVariableSpec struct { Name *properties.NameVariant LocationFilter bool diff --git a/pkg/translate/terraform_provider/crud_operations.go b/pkg/translate/terraform_provider/crud_operations.go index 8b44754a..0b7d23d7 100644 --- a/pkg/translate/terraform_provider/crud_operations.go +++ b/pkg/translate/terraform_provider/crud_operations.go @@ -75,7 +75,7 @@ func ResourceCreateFunction(resourceTyp properties.ResourceType, names *NameProv data := map[string]interface{}{ "PluralType": paramSpec.TerraformProviderConfig.PluralType, "HasEncryptedResources": paramSpec.HasEncryptedResources(), - "HasImports": len(paramSpec.Imports) > 0, + "HasImports": len(paramSpec.Imports.Variants) > 0, "Exhaustive": exhaustive, "ListAttribute": listAttributeVariant, "EntryOrConfig": paramSpec.EntryOrConfig(), @@ -327,7 +327,7 @@ func ResourceDeleteFunction(resourceTyp properties.ResourceType, names *NameProv data := map[string]interface{}{ "PluralType": paramSpec.TerraformProviderConfig.PluralType, "HasEncryptedResources": paramSpec.HasEncryptedResources(), - "HasImports": len(paramSpec.Imports) > 0, + "HasImports": len(paramSpec.Imports.Variants) > 0, "EntryOrConfig": paramSpec.EntryOrConfig(), "ListAttribute": listAttributeVariant, "Exhaustive": exhaustive, diff --git a/pkg/translate/terraform_provider/entity_generators.go b/pkg/translate/terraform_provider/entity_generators.go index 76be8936..8f0221b0 100644 --- a/pkg/translate/terraform_provider/entity_generators.go +++ b/pkg/translate/terraform_provider/entity_generators.go @@ -27,7 +27,7 @@ func (g *GenerateTerraformProvider) GenerateTerraformResource(resourceTyp proper funcMap := template.FuncMap{ "GoSDKSkipped": func() bool { return spec.GoSdkSkip }, "IsEntry": func() bool { return spec.HasEntryName() && !spec.HasEntryUuid() }, - "HasImports": func() bool { return len(spec.Imports) > 0 }, + "HasImports": func() bool { return len(spec.Imports.Variants) > 0 }, "IsCustom": func() bool { return spec.TerraformProviderConfig.ResourceType == properties.TerraformResourceCustom }, "IsUuid": func() bool { return spec.HasEntryUuid() }, @@ -157,6 +157,8 @@ func (g *GenerateTerraformProvider) GenerateTerraformResource(resourceTyp proper terraformProvider.ImportManager.AddSdkImport("github.com/PaloAltoNetworks/pango/movement", "") terraformProvider.ImportManager.AddStandardImport("strings", "") case properties.ResourceEntry: + terraformProvider.ImportManager.AddStandardImport("strings", "") + terraformProvider.ImportManager.AddHashicorpImport("github.com/hashicorp/terraform-plugin-framework/resource/identityschema", "") case properties.ResourceEntryPlural: case properties.ResourceCustom, properties.ResourceConfig: } @@ -215,7 +217,11 @@ func (g *GenerateTerraformProvider) GenerateTerraformResource(resourceTyp proper } switch resourceTyp { - case properties.ResourceEntry, properties.ResourceConfig: + case properties.ResourceEntry: + terraformProvider.ImportManager.AddStandardImport("strings", "") + terraformProvider.ImportManager.AddHashicorpImport("github.com/hashicorp/terraform-plugin-framework/resource/identityschema", "") + terraformProvider.ImportManager.AddStandardImport("errors", "") + case properties.ResourceConfig: terraformProvider.ImportManager.AddStandardImport("errors", "") case properties.ResourceEntryPlural, properties.ResourceUuid: case properties.ResourceUuidPlural, properties.ResourceCustom: @@ -349,7 +355,7 @@ func (g *GenerateTerraformProvider) GenerateTerraformDataSource(resourceTyp prop panic("unreachable") }, - "HasImports": func() bool { return len(spec.Imports) > 0 }, + "HasImports": func() bool { return len(spec.Imports.Variants) > 0 }, "HasLocations": func() bool { return len(spec.Locations) > 0 }, "IsCustom": func() bool { return spec.TerraformProviderConfig.ResourceType == properties.TerraformResourceCustom }, "IsUuid": func() bool { return spec.HasEntryUuid() }, @@ -433,6 +439,8 @@ func (g *GenerateTerraformProvider) GenerateCommonCode(resourceTyp properties.Re names := NewNameProvider(spec, resourceTyp) funcMap := template.FuncMap{ "HasLocations": func() bool { return len(spec.Locations) > 0 }, + "RenderIdentityModel": func() (string, error) { return RenderIdentityModel(resourceTyp, names, spec) }, + "RenderIdentitySchema": func() (string, error) { return RenderIdentitySchema(resourceTyp, names, spec) }, "RenderLocationStructs": func() (string, error) { return RenderLocationStructs(resourceTyp, names, spec) }, "RenderLocationSchemaGetter": func() (string, error) { return RenderLocationSchemaGetter(names, spec, terraformProvider.ImportManager) @@ -441,6 +449,9 @@ func (g *GenerateTerraformProvider) GenerateCommonCode(resourceTyp properties.Re "RenderLocationAttributeTypes": func() (string, error) { return RenderLocationAttributeTypes(names, spec) }, + "RenderLocationAsIdentityGetter": func() (string, error) { + return RenderLocationAsIdentityGetter(resourceTyp, names, spec) + }, } return g.generateTerraformEntityTemplate(resourceTyp, properties.SchemaCommon, names, spec, terraformProvider, "common/common.tmpl", funcMap) } diff --git a/pkg/translate/terraform_provider/import_state.go b/pkg/translate/terraform_provider/import_state.go index a4a733b5..a90a9213 100644 --- a/pkg/translate/terraform_provider/import_state.go +++ b/pkg/translate/terraform_provider/import_state.go @@ -25,100 +25,34 @@ func RenderImportStateMarshallers(resourceTyp properties.ResourceType, names *Na // RenderImportLocationAssignment generates code to assign import location data. func RenderImportLocationAssignment(names *NameProvider, spec *properties.Normalization, source string, dest string) (string, error) { - if len(spec.Imports) == 0 { + if len(spec.Imports.Variants) == 0 { return "", nil } - type importVariantSpec struct { - PangoStructNames *map[string]string - } - - type importLocationSpec struct { - TerraformStructName string - Name *properties.NameVariant - Fields []string + type context struct { + Variants []*properties.NameVariant + StructName string + Source string + Destination string } - type importSpec struct { - TerraformStructName string - Name *properties.NameVariant - Locations []importLocationSpec - } + var variants []*properties.NameVariant - var importSpecs []importSpec - variantsByName := make(map[string]importVariantSpec) - for _, elt := range spec.Imports { - existing, found := variantsByName[elt.Type.CamelCase] - if !found { - pangoStructNames := make(map[string]string) - existing = importVariantSpec{ - PangoStructNames: &pangoStructNames, - } + for _, elt := range spec.Imports.Variants { + if elt == "*" { + continue } - - var locations []importLocationSpec - for _, loc := range elt.Locations { - if !loc.Required { - continue - } - - var fields []string - for _, elt := range loc.XpathVariables { - fields = append(fields, elt.Name.CamelCase) - } - - tfStructName := fmt.Sprintf("%s%sLocation", names.StructName, elt.Type.CamelCase) - pangoStructName := fmt.Sprintf("%s%s%sImportLocation", elt.Variant.CamelCase, elt.Type.CamelCase, loc.Name.CamelCase) - (*existing.PangoStructNames)[loc.Name.CamelCase] = pangoStructName - locations = append(locations, importLocationSpec{ - TerraformStructName: tfStructName, - Name: loc.Name, - Fields: fields, - }) - } - variantsByName[elt.Type.CamelCase] = existing - - importSpecs = append(importSpecs, importSpec{ - Name: elt.Type, - Locations: locations, - }) - } - - type context struct { - TerraformStructName string - PackageName string - Source string - Dest string - Variants map[string]importVariantSpec - Specs []importSpec + variants = append(variants, properties.NewNameVariant(elt)) } data := context{ - TerraformStructName: fmt.Sprintf("%sLocation", names.StructName), - PackageName: names.PackageName, - Source: source, - Dest: dest, - Variants: variantsByName, - Specs: importSpecs, - } - - funcMap := template.FuncMap{ - "GetPangoStructForLocation": func(variants map[string]importVariantSpec, typ *properties.NameVariant, location *properties.NameVariant) (string, error) { - variantSpec, found := variants[typ.CamelCase] - if !found { - return "", fmt.Errorf("failed to find variant for type '%s'", typ.CamelCase) - } - - structName, found := (*variantSpec.PangoStructNames)[location.CamelCase] - if !found { - return "", fmt.Errorf("failed to find variant for type '%s', location '%s'", typ.CamelCase, location.CamelCase) - } - - return structName, nil - }, + Variants: variants, + StructName: names.StructName, + Source: source, + Destination: dest, } - return processTemplate("location/assignment.tmpl", "render-locations-pango-to-state", data, funcMap) + return processTemplate("import/import_location_assignment.tmpl", "render-locations-pango-to-state", data, commonFuncMap) } // importStateStructFieldSpec describes a field in an import state struct. diff --git a/pkg/translate/terraform_provider/import_state_test.go b/pkg/translate/terraform_provider/import_state_test.go new file mode 100644 index 00000000..743c09b7 --- /dev/null +++ b/pkg/translate/terraform_provider/import_state_test.go @@ -0,0 +1,102 @@ +package terraform_provider_test + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/paloaltonetworks/pan-os-codegen/pkg/properties" + "github.com/paloaltonetworks/pan-os-codegen/pkg/schema/imports" + "github.com/paloaltonetworks/pan-os-codegen/pkg/translate/terraform_provider" +) + +var _ = Describe("RenderImportLocationAssignment", func() { + var ( + names *terraform_provider.NameProvider + spec *properties.Normalization + ) + + BeforeEach(func() { + spec = &properties.Normalization{ + Name: "TestResource", + } + names = &terraform_provider.NameProvider{ + StructName: "TestResource", + } + }) + + Context("with empty variants", func() { + It("should return empty string", func() { + spec.Imports = imports.Import{ + Variants: []string{}, + } + + result, err := terraform_provider.RenderImportLocationAssignment(names, spec, "state", "importVsys") + + Expect(err).ShouldNot(HaveOccurred()) + Expect(result).Should(BeEmpty()) + }) + }) + + Context("with wildcard-only variants", func() { + It("should generate unconditional locationRequiresImport assignment", func() { + spec.Imports = imports.Import{ + Variants: []string{"*"}, + Target: "interface", + } + + result, err := terraform_provider.RenderImportLocationAssignment(names, spec, "state", "importVsys") + + Expect(err).ShouldNot(HaveOccurred()) + Expect(result).Should(ContainSubstring("locationRequiresImport := true")) + Expect(result).ShouldNot(ContainSubstring("var locationRequiresImport bool")) + }) + }) + + Context("with specific variants", func() { + It("should generate conditional checks for each variant", func() { + spec.Imports = imports.Import{ + Variants: []string{"layer2", "layer3"}, + Target: "interface", + } + + result, err := terraform_provider.RenderImportLocationAssignment(names, spec, "state", "importVsys") + + Expect(err).ShouldNot(HaveOccurred()) + Expect(result).Should(ContainSubstring("var locationRequiresImport bool")) + Expect(result).Should(ContainSubstring("state.Layer2")) + Expect(result).Should(ContainSubstring("state.Layer3")) + Expect(result).Should(ContainSubstring("locationRequiresImport = true")) + }) + }) + + Context("with mixed variants including wildcard", func() { + It("should filter out wildcard and generate checks for remaining variants", func() { + spec.Imports = imports.Import{ + Variants: []string{"layer2", "*"}, + Target: "interface", + } + + result, err := terraform_provider.RenderImportLocationAssignment(names, spec, "state", "importVsys") + + Expect(err).ShouldNot(HaveOccurred()) + Expect(result).Should(ContainSubstring("var locationRequiresImport bool")) + Expect(result).Should(ContainSubstring("state.Layer2")) + Expect(result).ShouldNot(ContainSubstring("state.*")) + }) + }) + + Context("source and destination substitution", func() { + It("should use the provided source and destination values", func() { + spec.Imports = imports.Import{ + Variants: []string{"*"}, + Target: "interface", + } + + result, err := terraform_provider.RenderImportLocationAssignment(names, spec, "state", "importVsys") + + Expect(err).ShouldNot(HaveOccurred()) + Expect(result).Should(ContainSubstring("state.Location")) + Expect(result).Should(ContainSubstring("importVsys = terraformInnerLocation.Vsys.ValueString()")) + }) + }) +}) diff --git a/pkg/translate/terraform_provider/location.go b/pkg/translate/terraform_provider/location.go index 28aacb3e..742ba365 100644 --- a/pkg/translate/terraform_provider/location.go +++ b/pkg/translate/terraform_provider/location.go @@ -12,12 +12,15 @@ type locationStructFieldCtx struct { Name *properties.NameVariant TerraformType string Type string + AsIdentity bool Tags []string } // locationStructCtx describes a location struct. type locationStructCtx struct { StructName string + XpathName *properties.NameVariant + TopLevel bool Fields []locationStructFieldCtx } @@ -32,6 +35,7 @@ func getLocationStructsContext(names *NameProvider, spec *properties.Normalizati // Create the top location structure that references other locations topLocation := locationStructCtx{ StructName: fmt.Sprintf("%sLocation", names.StructName), + TopLevel: true, } for _, data := range spec.OrderedLocations() { @@ -43,25 +47,18 @@ func getLocationStructsContext(names *NameProvider, spec *properties.Normalizati Name: data.Name, TerraformType: structName, Type: structType, + AsIdentity: true, Tags: []string{tfTag}, }) var fields []locationStructFieldCtx - for _, i := range spec.Imports { - if i.Type.CamelCase != data.Name.CamelCase { - continue - } - - for _, elt := range i.OrderedLocations() { - if elt.Required { - fields = append(fields, locationStructFieldCtx{ - Name: elt.Name, - Type: "types.String", - Tags: []string{fmt.Sprintf("`tfsdk:\"%s\"`", elt.Name.Underscore)}, - }) - } - } + if data.Name.Original == "template" && len(spec.Imports.Variants) > 0 { + fields = append(fields, locationStructFieldCtx{ + Name: properties.NewNameVariant("vsys"), + Type: "types.String", + Tags: []string{"`tfsdk:\"vsys\"`"}, + }) } for _, param := range data.OrderedVars() { @@ -72,13 +69,15 @@ func getLocationStructsContext(names *NameProvider, spec *properties.Normalizati paramTag = "`tfsdk:\"name\"`" } fields = append(fields, locationStructFieldCtx{ - Name: name, - Type: "types.String", - Tags: []string{paramTag}, + Name: name, + Type: "types.String", + AsIdentity: true, + Tags: []string{paramTag}, }) } location := locationStructCtx{ + XpathName: data.Name, StructName: structName, Fields: fields, } @@ -121,33 +120,23 @@ func RenderLocationSchemaGetter(names *NameProvider, spec *properties.Normalizat for _, data := range spec.OrderedLocations() { var variableAttrs []attributeCtx - for _, i := range spec.Imports { - if i.Type.CamelCase != data.Name.CamelCase { - continue - } - - for _, elt := range i.OrderedLocations() { - if elt.Required { - var defaultValue *defaultCtx - for varName, variable := range elt.XpathVariables { - if varName == elt.Name.Original && variable.Default != "" { - defaultValue = &defaultCtx{ - Type: "stringdefault.StaticString", - Value: fmt.Sprintf(`"%s"`, variable.Default), - } - } - } - variableAttrs = append(variableAttrs, attributeCtx{ - Name: elt.Name, - SchemaType: "rsschema.StringAttribute", - Required: defaultValue == nil, - Optional: defaultValue != nil, - Computed: defaultValue != nil, - ModifierType: "String", - Default: defaultValue, - }) + if data.Name.Original == "template" && len(spec.Imports.Variants) > 0 { + var defaultValue *defaultCtx + if spec.Imports.DefaultValue != nil { + defaultValue = &defaultCtx{ + Type: "stringdefault.StaticString", + Value: fmt.Sprintf(`"%s"`, *spec.Imports.DefaultValue), } } + variableAttrs = append(variableAttrs, attributeCtx{ + Name: properties.NewNameVariant("vsys"), + SchemaType: "rsschema.StringAttribute", + Required: false, + Optional: true, + Computed: true, + ModifierType: "String", + Default: defaultValue, + }) } for _, variable := range data.OrderedVars() { @@ -279,20 +268,12 @@ func createLocationMarshallerSpecs(names *NameProvider, spec *properties.Normali } // Add import location (e.g. vsys) name to location - for _, i := range spec.Imports { - if i.Type.CamelCase != loc.Name.CamelCase { - continue - } - - for _, elt := range i.OrderedLocations() { - if elt.Required { - fields = append(fields, marshallerFieldSpec{ - Name: elt.Name, - Type: "string", - Tags: fmt.Sprintf("`tfsdk:\"%s\"`", elt.Name.Underscore), - }) - } - } + if loc.Name.Original == "template" && len(spec.Imports.Variants) > 0 { + fields = append(fields, marshallerFieldSpec{ + Name: properties.NewNameVariant("vsys"), + Type: "string", + Tags: "`tfsdk:\"vsys\"`", + }) } specs = append(specs, marshallerSpec{ @@ -416,3 +397,138 @@ func RenderLocationAttributeTypes(names *NameProvider, spec *properties.Normaliz } return processTemplate("location/attribute_types.tmpl", "render-location-structs", data, commonFuncMap) } + +// RenderIdentityModel generates identity model struct. +func RenderIdentityModel(resourceTyp properties.ResourceType, names *NameProvider, spec *properties.Normalization) (string, error) { + switch resourceTyp { + case properties.ResourceUuid, properties.ResourceUuidPlural, properties.ResourceEntryPlural: + return "", nil + case properties.ResourceConfig, properties.ResourceCustom: + return "", nil + case properties.ResourceEntry: + } + + if spec.TerraformProviderConfig.Ephemeral { + return "", nil + } + + if spec.TerraformProviderConfig.SkipResource { + return "", nil + } + + type fieldCtx struct { + Name string + Type string + Tag string + } + + type context struct { + StructName string + Fields []fieldCtx + } + + var fields []fieldCtx + if spec.HasEntryName() { + fields = []fieldCtx{ + {Name: "Name", Type: "types.String", Tag: "name"}, + {Name: "Location", Type: "types.String", Tag: "location"}, + } + } else { + fields = []fieldCtx{ + {Name: "Config", Type: "types.String", Tag: "config"}, + {Name: "Location", Type: "types.String", Tag: "location"}, + } + } + + data := context{ + StructName: names.IdentityModelStructName(), + Fields: fields, + } + + return processTemplate("common/identity_model.tmpl", "render-identity-model", data, commonFuncMap) +} + +// RenderIdentitySchema generates identity schema method. +func RenderIdentitySchema(resourceTyp properties.ResourceType, names *NameProvider, spec *properties.Normalization) (string, error) { + switch resourceTyp { + case properties.ResourceUuid, properties.ResourceUuidPlural, properties.ResourceEntryPlural: + return "", nil + case properties.ResourceConfig, properties.ResourceCustom: + return "", nil + case properties.ResourceEntry: + } + + if spec.TerraformProviderConfig.Ephemeral { + return "", nil + } + + if spec.TerraformProviderConfig.SkipResource { + return "", nil + } + + type fieldCtx struct { + Name string + Type string + RequiredForImport bool + OptionalForImport bool + } + + type context struct { + StructName string + Fields []fieldCtx + } + + var fields []fieldCtx + if spec.HasEntryName() { + fields = []fieldCtx{ + {Name: "name", Type: "identityschema.StringAttribute", RequiredForImport: true}, + {Name: "location", Type: "identityschema.StringAttribute", RequiredForImport: true}, + } + } else { + fields = []fieldCtx{ + {Name: "config", Type: "identityschema.StringAttribute", RequiredForImport: true}, + {Name: "location", Type: "identityschema.StringAttribute", RequiredForImport: true}, + } + } + + data := context{ + StructName: fmt.Sprintf("%sResource", names.StructName), + Fields: fields, + } + + return processTemplate("common/identity_schema.tmpl", "render-identity-schema", data, commonFuncMap) + +} + +// RenderLocationAsIdentityGetter generates location-to-identity conversion code. +func RenderLocationAsIdentityGetter(resourceTyp properties.ResourceType, names *NameProvider, spec *properties.Normalization) (string, error) { + switch resourceTyp { + case properties.ResourceUuid, properties.ResourceUuidPlural, properties.ResourceEntryPlural: + return "", nil + case properties.ResourceConfig, properties.ResourceCustom: + return "", nil + case properties.ResourceEntry: + } + + if spec.TerraformProviderConfig.Ephemeral { + return "", nil + } + + if spec.TerraformProviderConfig.SkipResource { + return "", nil + } + + type context struct { + StructName string + Specs []locationStructCtx + } + + locations := getLocationStructsContext(names, spec) + + data := context{ + StructName: names.StructName, + Specs: locations, + } + + return processTemplate("location/identity.tmpl", "render-location-as-identity", data, commonFuncMap) +} diff --git a/pkg/translate/terraform_provider/terraform_provider_suite_test.go b/pkg/translate/terraform_provider/terraform_provider_suite_test.go new file mode 100644 index 00000000..7c6907af --- /dev/null +++ b/pkg/translate/terraform_provider/terraform_provider_suite_test.go @@ -0,0 +1,15 @@ +package terraform_provider_test + +import ( + "log/slog" + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestTerraformProvider(t *testing.T) { + slog.SetDefault(slog.New(slog.NewTextHandler(GinkgoWriter, &slog.HandlerOptions{Level: slog.LevelDebug}))) + RegisterFailHandler(Fail) + RunSpecs(t, "Terraform Provider Suite") +} diff --git a/specs/actions/commit.yaml b/specs/actions/commit.yaml index 71022ba8..15001e0c 100644 --- a/specs/actions/commit.yaml +++ b/specs/actions/commit.yaml @@ -15,7 +15,6 @@ panos_xpath: path: ["api-key"] locations: [] entries: [] -imports: [] version: 11.0.2 spec: params: diff --git a/specs/actions/push_to_devices.yaml b/specs/actions/push_to_devices.yaml index dd09c8f2..b91ba8dc 100644 --- a/specs/actions/push_to_devices.yaml +++ b/specs/actions/push_to_devices.yaml @@ -15,7 +15,6 @@ panos_xpath: path: ["api-key"] locations: [] entries: [] -imports: [] version: 11.0.2 spec: params: diff --git a/specs/device/adminrole.yaml b/specs/device/adminrole.yaml index 7df47acc..69eead1b 100644 --- a/specs/device/adminrole.yaml +++ b/specs/device/adminrole.yaml @@ -51,7 +51,6 @@ entries: - name: name description: '' validators: [] -imports: [] spec: params: - name: description diff --git a/specs/device/api_key.yaml b/specs/device/api_key.yaml index 32133599..7a9a4bd7 100644 --- a/specs/device/api_key.yaml +++ b/specs/device/api_key.yaml @@ -14,7 +14,6 @@ panos_xpath: path: ["api-key"] locations: [] entries: [] -imports: [] version: 11.0.2 spec: params: diff --git a/specs/device/authentication-profile.yaml b/specs/device/authentication-profile.yaml index 814eae9e..4c6537a8 100644 --- a/specs/device/authentication-profile.yaml +++ b/specs/device/authentication-profile.yaml @@ -84,7 +84,7 @@ locations: required: true validators: [] type: entry - description: Located in a specific template + description: A shared resource located within a specific template devices: - panorama validators: [] @@ -220,7 +220,6 @@ entries: - name: name description: '' validators: [] -imports: [] spec: params: - name: allow-list diff --git a/specs/device/certificate.yaml b/specs/device/certificate.yaml index 1cf31da0..aa714a33 100644 --- a/specs/device/certificate.yaml +++ b/specs/device/certificate.yaml @@ -223,7 +223,6 @@ entries: - name: name description: '' validators: [] -imports: [] spec: params: - name: algorithm diff --git a/specs/device/dns.yaml b/specs/device/dns.yaml index b5811fa4..077f5a3b 100644 --- a/specs/device/dns.yaml +++ b/specs/device/dns.yaml @@ -112,7 +112,6 @@ locations: required: false read_only: false entries: [] -imports: [] spec: params: - name: dns-setting diff --git a/specs/device/dynamic-updates.yaml b/specs/device/dynamic-updates.yaml index 1f7d1a0e..b8fff91c 100644 --- a/specs/device/dynamic-updates.yaml +++ b/specs/device/dynamic-updates.yaml @@ -115,7 +115,6 @@ locations: required: false read_only: false entries: [] -imports: [] spec: params: - name: update-schedule diff --git a/specs/device/general.yaml b/specs/device/general.yaml index 99df5f18..41b4c379 100644 --- a/specs/device/general.yaml +++ b/specs/device/general.yaml @@ -112,7 +112,6 @@ locations: required: false read_only: false entries: [] -imports: [] spec: params: - name: domain diff --git a/specs/device/globalprotect-gateway.yaml b/specs/device/globalprotect-gateway.yaml index 79da3ea2..de5d91de 100644 --- a/specs/device/globalprotect-gateway.yaml +++ b/specs/device/globalprotect-gateway.yaml @@ -226,7 +226,6 @@ entries: - name: name description: '' validators: [] -imports: [] spec: params: - name: block-quarantined-devices diff --git a/specs/device/log-settings/config.yaml b/specs/device/log-settings/config.yaml index 7d48f975..8b1e63f5 100644 --- a/specs/device/log-settings/config.yaml +++ b/specs/device/log-settings/config.yaml @@ -191,7 +191,6 @@ entries: - name: name description: '' validators: [] -imports: [] spec: params: - name: description diff --git a/specs/device/log-settings/correlation.yaml b/specs/device/log-settings/correlation.yaml index 68098bf7..3a370354 100644 --- a/specs/device/log-settings/correlation.yaml +++ b/specs/device/log-settings/correlation.yaml @@ -191,7 +191,6 @@ entries: - name: name description: '' validators: [] -imports: [] spec: params: - name: actions diff --git a/specs/device/log-settings/globalprotect.yaml b/specs/device/log-settings/globalprotect.yaml index 26ca6781..b4d12474 100644 --- a/specs/device/log-settings/globalprotect.yaml +++ b/specs/device/log-settings/globalprotect.yaml @@ -191,7 +191,6 @@ entries: - name: name description: '' validators: [] -imports: [] spec: params: - name: actions diff --git a/specs/device/log-settings/hipmatch.yaml b/specs/device/log-settings/hipmatch.yaml index 6382a1e9..a9e5e4f4 100644 --- a/specs/device/log-settings/hipmatch.yaml +++ b/specs/device/log-settings/hipmatch.yaml @@ -191,7 +191,6 @@ entries: - name: name description: '' validators: [] -imports: [] spec: params: - name: actions diff --git a/specs/device/log-settings/iptag.yaml b/specs/device/log-settings/iptag.yaml index 2a7f74ae..c9d15d03 100644 --- a/specs/device/log-settings/iptag.yaml +++ b/specs/device/log-settings/iptag.yaml @@ -191,7 +191,6 @@ entries: - name: name description: '' validators: [] -imports: [] spec: params: - name: actions diff --git a/specs/device/log-settings/system.yaml b/specs/device/log-settings/system.yaml index a54555f0..8bcc5851 100644 --- a/specs/device/log-settings/system.yaml +++ b/specs/device/log-settings/system.yaml @@ -191,7 +191,6 @@ entries: - name: name description: '' validators: [] -imports: [] spec: params: - name: actions diff --git a/specs/device/log-settings/userid.yaml b/specs/device/log-settings/userid.yaml index 2d7d2d0b..81c001d7 100644 --- a/specs/device/log-settings/userid.yaml +++ b/specs/device/log-settings/userid.yaml @@ -191,7 +191,6 @@ entries: - name: name description: '' validators: [] -imports: [] spec: params: - name: actions diff --git a/specs/device/ntp.yaml b/specs/device/ntp.yaml index 282ea944..ff3aa12b 100644 --- a/specs/device/ntp.yaml +++ b/specs/device/ntp.yaml @@ -112,7 +112,6 @@ locations: required: false read_only: false entries: [] -imports: [] spec: params: - name: ntp-servers diff --git a/specs/device/profiles/data-filtering.yaml b/specs/device/profiles/data-filtering.yaml index bc325c67..a3f8fc49 100644 --- a/specs/device/profiles/data-filtering.yaml +++ b/specs/device/profiles/data-filtering.yaml @@ -71,7 +71,6 @@ entries: - name: name description: '' validators: [] -imports: [] spec: params: - name: data-capture diff --git a/specs/device/profiles/dos-protection.yaml b/specs/device/profiles/dos-protection.yaml index 83d4619f..ea9226a8 100644 --- a/specs/device/profiles/dos-protection.yaml +++ b/specs/device/profiles/dos-protection.yaml @@ -71,7 +71,6 @@ entries: - name: name description: '' validators: [] -imports: [] spec: params: - name: description diff --git a/specs/device/profiles/ldap.yaml b/specs/device/profiles/ldap.yaml index 4e461af0..9bff524e 100644 --- a/specs/device/profiles/ldap.yaml +++ b/specs/device/profiles/ldap.yaml @@ -235,7 +235,6 @@ entries: - name: name description: '' validators: [] -imports: [] spec: params: - name: base diff --git a/specs/device/profiles/syslog.yaml b/specs/device/profiles/syslog.yaml index a5158935..431800a1 100644 --- a/specs/device/profiles/syslog.yaml +++ b/specs/device/profiles/syslog.yaml @@ -222,7 +222,6 @@ entries: - name: name description: '' validators: [] -imports: [] spec: params: - name: format diff --git a/specs/device/proxy.yaml b/specs/device/proxy.yaml index a56262ca..cbcaa709 100644 --- a/specs/device/proxy.yaml +++ b/specs/device/proxy.yaml @@ -112,7 +112,6 @@ locations: required: false read_only: false entries: [] -imports: [] spec: params: - name: lcaas-use-proxy diff --git a/specs/device/ssl-decrypt.yaml b/specs/device/ssl-decrypt.yaml index 2f77793c..1fc9a999 100644 --- a/specs/device/ssl-decrypt.yaml +++ b/specs/device/ssl-decrypt.yaml @@ -16,7 +16,8 @@ go_sdk_config: - device - ssldecrypt panos_xpath: - path: ["ssl-decrypt"] + path: + - ssl-decrypt vars: [] locations: - name: panorama @@ -200,7 +201,6 @@ locations: required: false read_only: false entries: [] -imports: [] spec: params: - name: disabled-ssl-exclude-cert-from-predefined diff --git a/specs/network/dhcp.yaml b/specs/network/dhcp.yaml index 7ee4e717..6a70ae36 100644 --- a/specs/network/dhcp.yaml +++ b/specs/network/dhcp.yaml @@ -115,7 +115,6 @@ entries: - name: name description: '' validators: [] -imports: [] spec: params: [] variants: diff --git a/specs/network/dns-proxy.yaml b/specs/network/dns-proxy.yaml index 4b306632..cb612671 100644 --- a/specs/network/dns-proxy.yaml +++ b/specs/network/dns-proxy.yaml @@ -95,7 +95,6 @@ entries: - name: name description: '' validators: [] -imports: [] spec: params: - name: cache diff --git a/specs/network/globalprotect-portal.yaml b/specs/network/globalprotect-portal.yaml index d39e4377..8e24a87a 100644 --- a/specs/network/globalprotect-portal.yaml +++ b/specs/network/globalprotect-portal.yaml @@ -226,7 +226,6 @@ entries: - name: name description: '' validators: [] -imports: [] spec: params: - name: client-config diff --git a/specs/network/ike-gateway.yaml b/specs/network/ike-gateway.yaml index ebff663e..f6ea4f35 100644 --- a/specs/network/ike-gateway.yaml +++ b/specs/network/ike-gateway.yaml @@ -115,7 +115,6 @@ entries: - name: name description: '' validators: [] -imports: [] spec: params: - name: authentication diff --git a/specs/network/interface/aggregate.yaml b/specs/network/interface/aggregate.yaml index bde440a6..fae99e02 100644 --- a/specs/network/interface/aggregate.yaml +++ b/specs/network/interface/aggregate.yaml @@ -129,7 +129,11 @@ entries: - name: name description: '' validators: [] -imports: [] +imports: + target: interface + variants: + - layer2 + - layer3 spec: params: - name: comment diff --git a/specs/network/interface/ethernet.yaml b/specs/network/interface/ethernet.yaml index b47b79e7..8c0811a4 100644 --- a/specs/network/interface/ethernet.yaml +++ b/specs/network/interface/ethernet.yaml @@ -130,106 +130,12 @@ entries: description: '' validators: [] imports: -- variant: layer3 - type: template - locations: - - name: virtual-router - xpath: - path: - - network - - virtual-router - - $router - - interface - vars: - - name: vsys - description: The vsys. - required: false - default: vsys1 - validators: [] - type: entry - - name: router - description: Virtual Router - required: false - validators: [] - type: entry - description: '' - validators: [] - required: false - read_only: false - - name: logical-router - xpath: - path: - - network - - logical-router - - $router - - vrf - - $vrf - - interface - vars: - - name: vsys - description: The vsys. - required: false - default: vsys1 - validators: [] - type: entry - - name: router - description: Logical Router Name - required: false - validators: [] - type: entry - - name: vrf - description: Logical Router VRF - required: false - validators: [] - type: entry - description: '' - validators: [] - required: false - read_only: false - - name: vsys - xpath: - path: - - vsys - - $vsys - - import - - network - - interface - vars: - - name: vsys - description: The vsys. - required: false - default: vsys1 - validators: [] - type: entry - description: '' - validators: [] - required: true - read_only: false - - name: zone - xpath: - path: - - vsys - - $vsys - - zone - - $zone - - network - - layer3 - vars: - - name: vsys - description: The vsys. - required: false - default: vsys1 - validators: [] - type: entry - - name: zone - description: Security Zone Identifier - required: false - validators: [] - type: entry - description: '' - validators: [] - required: false - read_only: false + target: interface + variants: + - layer2 + - layer3 + - tap + default_value: vsys1 spec: params: - name: comment diff --git a/specs/network/interface/loopback.yaml b/specs/network/interface/loopback.yaml index 8873d770..6fa18fb1 100644 --- a/specs/network/interface/loopback.yaml +++ b/specs/network/interface/loopback.yaml @@ -117,7 +117,6 @@ entries: - name: name description: '' validators: [] -imports: [] spec: params: - name: adjust-tcp-mss diff --git a/specs/network/interface/tunnel.yaml b/specs/network/interface/tunnel.yaml index 25c6025b..b4ee9ba0 100644 --- a/specs/network/interface/tunnel.yaml +++ b/specs/network/interface/tunnel.yaml @@ -131,28 +131,10 @@ entries: description: '' validators: [] imports: -- variant: layer3 - type: template - locations: - - name: vsys - xpath: - path: - - vsys - - $vsys - - import - - network - - interface - vars: - - name: vsys - description: The vsys. - required: false - default: vsys1 - validators: [] - type: entry - description: '' - validators: [] - required: true - read_only: false + target: interface + variants: + - '*' + default_value: vsys1 spec: params: - name: bonjour diff --git a/specs/network/interface/vlan.yaml b/specs/network/interface/vlan.yaml index 01593489..15a2cf0a 100644 --- a/specs/network/interface/vlan.yaml +++ b/specs/network/interface/vlan.yaml @@ -130,7 +130,6 @@ entries: - name: name description: '' validators: [] -imports: [] spec: params: - name: adjust-tcp-mss diff --git a/specs/network/logical-router.yaml b/specs/network/logical-router.yaml index bdaf0e6b..1e03b4fb 100644 --- a/specs/network/logical-router.yaml +++ b/specs/network/logical-router.yaml @@ -146,7 +146,6 @@ entries: - name: name description: '' validators: [] -imports: [] spec: params: - name: vrf diff --git a/specs/network/profiles/anti-spyware-profile.yaml b/specs/network/profiles/anti-spyware-profile.yaml index d543b475..cac80dd9 100644 --- a/specs/network/profiles/anti-spyware-profile.yaml +++ b/specs/network/profiles/anti-spyware-profile.yaml @@ -104,7 +104,6 @@ entries: - name: name description: '' validators: [] -imports: [] spec: params: - name: botnet-domains diff --git a/specs/network/profiles/interface-management-profile.yaml b/specs/network/profiles/interface-management-profile.yaml index 6a42408b..e05f3358 100644 --- a/specs/network/profiles/interface-management-profile.yaml +++ b/specs/network/profiles/interface-management-profile.yaml @@ -115,7 +115,6 @@ entries: - name: name description: '' validators: [] -imports: [] spec: params: - name: http diff --git a/specs/network/profiles/monitoring.yaml b/specs/network/profiles/monitoring.yaml index 2dec227c..d36c00aa 100644 --- a/specs/network/profiles/monitoring.yaml +++ b/specs/network/profiles/monitoring.yaml @@ -116,7 +116,6 @@ entries: - name: name description: '' validators: [] -imports: [] spec: params: - name: action diff --git a/specs/network/routing-profiles/bgp-address-family.yaml b/specs/network/routing-profiles/bgp-address-family.yaml index 0fa1decc..a8115e89 100644 --- a/specs/network/routing-profiles/bgp-address-family.yaml +++ b/specs/network/routing-profiles/bgp-address-family.yaml @@ -118,7 +118,6 @@ entries: - name: name description: '' validators: [] -imports: [] spec: params: [] variants: diff --git a/specs/network/routing-profiles/bgp-auth-profile.yaml b/specs/network/routing-profiles/bgp-auth-profile.yaml index 5f6585a1..17fd0017 100644 --- a/specs/network/routing-profiles/bgp-auth-profile.yaml +++ b/specs/network/routing-profiles/bgp-auth-profile.yaml @@ -118,7 +118,6 @@ entries: - name: name description: '' validators: [] -imports: [] spec: params: - name: secret diff --git a/specs/network/routing-profiles/bgp-dampening-profile.yaml b/specs/network/routing-profiles/bgp-dampening-profile.yaml index f6bf10a9..6e3f507a 100644 --- a/specs/network/routing-profiles/bgp-dampening-profile.yaml +++ b/specs/network/routing-profiles/bgp-dampening-profile.yaml @@ -118,7 +118,6 @@ entries: - name: name description: '' validators: [] -imports: [] spec: params: - name: description diff --git a/specs/network/routing-profiles/bgp-filtering-profile.yaml b/specs/network/routing-profiles/bgp-filtering-profile.yaml index c30ecd71..0753bd23 100644 --- a/specs/network/routing-profiles/bgp-filtering-profile.yaml +++ b/specs/network/routing-profiles/bgp-filtering-profile.yaml @@ -118,7 +118,6 @@ entries: - name: name description: '' validators: [] -imports: [] spec: params: - name: description diff --git a/specs/network/routing-profiles/bgp-redistribution-profile.yaml b/specs/network/routing-profiles/bgp-redistribution-profile.yaml index 0284dd71..6e7b396b 100644 --- a/specs/network/routing-profiles/bgp-redistribution-profile.yaml +++ b/specs/network/routing-profiles/bgp-redistribution-profile.yaml @@ -118,7 +118,6 @@ entries: - name: name description: '' validators: [] -imports: [] spec: params: [] variants: diff --git a/specs/network/routing-profiles/bgp-timer-profile.yaml b/specs/network/routing-profiles/bgp-timer-profile.yaml index 01925f2b..23f75cb7 100644 --- a/specs/network/routing-profiles/bgp-timer-profile.yaml +++ b/specs/network/routing-profiles/bgp-timer-profile.yaml @@ -118,7 +118,6 @@ entries: - name: name description: '' validators: [] -imports: [] spec: params: - name: hold-time diff --git a/specs/network/routing-profiles/filters-access-list-profile.yaml b/specs/network/routing-profiles/filters-access-list-profile.yaml index 94c0a8ed..5ff3c015 100644 --- a/specs/network/routing-profiles/filters-access-list-profile.yaml +++ b/specs/network/routing-profiles/filters-access-list-profile.yaml @@ -118,7 +118,6 @@ entries: - name: name description: '' validators: [] -imports: [] spec: params: - name: description diff --git a/specs/network/routing-profiles/filters-as-path-access-list-profile.yaml b/specs/network/routing-profiles/filters-as-path-access-list-profile.yaml index f268d6a0..e96b23de 100644 --- a/specs/network/routing-profiles/filters-as-path-access-list-profile.yaml +++ b/specs/network/routing-profiles/filters-as-path-access-list-profile.yaml @@ -118,7 +118,6 @@ entries: - name: name description: '' validators: [] -imports: [] spec: params: - name: aspath-entry diff --git a/specs/network/routing-profiles/filters-bgp-route-map-profile.yaml b/specs/network/routing-profiles/filters-bgp-route-map-profile.yaml index 8f4584f7..e2293a17 100644 --- a/specs/network/routing-profiles/filters-bgp-route-map-profile.yaml +++ b/specs/network/routing-profiles/filters-bgp-route-map-profile.yaml @@ -120,7 +120,6 @@ entries: - name: name description: '' validators: [] -imports: [] spec: params: - name: description diff --git a/specs/network/routing-profiles/filters-community-list-profile.yaml b/specs/network/routing-profiles/filters-community-list-profile.yaml index 81be057a..86ac33f3 100644 --- a/specs/network/routing-profiles/filters-community-list-profile.yaml +++ b/specs/network/routing-profiles/filters-community-list-profile.yaml @@ -118,7 +118,6 @@ entries: - name: name description: '' validators: [] -imports: [] spec: params: - name: description diff --git a/specs/network/routing-profiles/filters-prefix-list-profile.yaml b/specs/network/routing-profiles/filters-prefix-list-profile.yaml index 67055ed5..570857a9 100644 --- a/specs/network/routing-profiles/filters-prefix-list-profile.yaml +++ b/specs/network/routing-profiles/filters-prefix-list-profile.yaml @@ -118,7 +118,6 @@ entries: - name: name description: '' validators: [] -imports: [] spec: params: - name: description diff --git a/specs/network/routing-profiles/filters-route-maps-redistribution-profile.yaml b/specs/network/routing-profiles/filters-route-maps-redistribution-profile.yaml index e1aaa336..f1525db1 100644 --- a/specs/network/routing-profiles/filters-route-maps-redistribution-profile.yaml +++ b/specs/network/routing-profiles/filters-route-maps-redistribution-profile.yaml @@ -120,7 +120,6 @@ entries: - name: name description: '' validators: [] -imports: [] spec: params: - name: description diff --git a/specs/network/subinterface/aggregate-ethernet/layer3.yaml b/specs/network/subinterface/aggregate-ethernet/layer3.yaml index 57e9ed06..257962e0 100644 --- a/specs/network/subinterface/aggregate-ethernet/layer3.yaml +++ b/specs/network/subinterface/aggregate-ethernet/layer3.yaml @@ -10,6 +10,7 @@ terraform_provider_config: plural_suffix: '' plural_name: '' plural_description: '' + custom_validation: false go_sdk_config: skip: false package: @@ -37,19 +38,6 @@ panos_xpath: type: entry xpath: /params[@name="name"] locations: -- name: shared - xpath: - path: - - config - - shared - vars: [] - description: Panorama shared object - devices: - - panorama - - ngfw - validators: [] - required: false - read_only: false - name: template xpath: path: @@ -143,7 +131,10 @@ entries: - name: name description: '' validators: [] -imports: [] +imports: + target: interface + variants: + - '*' spec: params: - name: parent diff --git a/specs/network/subinterface/ethernet/layer3.yaml b/specs/network/subinterface/ethernet/layer3.yaml index 38683048..3f55b675 100644 --- a/specs/network/subinterface/ethernet/layer3.yaml +++ b/specs/network/subinterface/ethernet/layer3.yaml @@ -144,28 +144,10 @@ entries: description: '' validators: [] imports: -- variant: '' - type: template - locations: - - name: vsys - xpath: - path: - - vsys - - $vsys - - import - - network - - interface - vars: - - name: vsys - description: The vsys. - required: false - default: vsys1 - validators: [] - type: entry - description: '' - validators: [] - required: true - read_only: false + target: interface + variants: + - '*' + default_value: vsys1 spec: params: - name: parent diff --git a/specs/network/tunnels/ipsec.yaml b/specs/network/tunnels/ipsec.yaml index ecee1f10..8b267da1 100644 --- a/specs/network/tunnels/ipsec.yaml +++ b/specs/network/tunnels/ipsec.yaml @@ -10,6 +10,7 @@ terraform_provider_config: plural_suffix: '' plural_name: '' plural_description: '' + custom_validation: false go_sdk_config: skip: false package: @@ -116,7 +117,6 @@ entries: - name: name description: '' validators: [] -imports: [] spec: params: - name: anti-replay diff --git a/specs/network/virtual-router-static-route-ipv4.yaml b/specs/network/virtual-router-static-route-ipv4.yaml index 86e1b3dc..db77414f 100644 --- a/specs/network/virtual-router-static-route-ipv4.yaml +++ b/specs/network/virtual-router-static-route-ipv4.yaml @@ -5,459 +5,457 @@ terraform_provider_config: skip_datasource: false resource_type: entry resource_variants: - - singular - - plural + - singular + - plural suffix: virtual_router_static_route_ipv4 plural_suffix: virtual_router_static_routes_ipv4 plural_name: static-routes - plural_description: "" + plural_description: '' plural_type: list go_sdk_config: skip: false package: - - network - - virtual_router - - ipv4 - - staticroute + - network + - virtual_router + - ipv4 + - staticroute panos_xpath: path: - - network - - virtual-router - - $parent - - routing-table - - ip - - static-route - - $name + - network + - virtual-router + - $parent + - routing-table + - ip + - static-route + - $name vars: - - name: parent - spec: - type: entry - xpath: /params[@name="virtual-router"] - - name: name - spec: - type: entry - xpath: /params[@name="name"] + - name: parent + spec: + type: entry + xpath: /params[@name="virtual-router"] + - name: name + spec: + type: entry + xpath: /params[@name="name"] locations: - - name: ngfw - xpath: - path: - - config - - devices - - $ngfw_device - vars: - - name: ngfw_device - description: The NGFW device - required: false - default: localhost.localdomain - validators: [] - type: entry - description: Located in a specific NGFW device - devices: - - ngfw +- name: ngfw + xpath: + path: + - config + - devices + - $ngfw_device + vars: + - name: ngfw_device + description: The NGFW device + required: false + default: localhost.localdomain + validators: [] + type: entry + description: Located in a specific NGFW device + devices: + - ngfw + validators: [] + required: false + read_only: false +- name: template + xpath: + path: + - config + - devices + - $panorama_device + - template + - $template + - config + - devices + - $ngfw_device + vars: + - name: panorama_device + description: Specific Panorama device + required: false + default: localhost.localdomain + validators: [] + type: entry + - name: template + description: Specific Panorama template + required: true + validators: [] + type: entry + - name: ngfw_device + description: The NGFW device + required: false + default: localhost.localdomain + validators: [] + type: entry + description: Located in a specific template + devices: + - panorama + validators: [] + required: false + read_only: false +- name: template-stack + xpath: + path: + - config + - devices + - $panorama_device + - template-stack + - $template_stack + - config + - devices + - $ngfw_device + vars: + - name: panorama_device + description: Specific Panorama device + required: false + default: localhost.localdomain + validators: [] + type: entry + - name: template_stack + description: Specific Panorama template stack + required: true + validators: [] + type: entry + - name: ngfw_device + description: The NGFW device + required: false + default: localhost.localdomain + validators: [] + type: entry + description: Located in a specific template stack + devices: + - panorama + validators: [] + required: false + read_only: false +entries: +- name: name + description: '' + validators: [] +spec: + params: + - name: virtual-router + type: string + profiles: + - xpath: [] + codegen_overrides: + gosdk: + skip: true + terraform: + xpath_variable: virtual-router + - name: admin-dist + type: int64 + profiles: + - xpath: + - admin-dist + validators: + - type: length + spec: + min: 10 + max: 240 + spec: {} + description: adminitrative distance + required: false + - name: bfd + type: object + profiles: + - xpath: + - bfd validators: [] + spec: + params: + - name: profile + type: string + profiles: + - xpath: + - profile + validators: [] + spec: + default: None + description: BFD profile + required: false + variants: [] + description: BFD configuration required: false - read_only: false - - name: template - xpath: - path: - - config - - devices - - $panorama_device - - template - - $template - - config - - devices - - $ngfw_device - vars: - - name: panorama_device - description: Specific Panorama device - required: false - default: localhost.localdomain - validators: [] - type: entry - - name: template - description: Specific Panorama template - required: true - validators: [] - type: entry - - name: ngfw_device - description: The NGFW device - required: false - default: localhost.localdomain - validators: [] - type: entry - description: Located in a specific template - devices: - - panorama + - name: destination + type: string + profiles: + - xpath: + - destination validators: [] + spec: {} + description: Destination IP address/prefix required: false - read_only: false - - name: template-stack - xpath: - path: - - config - - devices - - $panorama_device - - template-stack - - $template_stack - - config - - devices - - $ngfw_device - vars: - - name: panorama_device - description: Specific Panorama device - required: false - default: localhost.localdomain - validators: [] - type: entry - - name: template_stack - description: Specific Panorama template stack - required: true - validators: [] - type: entry - - name: ngfw_device - description: The NGFW device - required: false - default: localhost.localdomain - validators: [] - type: entry - description: Located in a specific template stack - devices: - - panorama + - name: interface + type: string + profiles: + - xpath: + - interface validators: [] + spec: {} + description: '' required: false - read_only: false -entries: - - name: name - description: "" + - name: metric + type: int64 + profiles: + - xpath: + - metric + validators: + - type: length + spec: + min: 1 + max: 65535 + spec: + default: 10 + description: metric value (path cost) + required: false + - name: nexthop + type: object + profiles: + - xpath: + - nexthop validators: [] -imports: [] -spec: - params: - - name: virtual-router - type: string - profiles: - - xpath: [] - codegen_overrides: - gosdk: - skip: true - terraform: - xpath_variable: virtual-router - - name: admin-dist - type: int64 - profiles: + spec: + params: [] + variants: + - name: discard + type: object + profiles: - xpath: - - admin-dist - validators: + - discard + validators: [] + spec: + params: [] + variants: [] + description: Discard packet + required: false + - name: fqdn + type: string + profiles: + - xpath: + - fqdn + validators: - type: length spec: - min: 10 - max: 240 - spec: {} - description: adminitrative distance - required: false - - name: bfd - type: object - profiles: + max: 255 + spec: {} + description: nexthop address FQDN address object configuration + required: false + - name: ip-address + type: string + profiles: - xpath: - - bfd - validators: [] - spec: - params: - - name: profile - type: string - profiles: - - xpath: - - profile - validators: [] - spec: - default: None - description: BFD profile - required: false - variants: [] - description: BFD configuration - required: false - - name: destination - type: string - profiles: + - ip-address + validators: [] + spec: {} + description: Next hop IP address + required: false + - name: next-vr + type: string + profiles: - xpath: - - destination - validators: [] - spec: {} - description: Destination IP address/prefix - required: false - - name: interface - type: string - profiles: + - next-vr + validators: [] + spec: {} + description: Next hop virtual router + required: false + - name: receive + type: object + profiles: - xpath: - - interface - validators: [] - spec: {} - description: "" - required: false - - name: metric - type: int64 - profiles: + - receive + validators: [] + spec: + params: [] + variants: [] + description: Forward packet to host + required: false + description: Next hop to destination + required: false + - name: path-monitor + type: object + profiles: + - xpath: + - path-monitor + validators: [] + spec: + params: + - name: enable + type: bool + profiles: + - xpath: + - enable + validators: [] + spec: {} + description: '' + required: false + - name: failure-condition + type: enum + profiles: + - xpath: + - failure-condition + validators: + - type: values + spec: + values: + - any + - all + spec: + default: any + values: + - value: any + - value: all + description: failure condition + required: false + - name: hold-time + type: int64 + profiles: - xpath: - - metric - validators: + - hold-time + validators: - type: length spec: - min: 1 - max: 65535 - spec: - default: 10 - description: metric value (path cost) - required: false - - name: nexthop - type: object - profiles: + min: 0 + max: 1440 + spec: + default: 2 + description: hold time (minutes) + required: false + - name: monitor-destinations + type: list + profiles: - xpath: - - nexthop - validators: [] - spec: - params: [] - variants: - - name: discard - type: object - profiles: - - xpath: - - discard - validators: [] - spec: - params: [] - variants: [] - description: Discard packet - required: false - - name: fqdn - type: string - profiles: - - xpath: - - fqdn - validators: - - type: length - spec: - max: 255 - spec: {} - description: nexthop address FQDN address object configuration - required: false - - name: ip-address - type: string - profiles: - - xpath: - - ip-address - validators: [] - spec: {} - description: Next hop IP address - required: false - - name: next-vr - type: string - profiles: - - xpath: - - next-vr - validators: [] - spec: {} - description: Next hop virtual router - required: false - - name: receive + - monitor-destinations + - entry + type: entry + validators: [] + spec: + type: object + items: type: object - profiles: - - xpath: - - receive - validators: [] spec: - params: [] - variants: [] - description: Forward packet to host - required: false - description: Next hop to destination - required: false - - name: path-monitor - type: object - profiles: - - xpath: - - path-monitor - validators: [] - spec: - params: - - name: enable - type: bool - profiles: - - xpath: + params: + - name: enable + type: bool + profiles: + - xpath: - enable - validators: [] - spec: {} - description: "" - required: false - - name: failure-condition - type: enum - profiles: - - xpath: - - failure-condition - validators: - - type: values + validators: [] + spec: {} + description: '' + required: false + - name: source + type: string + profiles: + - xpath: + - source + validators: [] + spec: {} + description: Source IP address + required: false + - name: destination + type: string + profiles: + - xpath: + - destination + validators: + - type: length + spec: + max: 63 + spec: {} + description: Destination IP address + required: false + - name: interval + type: int64 + profiles: + - xpath: + - interval + validators: + - type: length + spec: + min: 1 + max: 60 spec: - values: - - any - - all - spec: - default: any - values: - - value: any - - value: all - description: failure condition - required: false - - name: hold-time - type: int64 - profiles: - - xpath: - - hold-time - validators: - - type: length + default: 3 + description: ping interval + required: false + - name: count + type: int64 + profiles: + - xpath: + - count + validators: + - type: length + spec: + min: 3 + max: 10 spec: - min: 0 - max: 1440 - spec: - default: 2 - description: hold time (minutes) - required: false - - name: monitor-destinations - type: list - profiles: - - xpath: - - monitor-destinations - - entry - type: entry - validators: [] - spec: - type: object - items: - type: object - spec: - params: - - name: enable - type: bool - profiles: - - xpath: - - enable - validators: [] - spec: {} - description: "" - required: false - - name: source - type: string - profiles: - - xpath: - - source - validators: [] - spec: {} - description: Source IP address - required: false - - name: destination - type: string - profiles: - - xpath: - - destination - validators: - - type: length - spec: - max: 63 - spec: {} - description: Destination IP address - required: false - - name: interval - type: int64 - profiles: - - xpath: - - interval - validators: - - type: length - spec: - min: 1 - max: 60 - spec: - default: 3 - description: ping interval - required: false - - name: count - type: int64 - profiles: - - xpath: - - count - validators: - - type: length - spec: - min: 3 - max: 10 - spec: - default: 5 - description: ping count - required: false - variants: [] - description: "" - required: false - variants: [] - description: Static Route Path Monitoring - required: false - - name: route-table - type: object - profiles: - - xpath: - - route-table - validators: [] - spec: - params: [] - variants: - - name: both - type: object - profiles: - - xpath: - - both - validators: [] - spec: - params: [] - variants: [] - description: Install route into both unicast and multicast routing table - required: false - variant_group_id: 0 - - name: multicast - type: object - profiles: - - xpath: - - multicast - validators: [] - spec: - params: [] - variants: [] - description: - Install route into multicast routing table, this will create - multicast routing table if not exists - required: false - variant_group_id: 0 - - name: no-install - type: object - profiles: - - xpath: - - no-install - validators: [] - spec: - params: [] + default: 5 + description: ping count + required: false variants: [] - description: Do not install route into forwarding table - required: false - variant_group_id: 0 - - name: unicast - type: object - profiles: - - xpath: - - unicast - validators: [] - spec: - params: [] - variants: [] - description: Install route into unicast routing table - required: false - variant_group_id: 0 - description: target routing table to install the route - required: false + description: '' + required: false + variants: [] + description: Static Route Path Monitoring + required: false + - name: route-table + type: object + profiles: + - xpath: + - route-table + validators: [] + spec: + params: [] + variants: + - name: both + type: object + profiles: + - xpath: + - both + validators: [] + spec: + params: [] + variants: [] + description: Install route into both unicast and multicast routing table + required: false + variant_group_id: 0 + - name: multicast + type: object + profiles: + - xpath: + - multicast + validators: [] + spec: + params: [] + variants: [] + description: Install route into multicast routing table, this will create + multicast routing table if not exists + required: false + variant_group_id: 0 + - name: no-install + type: object + profiles: + - xpath: + - no-install + validators: [] + spec: + params: [] + variants: [] + description: Do not install route into forwarding table + required: false + variant_group_id: 0 + - name: unicast + type: object + profiles: + - xpath: + - unicast + validators: [] + spec: + params: [] + variants: [] + description: Install route into unicast routing table + required: false + variant_group_id: 0 + description: target routing table to install the route + required: false variants: [] diff --git a/specs/network/virtual-router-static-route-ipv6.yaml b/specs/network/virtual-router-static-route-ipv6.yaml index ec839140..e40563f3 100644 --- a/specs/network/virtual-router-static-route-ipv6.yaml +++ b/specs/network/virtual-router-static-route-ipv6.yaml @@ -131,7 +131,6 @@ entries: - name: name description: '' validators: [] -imports: [] spec: params: - name: virtual-router diff --git a/specs/network/virtual-router.yaml b/specs/network/virtual-router.yaml index 33ff2469..5113268d 100644 --- a/specs/network/virtual-router.yaml +++ b/specs/network/virtual-router.yaml @@ -245,7 +245,10 @@ entries: - name: name description: '' validators: [] -imports: [] +imports: + target: virtual-router + variants: + - '*' spec: params: - name: admin-dists diff --git a/specs/network/zone.yaml b/specs/network/zone.yaml index c8c85b9b..726e47b0 100644 --- a/specs/network/zone.yaml +++ b/specs/network/zone.yaml @@ -139,7 +139,6 @@ entries: - name: name description: '' validators: [] -imports: [] spec: params: - name: device-acl diff --git a/specs/objects/address-group.yaml b/specs/objects/address-group.yaml index 586c9cae..93eb50bc 100644 --- a/specs/objects/address-group.yaml +++ b/specs/objects/address-group.yaml @@ -103,7 +103,6 @@ entries: - name: name description: '' validators: [] -imports: [] spec: params: - name: description diff --git a/specs/objects/address.yaml b/specs/objects/address.yaml index 1433991a..25ed1aa2 100644 --- a/specs/objects/address.yaml +++ b/specs/objects/address.yaml @@ -103,7 +103,6 @@ entries: - name: name description: '' validators: [] -imports: [] spec: params: - name: description diff --git a/specs/objects/administrative-tag.yaml b/specs/objects/administrative-tag.yaml index e1fe0d2b..6280c684 100644 --- a/specs/objects/administrative-tag.yaml +++ b/specs/objects/administrative-tag.yaml @@ -102,7 +102,6 @@ entries: - name: name description: '' validators: [] -imports: [] spec: params: - name: color diff --git a/specs/objects/application-group.yaml b/specs/objects/application-group.yaml index b1bbb4ac..d7c68e39 100644 --- a/specs/objects/application-group.yaml +++ b/specs/objects/application-group.yaml @@ -102,7 +102,6 @@ entries: - name: name description: '' validators: [] -imports: [] spec: params: - name: disable-override diff --git a/specs/objects/application.yaml b/specs/objects/application.yaml index ae3e2722..1ee036fa 100644 --- a/specs/objects/application.yaml +++ b/specs/objects/application.yaml @@ -101,7 +101,6 @@ entries: - name: name description: '' validators: [] -imports: [] spec: params: - name: able-to-transfer-file diff --git a/specs/objects/custom-data-object.yaml b/specs/objects/custom-data-object.yaml index c880df1a..40a54086 100644 --- a/specs/objects/custom-data-object.yaml +++ b/specs/objects/custom-data-object.yaml @@ -103,7 +103,6 @@ entries: - name: name description: '' validators: [] -imports: [] spec: params: - name: description diff --git a/specs/objects/custom-url-category.yaml b/specs/objects/custom-url-category.yaml index 5a9fe346..254167ee 100644 --- a/specs/objects/custom-url-category.yaml +++ b/specs/objects/custom-url-category.yaml @@ -104,7 +104,6 @@ entries: - name: name description: '' validators: [] -imports: [] spec: params: - name: description diff --git a/specs/objects/custom-vulnerability.yaml b/specs/objects/custom-vulnerability.yaml index df2cb49f..98eda989 100644 --- a/specs/objects/custom-vulnerability.yaml +++ b/specs/objects/custom-vulnerability.yaml @@ -103,7 +103,6 @@ entries: - name: name description: '' validators: [] -imports: [] spec: params: - name: affected-host diff --git a/specs/objects/ephemeral-vm-auth-key.yaml b/specs/objects/ephemeral-vm-auth-key.yaml index 38633ca1..aa11230c 100644 --- a/specs/objects/ephemeral-vm-auth-key.yaml +++ b/specs/objects/ephemeral-vm-auth-key.yaml @@ -14,7 +14,6 @@ panos_xpath: path: ["vmauthkey"] # unused locations: [] entries: [] -imports: [] version: 11.0.2 spec: params: diff --git a/specs/objects/external-dynamic-list.yaml b/specs/objects/external-dynamic-list.yaml index 4a52f43b..1b963fbb 100644 --- a/specs/objects/external-dynamic-list.yaml +++ b/specs/objects/external-dynamic-list.yaml @@ -102,7 +102,6 @@ entries: - name: name description: '' validators: [] -imports: [] spec: params: - name: disable-override diff --git a/specs/objects/profiles/antivirus.yaml b/specs/objects/profiles/antivirus.yaml index 01b4b363..159484cd 100644 --- a/specs/objects/profiles/antivirus.yaml +++ b/specs/objects/profiles/antivirus.yaml @@ -104,7 +104,6 @@ entries: - name: name description: '' validators: [] -imports: [] spec: params: - name: application diff --git a/specs/objects/profiles/certificate.yaml b/specs/objects/profiles/certificate.yaml index de5dcd7b..55704df7 100644 --- a/specs/objects/profiles/certificate.yaml +++ b/specs/objects/profiles/certificate.yaml @@ -201,7 +201,6 @@ entries: - name: name description: '' validators: [] -imports: [] spec: params: - name: block-expired-certificate diff --git a/specs/objects/profiles/file-blocking.yaml b/specs/objects/profiles/file-blocking.yaml index fa1c33d0..63b67c86 100644 --- a/specs/objects/profiles/file-blocking.yaml +++ b/specs/objects/profiles/file-blocking.yaml @@ -103,7 +103,6 @@ entries: - name: name description: '' validators: [] -imports: [] spec: params: - name: description diff --git a/specs/objects/profiles/ike-crypto-profile.yaml b/specs/objects/profiles/ike-crypto-profile.yaml index db0ff675..6fd34993 100644 --- a/specs/objects/profiles/ike-crypto-profile.yaml +++ b/specs/objects/profiles/ike-crypto-profile.yaml @@ -116,7 +116,6 @@ entries: - name: name description: '' validators: [] -imports: [] spec: params: - name: authentication-multiple diff --git a/specs/objects/profiles/ipsec-crypto-profile.yaml b/specs/objects/profiles/ipsec-crypto-profile.yaml index 652faa4b..09886487 100644 --- a/specs/objects/profiles/ipsec-crypto-profile.yaml +++ b/specs/objects/profiles/ipsec-crypto-profile.yaml @@ -116,7 +116,6 @@ entries: - name: name description: '' validators: [] -imports: [] spec: params: - name: dh-group diff --git a/specs/objects/profiles/log-forwarding.yaml b/specs/objects/profiles/log-forwarding.yaml index 16fc7472..58cf9fa3 100644 --- a/specs/objects/profiles/log-forwarding.yaml +++ b/specs/objects/profiles/log-forwarding.yaml @@ -71,7 +71,6 @@ entries: - name: name description: '' validators: [] -imports: [] spec: params: - name: description diff --git a/specs/objects/profiles/security-profile-group.yaml b/specs/objects/profiles/security-profile-group.yaml index 922cab28..42fb8ac7 100644 --- a/specs/objects/profiles/security-profile-group.yaml +++ b/specs/objects/profiles/security-profile-group.yaml @@ -70,7 +70,6 @@ entries: - name: name description: '' validators: [] -imports: [] spec: params: - name: data-filtering diff --git a/specs/objects/profiles/ssl-tls-service.yaml b/specs/objects/profiles/ssl-tls-service.yaml index e947156a..492e3ccc 100644 --- a/specs/objects/profiles/ssl-tls-service.yaml +++ b/specs/objects/profiles/ssl-tls-service.yaml @@ -202,7 +202,6 @@ entries: - name: name description: '' validators: [] -imports: [] spec: params: - name: certificate diff --git a/specs/objects/profiles/url-filtering.yaml b/specs/objects/profiles/url-filtering.yaml index f569c4e6..c0bfec21 100644 --- a/specs/objects/profiles/url-filtering.yaml +++ b/specs/objects/profiles/url-filtering.yaml @@ -71,7 +71,6 @@ entries: - name: name description: '' validators: [] -imports: [] spec: params: - name: alert diff --git a/specs/objects/profiles/vulnerability.yaml b/specs/objects/profiles/vulnerability.yaml index 08fd6305..9e6b06aa 100644 --- a/specs/objects/profiles/vulnerability.yaml +++ b/specs/objects/profiles/vulnerability.yaml @@ -71,7 +71,6 @@ entries: - name: name description: '' validators: [] -imports: [] spec: params: - name: cloud-inline-analysis diff --git a/specs/objects/profiles/wildfire-analysis.yaml b/specs/objects/profiles/wildfire-analysis.yaml index f097fbf8..6f1f2f2d 100644 --- a/specs/objects/profiles/wildfire-analysis.yaml +++ b/specs/objects/profiles/wildfire-analysis.yaml @@ -71,7 +71,6 @@ entries: - name: name description: '' validators: [] -imports: [] spec: params: - name: description diff --git a/specs/objects/service-group.yaml b/specs/objects/service-group.yaml index cfdc1662..ca688c87 100644 --- a/specs/objects/service-group.yaml +++ b/specs/objects/service-group.yaml @@ -103,7 +103,6 @@ entries: - name: name description: '' validators: [] -imports: [] spec: params: - name: disable-override diff --git a/specs/objects/service.yaml b/specs/objects/service.yaml index fe8be348..e34a662b 100644 --- a/specs/objects/service.yaml +++ b/specs/objects/service.yaml @@ -102,7 +102,6 @@ entries: - name: name description: '' validators: [] -imports: [] spec: params: - name: description diff --git a/specs/policies/decryption-policy.yaml b/specs/policies/decryption-policy.yaml index 114bec62..07fc2b13 100644 --- a/specs/policies/decryption-policy.yaml +++ b/specs/policies/decryption-policy.yaml @@ -125,7 +125,6 @@ entries: - name: name description: '' validators: [] -imports: [] spec: params: - name: action diff --git a/specs/policies/default-security-policy-rule.yaml b/specs/policies/default-security-policy-rule.yaml index 5f5edc7f..94332440 100644 --- a/specs/policies/default-security-policy-rule.yaml +++ b/specs/policies/default-security-policy-rule.yaml @@ -73,7 +73,6 @@ entries: - name: name description: '' validators: [] -imports: [] spec: params: - name: action diff --git a/specs/policies/network-address-translation.yaml b/specs/policies/network-address-translation.yaml index 97a53f54..45ef7334 100644 --- a/specs/policies/network-address-translation.yaml +++ b/specs/policies/network-address-translation.yaml @@ -125,7 +125,6 @@ entries: - name: name description: '' validators: [] -imports: [] spec: params: - name: active-active-device-binding diff --git a/specs/policies/security-policy-rule.yaml b/specs/policies/security-policy-rule.yaml index 3dfc4b60..b264573b 100644 --- a/specs/policies/security-policy-rule.yaml +++ b/specs/policies/security-policy-rule.yaml @@ -125,7 +125,6 @@ entries: - name: name description: '' validators: [] -imports: [] spec: params: - name: action diff --git a/templates/sdk/location.tmpl b/templates/sdk/location.tmpl index a3e6054f..6de114b2 100644 --- a/templates/sdk/location.tmpl +++ b/templates/sdk/location.tmpl @@ -1,12 +1,6 @@ package {{packageName .GoSdkPath}} -{{- if $.Imports}} - {{ renderImports "location" "imports"}} -{{- else }} - {{ renderImports "location"}} -{{- end }} - -{{ RenderEntryImportStructs }} +{{ renderImports "location"}} type Location struct { {{range $location := .OrderedLocations}} diff --git a/templates/sdk/service.tmpl b/templates/sdk/service.tmpl index 51bb8726..8eb223dc 100644 --- a/templates/sdk/service.tmpl +++ b/templates/sdk/service.tmpl @@ -1,14 +1,14 @@ package {{packageName .GoSdkPath}} {{- if .Entry}} - {{- if $.Imports}} + {{- if $.Imports.Variants}} {{- if $.Spec.Params.uuid}} {{- if $.HasAuditComments }} - {{renderImports "service" "filtering" "sync" "audit" "rule" "version" "movement"}} + {{renderImports "service" "filtering" "sync" "audit" "rule" "version" "movement" "imports"}} {{- else }} - {{renderImports "service" "filtering" "sync" "rule" "version" "movement"}} + {{renderImports "service" "filtering" "sync" "rule" "version" "movement" "imports"}} {{- end }} {{- else}} - {{renderImports "service" "filtering" "sync"}} + {{renderImports "service" "filtering" "sync" "imports"}} {{- end}} {{- else}} {{- if $.Spec.Params.uuid}} @@ -41,9 +41,7 @@ client: client, } // Create adds new item, then returns the result. - {{- if and (.Entry) (.Imports)}} -func (s *Service) Create(ctx context.Context, loc Location, importLocations []ImportLocation, entry *Entry) (*Entry, error) { - {{- else if (.Entry) }} + {{- if (.Entry) }} func (s *Service) Create(ctx context.Context, loc Location, entry *Entry) (*Entry, error) { {{- else }} func (s *Service) Create(ctx context.Context, loc Location, config *Config) (*Config, error) { @@ -75,13 +73,6 @@ func (s *Service) Create(ctx context.Context, loc Location, config *Config) (*Co return nil, err } - {{- if .Imports }} - err = s.ImportToLocations(ctx, loc, importLocations, entry.Name) - if err != nil { - return nil, err - } - {{- end }} - return s.ReadWithXpath(ctx, util.AsXpath(path), "get") } @@ -127,137 +118,147 @@ func (s *Service) {{ $funcDef }} { {{- end }} } -{{- if .Imports }} +{{- if .Imports.Variants }} -func (s *Service) ImportToLocations(ctx context.Context, loc Location, importLocations []ImportLocation, entryName string) error { - vn := s.client.Versioning() +type member struct { + Name string `xml:",chardata"` +} - importToLocation := func(il ImportLocation) error { - xpath, err := il.XpathForLocation(vn, loc) - if err != nil { - return err - } +type response struct { + Members []member `xml:"result>{{ .Imports.Target }}>member"` +} - mutex := locking.GetMutex(locking.XpathLockCategory, util.AsXpath(xpath)) - mutex.Lock() - defer mutex.Unlock() +type request struct { + XMLName xml.Name `xml:"{{ .Imports.Target }}"` + Members []member `xml:"member"` +} - cmd := &xmlapi.Config{ - Action: "get", - Xpath: util.AsXpath(xpath), - } +func (s *Service) ImportToLocation(ctx context.Context, loc Location, vsys string, name string) error { + vn := s.client.Versioning() - bytes, _, err := s.client.Communicate(ctx, cmd, false, nil) - if err != nil && !errors.IsObjectNotFound(err) { - return err - } + xpath, err := loc.XpathPrefix(vn) + if err != nil { + return err + } + xpath = append(xpath, []string{"vsys", util.AsEntryXpath(vsys), "import", "network", "{{ .Imports.Target }}"}...) - existing, err := il.UnmarshalPangoXML(bytes) - if err != nil { - return err - } + mutex := locking.GetMutex(locking.XpathLockCategory, util.AsXpath(xpath)) + mutex.Lock() + defer mutex.Unlock() - for _, elt := range existing { - if elt == entryName { - return nil - } - } + cmd := &xmlapi.Config{ + Action: "get", + Xpath: util.AsXpath(xpath), + } + + bytes, _, err := s.client.Communicate(ctx, cmd, false, nil) + if err != nil && !errors.IsObjectNotFound(err) { + return err + } - existing = append(existing, entryName) + var resp response + err = xml.Unmarshal(bytes, &resp) + if err != nil { + return err + } - element, err := il.MarshalPangoXML(existing) - if err != nil { - return err - } + existing := resp.Members - cmd = &xmlapi.Config{ - Action: "set", - Xpath: util.AsXpath(xpath[:len(xpath)-1]), - Element: element, + for _, elt := range existing { + if elt.Name == name { + return nil } + } - _, _, err = s.client.Communicate(ctx, cmd, false, nil) - if err != nil { - return err - } + existing = append(existing, member{Name: name}) + req := request{ + Members: existing, + } + + element, err := xml.Marshal(req) + if err != nil { return err } - for _, elt := range importLocations { - err := importToLocation(elt) - if err != nil { - return err - } - } + cmd = &xmlapi.Config{ + Action: "set", + Xpath: util.AsXpath(xpath[:len(xpath)-1]), + Element: string(element), + } + + _, _, err = s.client.Communicate(ctx, cmd, false, nil) + if err != nil { + return err + } return nil } -func (s *Service) UnimportFromLocations(ctx context.Context, loc Location, importLocations []ImportLocation, values []string) error { +func (s *Service) UnimportFromLocation(ctx context.Context, loc Location, vsys string, name string) error { vn := s.client.Versioning() - valuesByName := make(map[string]bool) - for _, elt := range values { - valuesByName[elt] = true - } - unimportFromLocation := func(il ImportLocation) error { - xpath, err := il.XpathForLocation(vn, loc) - if err != nil { - return err - } + xpath, err := loc.XpathPrefix(vn) + if err != nil { + return err + } + xpath = append(xpath, []string{"vsys", util.AsEntryXpath(vsys), "import", "network", "{{ .Imports.Target }}"}...) - mutex := locking.GetMutex(locking.XpathLockCategory, util.AsXpath(xpath)) - mutex.Lock() - defer mutex.Unlock() + mutex := locking.GetMutex(locking.XpathLockCategory, util.AsXpath(xpath)) + mutex.Lock() + defer mutex.Unlock() - cmd := &xmlapi.Config{ - Action: "get", - Xpath: util.AsXpath(xpath), - } + cmd := &xmlapi.Config{ + Action: "get", + Xpath: util.AsXpath(xpath), + } - bytes, _, err := s.client.Communicate(ctx, cmd, false, nil) - if err != nil && !errors.IsObjectNotFound(err) { - return err - } + bytes, _, err := s.client.Communicate(ctx, cmd, false, nil) + if err != nil && !errors.IsObjectNotFound(err) { + return err + } - existing, err := il.UnmarshalPangoXML(bytes) - if err != nil { - return err - } + var resp response + err = xml.Unmarshal(bytes, &resp) + if err != nil { + return err + } - var filtered []string - for _, elt := range existing { - if _, found := valuesByName[elt]; !found { - filtered = append(filtered, elt) - } - } + existing := resp.Members - element, err := il.MarshalPangoXML(filtered) - if err != nil { - return err + var filtered []member + var updateRequired bool + for _, elt := range existing { + if elt.Name == name { + updateRequired = true + } else { + filtered = append(filtered, elt) } + } - cmd = &xmlapi.Config{ - Action: "edit", - Xpath: util.AsXpath(xpath), - Element: element, - } + if !updateRequired { + return nil + } - _, _, err = s.client.Communicate(ctx, cmd, false, nil) - if err != nil { - return err - } + req := request{ + Members: filtered, + } + element, err := xml.Marshal(req) + if err != nil { return err } - for _, elt := range importLocations { - err := unimportFromLocation(elt) - if err != nil { - return err - } - } + cmd = &xmlapi.Config{ + Action: "edit", + Xpath: util.AsXpath(xpath), + Element: string(element), + } + + _, _, err = s.client.Communicate(ctx, cmd, false, nil) + if err != nil { + return err + } return nil } @@ -497,18 +498,18 @@ func (s *Service) UpdateWithXpath(ctx context.Context, xpath string, entry *{{ $ } // Delete deletes the given item. -{{- if and .Entry .Imports}} - func (s *Service) Delete(ctx context.Context, loc Location, importLocations []ImportLocation, name ...string) error { +{{- if and .Entry .Imports.Variants}} + func (s *Service) Delete(ctx context.Context, loc Location, name ...string) error { {{- if $.Spec.Params.uuid}} - return s.delete(ctx, loc, importLocations, name, true) + return s.delete(ctx, loc, name, true) {{- else}} - return s.delete(ctx, loc, importLocations, name) + return s.delete(ctx, loc, name) {{- end}} } {{- if $.Spec.Params.uuid}} // DeleteById deletes the given item with specified ID. - func (s *Service) DeleteById(ctx context.Context, loc Location, importLocations []ImportLocation, uuid ...string) error { - return s.delete(ctx, loc, importLocations, uuid, false) + func (s *Service) DeleteById(ctx context.Context, loc Location, uuid ...string) error { + return s.delete(ctx, loc, uuid, false) } {{- end}} {{- else if .Entry }} @@ -531,11 +532,11 @@ func (s *Service) UpdateWithXpath(ctx context.Context, xpath string, entry *{{ $ } {{- end}} -{{- if and .Entry .Imports }} +{{- if and .Entry .Imports.Variants }} {{- if $.Spec.Params.uuid}} - func (s *Service) delete(ctx context.Context, loc Location, importLocations []ImportLocation, values []string, byName bool) error { + func (s *Service) delete(ctx context.Context, loc Location, values []string, byName bool) error { {{- else}} - func (s *Service) delete(ctx context.Context, loc Location, importLocations []ImportLocation, values []string) error { + func (s *Service) delete(ctx context.Context, loc Location, values []string) error { {{- end}} {{- else if .Entry}} {{- if $.Spec.Params.uuid}} @@ -573,12 +574,7 @@ vn := s.client.Versioning() {{- if .Entry}} var err error deletes := xmlapi.NewMultiConfig(len(values)) - {{- if .Imports }} - err = s.UnimportFromLocations(ctx, loc, importLocations, values) - if err != nil { - return err - } - {{- end }} + for _, value := range values { var path []string path, err = loc.XpathWithComponents(vn, util.AsEntryXpath(value)) @@ -683,7 +679,7 @@ return err {{- if false }} // RemoveFromImport removes the given config object from import func (s *Service) RemoveFromImport(ctx context.Context, loc Location, entry Entry) error { - {{- range $_, $import := $.Imports}} + {{- range $_, $import := $.Imports.Variants}} if loc.{{$import.Name.CamelCase}} != nil { templateLocation, templateApi, templateEntry, importedNames, err := s.getImportedNamesFor{{$import.Name.CamelCase}}(ctx, loc, entry) if err != nil { diff --git a/templates/terraform-provider/common/common.tmpl b/templates/terraform-provider/common/common.tmpl index 8ff756db..231fd60a 100644 --- a/templates/terraform-provider/common/common.tmpl +++ b/templates/terraform-provider/common/common.tmpl @@ -1,3 +1,6 @@ +{{ RenderIdentityModel }} + +{{ RenderIdentitySchema }} {{- if HasLocations }} {{- RenderLocationStructs }} @@ -7,4 +10,6 @@ {{- RenderLocationMarshallers }} {{- RenderLocationAttributeTypes }} + +{{- RenderLocationAsIdentityGetter }} {{- end }} diff --git a/templates/terraform-provider/common/identity_model.tmpl b/templates/terraform-provider/common/identity_model.tmpl new file mode 100644 index 00000000..e3aed08f --- /dev/null +++ b/templates/terraform-provider/common/identity_model.tmpl @@ -0,0 +1,6 @@ + +type {{ .StructName }} struct { +{{- range .Fields }} + {{ .Name }} {{ .Type }} `tfsdk:"{{ .Tag }}"` +{{ end }} +} diff --git a/templates/terraform-provider/common/identity_schema.tmpl b/templates/terraform-provider/common/identity_schema.tmpl new file mode 100644 index 00000000..89ab7343 --- /dev/null +++ b/templates/terraform-provider/common/identity_schema.tmpl @@ -0,0 +1,16 @@ + +func (o {{ .StructName }}) IdentitySchema(_ context.Context, _ resource.IdentitySchemaRequest, resp *resource.IdentitySchemaResponse) { + resp.IdentitySchema = identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ +{{- range .Fields }} + "{{ .Name }}": {{ .Type }}{ + {{- if .RequiredForImport }} + RequiredForImport: true, + {{- else if .OptionalForImport }} + OptionalForImport: true, + {{- end }} + }, +{{- end }} + }, + } +} diff --git a/templates/terraform-provider/datasource/datasource.tmpl b/templates/terraform-provider/datasource/datasource.tmpl index 9e5311e6..63b7ed76 100644 --- a/templates/terraform-provider/datasource/datasource.tmpl +++ b/templates/terraform-provider/datasource/datasource.tmpl @@ -16,7 +16,7 @@ type {{ dataSourceStructName }} struct { {{- if IsCustom }} custom *{{ structName }}Custom {{- else if and IsEntry HasImports }} - manager *sdkmanager.ImportableEntryObjectManager[*{{ resourceSDKName }}.Entry, {{ resourceSDKName }}.Location, {{ resourceSDKName }}.ImportLocation, *{{ resourceSDKName }}.Service] + manager *sdkmanager.ImportableEntryObjectManager[*{{ resourceSDKName }}.Entry, {{ resourceSDKName }}.Location, *{{ resourceSDKName }}.Service] {{- else if IsEntry }} manager *sdkmanager.EntryObjectManager[*{{ resourceSDKName }}.Entry, {{ resourceSDKName }}.Location, *{{ resourceSDKName }}.Service] {{- else if IsUuid }} diff --git a/templates/terraform-provider/import/import_location_assignment.tmpl b/templates/terraform-provider/import/import_location_assignment.tmpl new file mode 100644 index 00000000..a5aed4ca --- /dev/null +++ b/templates/terraform-provider/import/import_location_assignment.tmpl @@ -0,0 +1,48 @@ + +{ +var terraformLocation {{ .StructName }}Location +resp.Diagnostics.Append({{ $.Source }}.Location.As(ctx, &terraformLocation, basetypes.ObjectAsOptions{})...) +if resp.Diagnostics.HasError() { + return +} + +{{- if .Variants }} +var locationRequiresImport bool + {{- range .Variants }} +if !{{ $.Source }}.{{ .CamelCase }}.IsNull() { + locationRequiresImport = true +} + {{- end }} +{{- else }} +locationRequiresImport := true +{{- end }} + +if !terraformLocation.Template.IsNull() { + var terraformInnerLocation {{ .StructName }}TemplateLocation + resp.Diagnostics.Append(terraformLocation.Template.As(ctx, &terraformInnerLocation, basetypes.ObjectAsOptions{})...) + if resp.Diagnostics.HasError() { + return + } + + // if the vsys value is not known at this stage, we must explicitly set it to null ourselves. + if terraformInnerLocation.Vsys.IsUnknown() { + terraformInnerLocation.Vsys = types.StringNull() + object, diags := types.ObjectValueFrom(ctx, terraformInnerLocation.AttributeTypes(), terraformInnerLocation) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + terraformLocation.Template = object + + object, diags = types.ObjectValueFrom(ctx, terraformLocation.AttributeTypes(), terraformLocation) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + {{ $.Source }}.Location = object + } else if locationRequiresImport && !terraformInnerLocation.Vsys.IsNull() && terraformInnerLocation.Vsys.ValueString() != "" { + {{ $.Destination }} = terraformInnerLocation.Vsys.ValueString() + } +} + +} diff --git a/templates/terraform-provider/location/identity.tmpl b/templates/terraform-provider/location/identity.tmpl new file mode 100644 index 00000000..c315ea43 --- /dev/null +++ b/templates/terraform-provider/location/identity.tmpl @@ -0,0 +1,51 @@ + +{{- define "topLevelLocation" }} + {{- range $.Specs }} + {{- if not .TopLevel }}{{- continue }}{{- end }} + {{- range .Fields }} +if !o.{{ .Name.CamelCase }}.IsNull() { + var location {{ .TerraformType }} + diags := o.{{ .Name.CamelCase }}.As(ctx, &location, basetypes.ObjectAsOptions{}) + if diags.HasError() { + return types.StringNull(), diags + } + + identity, innerDiags := location.Identity(ctx) + diags.Append(innerDiags...) + if diags.HasError() { + return types.StringNull(), diags + } + + formatted := fmt.Sprintf("/{{ .Name.Dashed }}%s", identity.ValueString()) + return types.StringValue(formatted), nil +} + {{- end }} + {{- end }} +return types.StringNull(), diag.Diagnostics{diag.NewAttributeErrorDiagnostic( + path.Root("location"), + "Location Attribute is Not Set", + "The 'location' attribute must be configured with a non-empty value.", +)} +{{- end }} + +{{- define "location" }} +identity := []string{""} + {{- range .Fields }} + {{- if not .AsIdentity }}{{- continue }}{{- end }} +{ +value := fmt.Sprintf("{{ .Name.Dashed }}[\"%s\"]", o.{{ .Name.CamelCase }}.ValueString()) +identity = append(identity, value) +} + {{- end }} +return types.StringValue(strings.Join(identity, "/")), nil +{{- end }} + +{{- range .Specs }} +func (o *{{ .StructName }}) Identity(ctx context.Context) (types.String, diag.Diagnostics) { + {{- if .TopLevel }} + {{- template "topLevelLocation" Map "Specs" $.Specs }} + {{- else }} + {{- template "location" . }} + {{- end }} +} +{{- end }} diff --git a/templates/terraform-provider/location/state_to_pango.tmpl b/templates/terraform-provider/location/state_to_pango.tmpl index d368f115..d165b017 100644 --- a/templates/terraform-provider/location/state_to_pango.tmpl +++ b/templates/terraform-provider/location/state_to_pango.tmpl @@ -1,6 +1,6 @@ -{ var terraformLocation {{ .TerraformStructName }} +{ resp.Diagnostics.Append({{ $.Source }}.As(ctx, &terraformLocation, basetypes.ObjectAsOptions{})...) if resp.Diagnostics.HasError() { return diff --git a/templates/terraform-provider/resource/create.tmpl b/templates/terraform-provider/resource/create.tmpl index c2212c19..58650220 100644 --- a/templates/terraform-provider/resource/create.tmpl +++ b/templates/terraform-provider/resource/create.tmpl @@ -55,16 +55,16 @@ } {{- if .HasImports }} - var importLocation {{ .resourceSDKName }}.ImportLocation - {{ RenderImportLocationAssignment "state.Location" "importLocation" }} created, err := o.manager.Create(ctx, location, components, obj) if err != nil { resp.Diagnostics.AddError("Error in create", err.Error()) return } - if importLocation != nil { - err = o.manager.ImportToLocations(ctx, location, []{{ .resourceSDKName }}.ImportLocation{importLocation}, obj.Name) + var importVsys string + {{ RenderImportLocationAssignment "state" "importVsys" }} + if importVsys != "" { + err = o.manager.ImportToLocation(ctx, location, importVsys, obj.Name) if err != nil { resp.Diagnostics.AddError("Failed to import resource into location", err.Error()) return @@ -88,4 +88,20 @@ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +{{- if .HasEntryName }} + identityLocation, diags := terraformLocation.Identity(ctx) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + identity := {{ .structName }}IdentityModel{ +{{- if .HasEntryName }} + Name: state.Name, +{{- end }} + Location: identityLocation, + } + + resp.Diagnostics.Append(resp.Identity.Set(ctx, identity)...) +{{- end }} {{- /* Done */ -}} diff --git a/templates/terraform-provider/resource/delete.tmpl b/templates/terraform-provider/resource/delete.tmpl index 13964751..b689147b 100644 --- a/templates/terraform-provider/resource/delete.tmpl +++ b/templates/terraform-provider/resource/delete.tmpl @@ -32,19 +32,18 @@ return } {{- if .HasImports }} - var importLocation {{ .resourceSDKName }}.ImportLocation - {{ RenderImportLocationAssignment "state.Location" "importLocation" }} - if importLocation != nil { - err = o.manager.UnimportFromLocations(ctx, location, []{{ .resourceSDKName }}.ImportLocation{importLocation}, state.Name.ValueString()) + var importVsys string + {{ RenderImportLocationAssignment "state" "importVsys" }} + if importVsys != "" { + err = o.manager.UnimportFromLocation(ctx, location, importVsys, state.Name.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Failed to unimport resource from location", err.Error()) + return + } } - if err != nil { - resp.Diagnostics.AddError("Error in delete", err.Error()) - return - } - err = o.manager.Delete(ctx, location, []{{ .resourceSDKName }}.ImportLocation{importLocation}, components, []string{state.Name.ValueString()}) - {{- else }} - err = o.manager.Delete(ctx, location, components, []string{state.Name.ValueString()}) {{- end }} + + err = o.manager.Delete(ctx, location, components, []string{state.Name.ValueString()}) if err != nil && !errors.Is(err, sdkmanager.ErrObjectNotFound) { resp.Diagnostics.AddError("Error in delete", err.Error()) return diff --git a/templates/terraform-provider/resource/resource.tmpl b/templates/terraform-provider/resource/resource.tmpl index ac8a1689..5e5e6523 100644 --- a/templates/terraform-provider/resource/resource.tmpl +++ b/templates/terraform-provider/resource/resource.tmpl @@ -12,6 +12,9 @@ var ( _ resource.Resource = &{{ resourceStructName }}{} _ resource.ResourceWithConfigure = &{{ resourceStructName }}{} _ resource.ResourceWithImportState = &{{ resourceStructName }}{} + {{- if and (not IsCustom) (not IsResourcePlural) (not IsConfig) }} + _ resource.ResourceWithIdentity = &{{ resourceStructName }}{} + {{- end }} ) {{- end }} @@ -38,7 +41,7 @@ type {{ resourceStructName }} struct { {{- if IsCustom }} custom *{{ structName }}Custom {{- else if and IsEntry HasImports }} - manager *sdkmanager.ImportableEntryObjectManager[*{{ resourceSDKName }}.Entry, {{ resourceSDKName }}.Location, {{ resourceSDKName }}.ImportLocation, *{{ resourceSDKName }}.Service] + manager *sdkmanager.ImportableEntryObjectManager[*{{ resourceSDKName }}.Entry, {{ resourceSDKName }}.Location, *{{ resourceSDKName }}.Service] {{- else if IsEntry }} manager *sdkmanager.EntryObjectManager[*{{ resourceSDKName }}.Entry, {{ resourceSDKName }}.Location, *{{ resourceSDKName }}.Service] {{- else if IsUuid }}