diff --git a/nsxt/provider.go b/nsxt/provider.go index a318b6929..7714f67a1 100644 --- a/nsxt/provider.go +++ b/nsxt/provider.go @@ -522,6 +522,7 @@ func Provider() *schema.Provider { "nsxt_vpc_ip_address_allocation": resourceNsxtVpcIpAddressAllocation(), "nsxt_vpc_subnet": resourceNsxtVpcSubnet(), "nsxt_policy_transit_gateway_nat_rule": resourceNsxtPolicyTransitGatewayNatRule(), + "nsxt_vpc_static_route": resourceNsxtVpcStaticRoutes(), }, ConfigureFunc: providerConfigure, diff --git a/nsxt/resource_nsxt_vpc_static_routes.go b/nsxt/resource_nsxt_vpc_static_routes.go new file mode 100644 index 000000000..210548a8a --- /dev/null +++ b/nsxt/resource_nsxt_vpc_static_routes.go @@ -0,0 +1,223 @@ +/* Copyright © 2024 Broadcom, Inc. All Rights Reserved. + SPDX-License-Identifier: MPL-2.0 */ + +package nsxt + +import ( + "fmt" + "log" + "reflect" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/vmware/vsphere-automation-sdk-go/runtime/protocol/client" + "github.com/vmware/vsphere-automation-sdk-go/services/nsxt/model" + clientLayer "github.com/vmware/vsphere-automation-sdk-go/services/nsxt/orgs/projects/vpcs" + + utl "github.com/vmware/terraform-provider-nsxt/api/utl" + "github.com/vmware/terraform-provider-nsxt/nsxt/metadata" +) + +var staticRoutesSchema = map[string]*metadata.ExtendedSchema{ + "nsx_id": metadata.GetExtendedSchema(getNsxIDSchema()), + "path": metadata.GetExtendedSchema(getPathSchema()), + "display_name": metadata.GetExtendedSchema(getDisplayNameSchema()), + "description": metadata.GetExtendedSchema(getDescriptionSchema()), + "revision": metadata.GetExtendedSchema(getRevisionSchema()), + "tag": metadata.GetExtendedSchema(getTagsSchema()), + "context": metadata.GetExtendedSchema(getContextSchema(true, false, true)), + "next_hop": { + Schema: schema.Schema{ + Type: schema.TypeList, + Elem: &metadata.ExtendedResource{ + Schema: map[string]*metadata.ExtendedSchema{ + "ip_address": { + Schema: schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validateSingleIP(), + Optional: true, + }, + Metadata: metadata.Metadata{ + SchemaType: "string", + SdkFieldName: "IpAddress", + }, + }, + "admin_distance": { + Schema: schema.Schema{ + Type: schema.TypeInt, + Optional: true, + Default: 1, + }, + Metadata: metadata.Metadata{ + SchemaType: "int", + SdkFieldName: "AdminDistance", + }, + }, + }, + }, + Required: true, + }, + Metadata: metadata.Metadata{ + SchemaType: "list", + SdkFieldName: "NextHops", + ReflectType: reflect.TypeOf(model.RouterNexthop{}), + }, + }, + "network": { + Schema: schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validateCidrOrIPOrRange(), + Required: true, + }, + Metadata: metadata.Metadata{ + SchemaType: "string", + SdkFieldName: "Network", + }, + }, +} + +func resourceNsxtVpcStaticRoutes() *schema.Resource { + return &schema.Resource{ + Create: resourceNsxtVpcStaticRoutesCreate, + Read: resourceNsxtVpcStaticRoutesRead, + Update: resourceNsxtVpcStaticRoutesUpdate, + Delete: resourceNsxtVpcStaticRoutesDelete, + Importer: &schema.ResourceImporter{ + State: nsxtVPCPathResourceImporter, + }, + Schema: metadata.GetSchemaFromExtendedSchema(staticRoutesSchema), + } +} + +func resourceNsxtVpcStaticRoutesExists(sessionContext utl.SessionContext, id string, connector client.Connector) (bool, error) { + var err error + parents := getVpcParentsFromContext(sessionContext) + client := clientLayer.NewStaticRoutesClient(connector) + _, err = client.Get(parents[0], parents[1], parents[2], id) + if err == nil { + return true, nil + } + + if isNotFoundError(err) { + return false, nil + } + + return false, logAPIError("Error retrieving resource", err) +} + +func resourceNsxtVpcStaticRoutesCreate(d *schema.ResourceData, m interface{}) error { + connector := getPolicyConnector(m) + + id, err := getOrGenerateID2(d, m, resourceNsxtVpcStaticRoutesExists) + if err != nil { + return err + } + + parents := getVpcParentsFromContext(getSessionContext(d, m)) + displayName := d.Get("display_name").(string) + description := d.Get("description").(string) + tags := getPolicyTagsFromSchema(d) + + obj := model.StaticRoutes{ + DisplayName: &displayName, + Description: &description, + Tags: tags, + } + + elem := reflect.ValueOf(&obj).Elem() + if err := metadata.SchemaToStruct(elem, d, staticRoutesSchema, "", nil); err != nil { + return err + } + + log.Printf("[INFO] Creating StaticRoutes with ID %s", id) + + client := clientLayer.NewStaticRoutesClient(connector) + err = client.Patch(parents[0], parents[1], parents[2], id, obj) + if err != nil { + return handleCreateError("StaticRoutes", id, err) + } + d.SetId(id) + d.Set("nsx_id", id) + + return resourceNsxtVpcStaticRoutesRead(d, m) +} + +func resourceNsxtVpcStaticRoutesRead(d *schema.ResourceData, m interface{}) error { + connector := getPolicyConnector(m) + + id := d.Id() + if id == "" { + return fmt.Errorf("Error obtaining StaticRoutes ID") + } + + client := clientLayer.NewStaticRoutesClient(connector) + parents := getVpcParentsFromContext(getSessionContext(d, m)) + obj, err := client.Get(parents[0], parents[1], parents[2], id) + if err != nil { + return handleReadError(d, "StaticRoutes", id, err) + } + + setPolicyTagsInSchema(d, obj.Tags) + d.Set("nsx_id", id) + d.Set("display_name", obj.DisplayName) + d.Set("description", obj.Description) + d.Set("revision", obj.Revision) + d.Set("path", obj.Path) + + elem := reflect.ValueOf(&obj).Elem() + return metadata.StructToSchema(elem, d, staticRoutesSchema, "", nil) +} + +func resourceNsxtVpcStaticRoutesUpdate(d *schema.ResourceData, m interface{}) error { + + connector := getPolicyConnector(m) + + id := d.Id() + if id == "" { + return fmt.Errorf("Error obtaining StaticRoutes ID") + } + + parents := getVpcParentsFromContext(getSessionContext(d, m)) + description := d.Get("description").(string) + displayName := d.Get("display_name").(string) + tags := getPolicyTagsFromSchema(d) + + revision := int64(d.Get("revision").(int)) + + obj := model.StaticRoutes{ + DisplayName: &displayName, + Description: &description, + Tags: tags, + Revision: &revision, + } + + elem := reflect.ValueOf(&obj).Elem() + if err := metadata.SchemaToStruct(elem, d, staticRoutesSchema, "", nil); err != nil { + return err + } + client := clientLayer.NewStaticRoutesClient(connector) + _, err := client.Update(parents[0], parents[1], parents[2], id, obj) + if err != nil { + return handleUpdateError("StaticRoutes", id, err) + } + + return resourceNsxtVpcStaticRoutesRead(d, m) +} + +func resourceNsxtVpcStaticRoutesDelete(d *schema.ResourceData, m interface{}) error { + id := d.Id() + if id == "" { + return fmt.Errorf("Error obtaining StaticRoutes ID") + } + + connector := getPolicyConnector(m) + parents := getVpcParentsFromContext(getSessionContext(d, m)) + + client := clientLayer.NewStaticRoutesClient(connector) + err := client.Delete(parents[0], parents[1], parents[2], id) + + if err != nil { + return handleDeleteError("StaticRoutes", id, err) + } + + return nil +} diff --git a/nsxt/resource_nsxt_vpc_static_routes_test.go b/nsxt/resource_nsxt_vpc_static_routes_test.go new file mode 100644 index 000000000..626786d90 --- /dev/null +++ b/nsxt/resource_nsxt_vpc_static_routes_test.go @@ -0,0 +1,204 @@ +/* Copyright © 2024 Broadcom, Inc. All Rights Reserved. + SPDX-License-Identifier: MPL-2.0 */ + +package nsxt + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +var accTestStaticRoutesCreateAttributes = map[string]string{ + "display_name": getAccTestResourceName(), + "description": "terraform created", + "network": "2.2.2.0/24", + "ip_address": "3.1.1.1", + "admin_distance": "2", +} + +var accTestStaticRoutesUpdateAttributes = map[string]string{ + "display_name": getAccTestResourceName(), + "description": "terraform updated", + "network": "3.3.3.0/24", + "ip_address": "4.1.1.1", + "admin_distance": "5", +} + +func TestAccResourceNsxtVpcStaticRoutes_basic(t *testing.T) { + testResourceName := "nsxt_vpc_static_route.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + testAccOnlyVPC(t) + }, + Providers: testAccProviders, + CheckDestroy: func(state *terraform.State) error { + return testAccNsxtVpcStaticRoutesCheckDestroy(state, accTestStaticRoutesUpdateAttributes["display_name"]) + }, + Steps: []resource.TestStep{ + { + Config: testAccNsxtVpcStaticRoutesTemplate(true), + Check: resource.ComposeTestCheckFunc( + testAccNsxtVpcStaticRoutesExists(accTestStaticRoutesCreateAttributes["display_name"], testResourceName), + resource.TestCheckResourceAttr(testResourceName, "display_name", accTestStaticRoutesCreateAttributes["display_name"]), + resource.TestCheckResourceAttr(testResourceName, "description", accTestStaticRoutesCreateAttributes["description"]), + resource.TestCheckResourceAttr(testResourceName, "next_hop.#", "1"), + resource.TestCheckResourceAttr(testResourceName, "network", accTestStaticRoutesCreateAttributes["network"]), + resource.TestCheckResourceAttr(testResourceName, "next_hop.0.ip_address", accTestStaticRoutesCreateAttributes["ip_address"]), + resource.TestCheckResourceAttr(testResourceName, "next_hop.0.admin_distance", accTestStaticRoutesCreateAttributes["admin_distance"]), + + resource.TestCheckResourceAttrSet(testResourceName, "nsx_id"), + resource.TestCheckResourceAttrSet(testResourceName, "path"), + resource.TestCheckResourceAttrSet(testResourceName, "revision"), + resource.TestCheckResourceAttr(testResourceName, "tag.#", "1"), + ), + }, + { + Config: testAccNsxtVpcStaticRoutesTemplate(false), + Check: resource.ComposeTestCheckFunc( + testAccNsxtVpcStaticRoutesExists(accTestStaticRoutesUpdateAttributes["display_name"], testResourceName), + resource.TestCheckResourceAttr(testResourceName, "display_name", accTestStaticRoutesUpdateAttributes["display_name"]), + resource.TestCheckResourceAttr(testResourceName, "description", accTestStaticRoutesUpdateAttributes["description"]), + resource.TestCheckResourceAttr(testResourceName, "next_hop.#", "1"), + resource.TestCheckResourceAttr(testResourceName, "network", accTestStaticRoutesUpdateAttributes["network"]), + resource.TestCheckResourceAttr(testResourceName, "next_hop.0.ip_address", accTestStaticRoutesUpdateAttributes["ip_address"]), + resource.TestCheckResourceAttr(testResourceName, "next_hop.0.admin_distance", accTestStaticRoutesUpdateAttributes["admin_distance"]), + + resource.TestCheckResourceAttrSet(testResourceName, "nsx_id"), + resource.TestCheckResourceAttrSet(testResourceName, "path"), + resource.TestCheckResourceAttrSet(testResourceName, "revision"), + resource.TestCheckResourceAttr(testResourceName, "tag.#", "1"), + ), + }, + { + Config: testAccNsxtVpcStaticRoutesMinimalistic(), + Check: resource.ComposeTestCheckFunc( + testAccNsxtVpcStaticRoutesExists(accTestStaticRoutesCreateAttributes["display_name"], testResourceName), + resource.TestCheckResourceAttr(testResourceName, "description", ""), + resource.TestCheckResourceAttrSet(testResourceName, "nsx_id"), + resource.TestCheckResourceAttrSet(testResourceName, "path"), + resource.TestCheckResourceAttrSet(testResourceName, "revision"), + resource.TestCheckResourceAttr(testResourceName, "tag.#", "0"), + ), + }, + }, + }) +} + +func TestAccResourceNsxtVpcStaticRoutes_importBasic(t *testing.T) { + name := getAccTestResourceName() + testResourceName := "nsxt_vpc_static_route.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + testAccOnlyVPC(t) + }, + Providers: testAccProviders, + CheckDestroy: func(state *terraform.State) error { + return testAccNsxtVpcStaticRoutesCheckDestroy(state, name) + }, + Steps: []resource.TestStep{ + { + Config: testAccNsxtVpcStaticRoutesMinimalistic(), + }, + { + ResourceName: testResourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateIdFunc: testAccResourceNsxtPolicyImportIDRetriever(testResourceName), + }, + }, + }) +} + +func testAccNsxtVpcStaticRoutesExists(displayName string, resourceName string) resource.TestCheckFunc { + return func(state *terraform.State) error { + + connector := getPolicyConnector(testAccProvider.Meta().(nsxtClients)) + + rs, ok := state.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("Policy StaticRoutes resource %s not found in resources", resourceName) + } + + resourceID := rs.Primary.ID + if resourceID == "" { + return fmt.Errorf("Policy StaticRoutes resource ID not set in resources") + } + + exists, err := resourceNsxtVpcStaticRoutesExists(testAccGetSessionContext(), resourceID, connector) + if err != nil { + return err + } + if !exists { + return fmt.Errorf("Policy StaticRoutes %s does not exist", resourceID) + } + + return nil + } +} + +func testAccNsxtVpcStaticRoutesCheckDestroy(state *terraform.State, displayName string) error { + connector := getPolicyConnector(testAccProvider.Meta().(nsxtClients)) + for _, rs := range state.RootModule().Resources { + + if rs.Type != "nsxt_vpc_static_route" { + continue + } + + resourceID := rs.Primary.Attributes["id"] + exists, err := resourceNsxtVpcStaticRoutesExists(testAccGetSessionContext(), resourceID, connector) + if err == nil { + return err + } + + if exists { + return fmt.Errorf("Policy StaticRoutes %s still exists", displayName) + } + } + return nil +} + +func testAccNsxtVpcStaticRoutesTemplate(createFlow bool) string { + var attrMap map[string]string + if createFlow { + attrMap = accTestStaticRoutesCreateAttributes + } else { + attrMap = accTestStaticRoutesUpdateAttributes + } + return fmt.Sprintf(` +resource "nsxt_vpc_static_route" "test" { +%s + display_name = "%s" + description = "%s" + + network = "%s" + + next_hop { + ip_address = "%s" + admin_distance = %s + } + + tag { + scope = "scope1" + tag = "tag1" + } +}`, testAccNsxtPolicyMultitenancyContext(), attrMap["display_name"], attrMap["description"], attrMap["network"], attrMap["ip_address"], attrMap["admin_distance"]) +} + +func testAccNsxtVpcStaticRoutesMinimalistic() string { + return fmt.Sprintf(` +resource "nsxt_vpc_static_route" "test" { +%s + display_name = "%s" + network = "%s" + next_hop { + ip_address = "%s" + } +}`, testAccNsxtPolicyMultitenancyContext(), accTestStaticRoutesUpdateAttributes["display_name"], accTestStaticRoutesUpdateAttributes["network"], accTestStaticRoutesUpdateAttributes["ip_address"]) +} diff --git a/website/docs/r/vpc_static_routes.html.markdown b/website/docs/r/vpc_static_routes.html.markdown new file mode 100644 index 000000000..50ab6284b --- /dev/null +++ b/website/docs/r/vpc_static_routes.html.markdown @@ -0,0 +1,61 @@ +--- +subcategory: "VPC" +layout: "nsxt" +page_title: "NSXT: nsxt_policy_static_routes" +description: A resource to configure a StaticRoutes. +--- + +# nsxt_policy_static_route + +This resource provides a method for the management of VPC Static Routes. + +This resource is applicable to NSX Policy Manager. + +## Example Usage + +```hcl +resource "nsxt_vpc_static_route" "test" { + display_name = "test" + description = "Terraform provisioned StaticRoutes" + + network = "3.3.3.0/24" + + next_hop { + ip_address = "10.230.3.1" + admin_distance = 4 + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `display_name` - (Required) Display name of the resource. +* `description` - (Optional) Description of the resource. +* `tag` - (Optional) A list of scope + tag pairs to associate with this resource. +* `nsx_id` - (Optional) The NSX ID of this resource. If set, this ID will be used to create the resource. +* `network` - (Required) Specify network address in CIDR format. Optionally this can be allocated IP from one of the external blocks associated with VPC. Only /32 CIDR is allowed in case IP overlaps with external blocks. +* `next_hop` - (Required) Specify next hop routes for network. + * `ip_address` - (Optional) Next hop gateway IP address + * `admin_distance` - (Optional) Cost associated with next hop route. Default is 1. + +## Attributes Reference + +In addition to arguments listed above, the following attributes are exported: + +* `id` - ID of the resource. +* `revision` - Indicates current revision number of the object as seen by NSX-T API server. This attribute can be useful for debugging. +* `path` - The NSX path of the policy resource. + +## Importing + +An existing object can be [imported][docs-import] into this resource, via the following command: + +[docs-import]: https://www.terraform.io/cli/import + +``` +terraform import nsxt_vpc_static_route.test +``` + +The above command imports Static Route named `test` with the NSX policy VPC path `PATH`.