This is an automated email from the ASF dual-hosted git repository.

sureshanaparti pushed a commit to branch main
in repository 
https://gitbox.apache.org/repos/asf/cloudstack-terraform-provider.git


The following commit(s) were added to refs/heads/main by this push:
     new cd41438  Add nexthop parameter to cloudstack_static_route resource 
(#284)
cd41438 is described below

commit cd41438a76d8d9e4ec8c1c9fa448a7b21c56e54f
Author: Brad House - Nexthop <[email protected]>
AuthorDate: Fri Aug 14 04:30:33 2026 -0400

    Add nexthop parameter to cloudstack_static_route resource (#284)
    
    * Add nexthop parameter to cloudstack_static_route resource
    
    This change adds support for the nexthop parameter in static routes,
    which allows VPC-based routing without requiring a private gateway.
    
    Resource Changes:
    - Add nexthop and vpc_id optional parameters to schema
    - nexthop is mutually exclusive with gateway_id
    - When using nexthop, vpc_id is required
    - When using gateway_id, nexthop and vpc_id are not allowed
    - Update resource creation logic to support both routing methods:
      - Private Gateway routing (gateway_id)
      - VPC direct routing (nexthop + vpc_id)
    - Update Read function to maintain plan stability for both modes
    
    Test Coverage:
    - Add comprehensive test coverage for both routing methods
    - Add testAccPreCheckStaticRouteNexthop helper to check CloudStack version
    - Automatically skip nexthop tests on CloudStack versions < 4.22.0
    - Version check uses CloudStack capabilities API
    - Tests are skipped (not failed) on older versions for backward 
compatibility
    - All acceptance tests passing (2/2)
    
    Documentation:
    - Update documentation with examples for both routing methods
    - Document mutual exclusivity requirements
    - Note CloudStack 4.22+ requirement for nexthop support
    
    CI/CD:
    - Add CloudStack 4.22.0.0 to acceptance test matrix
    - Ensures tests run on both old and new CloudStack versions
    
    * Extract version parsing into reusable helper functions
    
    - Add parseCloudStackVersion() to parse version strings into numeric values
    - Add getCloudStackVersion() to retrieve version from API
    - Add requireMinimumCloudStackVersion() for easy version checking in tests
    - Refactor testAccPreCheckStaticRouteNexthop() to use new helpers
    - These helpers can now be reused by other tests that need version checks
    
    * Add CloudStack version requirement to nexthop documentation
    
    Document that the nexthop parameter requires CloudStack 4.22.0+
    
    * Add vpc_id assertion to nexthop acceptance test
    
    Add TestCheckResourceAttrPair to verify that vpc_id is properly persisted
    in state and matches the created VPC resource
    
    * Add parameter verification to static route Create function
    
    Add verifyStaticRouteParams() to validate that either gateway_id or
    (nexthop + vpc_id) is provided before calling the API. This provides
    clearer error messages at the Terraform level rather than letting
    invalid configurations fail at the CloudStack API level.
    
    * Fix testAccPreCheckStaticRouteNexthop to create CloudStack client
    
    The provider Meta() is not available during PreCheck, so we need to
    create a CloudStack client directly using the environment variables.
    
    * Add validation test cases for static route parameter verification
    
    Add test cases to verify that the parameter validation logic works 
correctly:
    - Test missing both gateway_id and nexthop
    - Test nexthop without vpc_id
    - Test vpc_id without nexthop
    
    These tests verify that the schema-level RequiredWith validation and
    the custom verifyStaticRouteParams function work as expected.
    
    * Deduplicate getCloudStackVersion after merge from main
    
    The merge from main introduced newTestClient and getCloudStackVersion(t)
    which conflicted with our branch's getCloudStackVersion(cs). Refactor to
    use main's versions: remove our duplicate, simplify
    requireMinimumCloudStackVersion to no longer require a client parameter,
    and simplify testAccPreCheckStaticRouteNexthop to drop inline client
    creation.
---
 cloudstack/provider_test.go                        |  59 ++++++++++++
 cloudstack/resource_cloudstack_static_route.go     |  79 ++++++++++++++-
 .../resource_cloudstack_static_route_test.go       | 106 +++++++++++++++++++++
 website/docs/r/static_route.html.markdown          |  25 ++++-
 4 files changed, 262 insertions(+), 7 deletions(-)

diff --git a/cloudstack/provider_test.go b/cloudstack/provider_test.go
index 1ac8198..ca47cbf 100644
--- a/cloudstack/provider_test.go
+++ b/cloudstack/provider_test.go
@@ -23,6 +23,8 @@ import (
        "context"
        "os"
        "regexp"
+       "strconv"
+       "strings"
        "testing"
 
        "github.com/apache/cloudstack-go/v2/cloudstack"
@@ -147,6 +149,63 @@ func testAccPreCheck(t *testing.T) {
        }
 }
 
+// parseCloudStackVersion parses a CloudStack version string (e.g., "4.22.0.0")
+// and returns a numeric value for comparison (e.g., 4.22 -> 4022).
+// The numeric value is calculated as: major * 1000 + minor.
+// Returns 0 if the version string cannot be parsed.
+func parseCloudStackVersion(version string) int {
+       parts := strings.Split(version, ".")
+       if len(parts) < 2 {
+               return 0
+       }
+
+       major := 0
+       minor := 0
+
+       // Parse major version - extract first numeric part
+       majorStr := regexp.MustCompile(`^\d+`).FindString(parts[0])
+       if majorStr != "" {
+               major, _ = strconv.Atoi(majorStr)
+       }
+
+       // Parse minor version - extract first numeric part
+       minorStr := regexp.MustCompile(`^\d+`).FindString(parts[1])
+       if minorStr != "" {
+               minor, _ = strconv.Atoi(minorStr)
+       }
+
+       return major*1000 + minor
+}
+
+// requireMinimumCloudStackVersion checks if the CloudStack version meets the 
minimum requirement.
+// If the version is below the minimum, it skips the test with an appropriate 
message.
+// The minVersion parameter should be in the format returned by 
parseCloudStackVersion (e.g., 4022 for 4.22.0).
+func requireMinimumCloudStackVersion(t *testing.T, minVersion int, featureName 
string) {
+       t.Helper()
+       version := getCloudStackVersion(t)
+       if version == "" {
+               t.Skipf("Unable to determine CloudStack version, skipping %s 
test", featureName)
+               return
+       }
+
+       versionNum := parseCloudStackVersion(version)
+       if versionNum < minVersion {
+               // Convert minVersion back to readable format (e.g., 4022 -> 
"4.22")
+               major := minVersion / 1000
+               minor := minVersion % 1000
+               t.Skipf("%s not supported in CloudStack version %s (requires 
%d.%d+)", featureName, version, major, minor)
+       }
+}
+
+// testAccPreCheckStaticRouteNexthop checks if the CloudStack version supports
+// the nexthop parameter for static routes (requires 4.22.0+)
+func testAccPreCheckStaticRouteNexthop(t *testing.T) {
+       testAccPreCheck(t)
+
+       const minVersionNum = 4022 // 4.22.0
+       requireMinimumCloudStackVersion(t, minVersionNum, "Static route nexthop 
parameter")
+}
+
 // newTestClient creates a CloudStack client from environment variables for 
use in test PreCheck functions.
 // This is needed because PreCheck functions run before the test framework 
configures the provider,
 // so testAccProvider.Meta() is nil at that point.
diff --git a/cloudstack/resource_cloudstack_static_route.go 
b/cloudstack/resource_cloudstack_static_route.go
index d9240b7..19dd8d9 100644
--- a/cloudstack/resource_cloudstack_static_route.go
+++ b/cloudstack/resource_cloudstack_static_route.go
@@ -42,9 +42,26 @@ func resourceCloudStackStaticRoute() *schema.Resource {
                        },
 
                        "gateway_id": {
-                               Type:     schema.TypeString,
-                               Required: true,
-                               ForceNew: true,
+                               Type:          schema.TypeString,
+                               Optional:      true,
+                               ForceNew:      true,
+                               ConflictsWith: []string{"nexthop", "vpc_id"},
+                       },
+
+                       "nexthop": {
+                               Type:          schema.TypeString,
+                               Optional:      true,
+                               ForceNew:      true,
+                               ConflictsWith: []string{"gateway_id"},
+                               RequiredWith:  []string{"vpc_id"},
+                       },
+
+                       "vpc_id": {
+                               Type:          schema.TypeString,
+                               Optional:      true,
+                               ForceNew:      true,
+                               ConflictsWith: []string{"gateway_id"},
+                               RequiredWith:  []string{"nexthop"},
                        },
                },
        }
@@ -53,16 +70,30 @@ func resourceCloudStackStaticRoute() *schema.Resource {
 func resourceCloudStackStaticRouteCreate(d *schema.ResourceData, meta 
interface{}) error {
        cs := meta.(*cloudstack.CloudStackClient)
 
+       // Verify that required parameters are set
+       if err := verifyStaticRouteParams(d); err != nil {
+               return err
+       }
+
        // Create a new parameter struct
        p := cs.VPC.NewCreateStaticRouteParams(
                d.Get("cidr").(string),
        )
 
+       // Set either gateway_id or nexthop+vpc_id (they are mutually exclusive)
        if v, ok := d.GetOk("gateway_id"); ok {
                p.SetGatewayid(v.(string))
        }
 
-       // Create the new private gateway
+       if v, ok := d.GetOk("nexthop"); ok {
+               p.SetNexthop(v.(string))
+       }
+
+       if v, ok := d.GetOk("vpc_id"); ok {
+               p.SetVpcid(v.(string))
+       }
+
+       // Create the new static route
        r, err := cs.VPC.CreateStaticRoute(p)
        if err != nil {
                return fmt.Errorf("Error creating static route for %s: %s", 
d.Get("cidr").(string), err)
@@ -76,7 +107,7 @@ func resourceCloudStackStaticRouteCreate(d 
*schema.ResourceData, meta interface{
 func resourceCloudStackStaticRouteRead(d *schema.ResourceData, meta 
interface{}) error {
        cs := meta.(*cloudstack.CloudStackClient)
 
-       // Get the virtual machine details
+       // Get the static route details
        r, count, err := cs.VPC.GetStaticRouteByID(d.Id())
        if err != nil {
                if count == 0 {
@@ -90,6 +121,19 @@ func resourceCloudStackStaticRouteRead(d 
*schema.ResourceData, meta interface{})
 
        d.Set("cidr", r.Cidr)
 
+       // Set gateway_id if it's not empty (indicates this route uses a 
gateway)
+       if r.Vpcgatewayid != "" {
+               d.Set("gateway_id", r.Vpcgatewayid)
+       }
+
+       // Set nexthop and vpc_id if nexthop is not empty (indicates this route 
uses nexthop)
+       if r.Nexthop != "" {
+               d.Set("nexthop", r.Nexthop)
+               if r.Vpcid != "" {
+                       d.Set("vpc_id", r.Vpcid)
+               }
+       }
+
        return nil
 }
 
@@ -114,3 +158,28 @@ func resourceCloudStackStaticRouteDelete(d 
*schema.ResourceData, meta interface{
 
        return nil
 }
+
+func verifyStaticRouteParams(d *schema.ResourceData) error {
+       _, hasGatewayID := d.GetOk("gateway_id")
+       _, hasNexthop := d.GetOk("nexthop")
+       _, hasVpcID := d.GetOk("vpc_id")
+
+       // Check that either gateway_id or (nexthop + vpc_id) is provided
+       if !hasGatewayID && !hasNexthop {
+               return fmt.Errorf(
+                       "You must supply either 'gateway_id' or 'nexthop' (with 
'vpc_id')")
+       }
+
+       // Check that nexthop and vpc_id are used together
+       if hasNexthop && !hasVpcID {
+               return fmt.Errorf(
+                       "You must supply 'vpc_id' when using 'nexthop'")
+       }
+
+       if hasVpcID && !hasNexthop {
+               return fmt.Errorf(
+                       "You must supply 'nexthop' when using 'vpc_id'")
+       }
+
+       return nil
+}
diff --git a/cloudstack/resource_cloudstack_static_route_test.go 
b/cloudstack/resource_cloudstack_static_route_test.go
index dcf754d..5c97de9 100644
--- a/cloudstack/resource_cloudstack_static_route_test.go
+++ b/cloudstack/resource_cloudstack_static_route_test.go
@@ -21,6 +21,7 @@ package cloudstack
 
 import (
        "fmt"
+       "regexp"
        "testing"
 
        "github.com/apache/cloudstack-go/v2/cloudstack"
@@ -42,6 +43,35 @@ func TestAccCloudStackStaticRoute_basic(t *testing.T) {
                                        testAccCheckCloudStackStaticRouteExists(
                                                "cloudstack_static_route.foo", 
&staticroute),
                                        
testAccCheckCloudStackStaticRouteAttributes(&staticroute),
+                                       resource.TestCheckResourceAttr(
+                                               "cloudstack_static_route.foo", 
"cidr", "172.16.0.0/16"),
+                               ),
+                       },
+               },
+       })
+}
+
+func TestAccCloudStackStaticRoute_nexthop(t *testing.T) {
+       var staticroute cloudstack.StaticRoute
+
+       resource.Test(t, resource.TestCase{
+               PreCheck:     func() { testAccPreCheckStaticRouteNexthop(t) },
+               Providers:    testAccProviders,
+               CheckDestroy: testAccCheckCloudStackStaticRouteDestroy,
+               Steps: []resource.TestStep{
+                       {
+                               Config: testAccCloudStackStaticRoute_nexthop,
+                               Check: resource.ComposeTestCheckFunc(
+                                       testAccCheckCloudStackStaticRouteExists(
+                                               "cloudstack_static_route.bar", 
&staticroute),
+                                       
testAccCheckCloudStackStaticRouteNexthopAttributes(&staticroute),
+                                       resource.TestCheckResourceAttr(
+                                               "cloudstack_static_route.bar", 
"cidr", "192.168.0.0/16"),
+                                       resource.TestCheckResourceAttr(
+                                               "cloudstack_static_route.bar", 
"nexthop", "10.1.1.1"),
+                                       resource.TestCheckResourceAttrPair(
+                                               "cloudstack_static_route.bar", 
"vpc_id",
+                                               "cloudstack_vpc.bar", "id"),
                                ),
                        },
                },
@@ -89,6 +119,22 @@ func testAccCheckCloudStackStaticRouteAttributes(
        }
 }
 
+func testAccCheckCloudStackStaticRouteNexthopAttributes(
+       staticroute *cloudstack.StaticRoute) resource.TestCheckFunc {
+       return func(s *terraform.State) error {
+
+               if staticroute.Cidr != "192.168.0.0/16" {
+                       return fmt.Errorf("Bad CIDR: %s", staticroute.Cidr)
+               }
+
+               if staticroute.Nexthop != "10.1.1.1" {
+                       return fmt.Errorf("Bad nexthop: %s", 
staticroute.Nexthop)
+               }
+
+               return nil
+       }
+}
+
 func testAccCheckCloudStackStaticRouteDestroy(s *terraform.State) error {
        cs := testAccProvider.Meta().(*cloudstack.CloudStackClient)
 
@@ -136,3 +182,63 @@ resource "cloudstack_static_route" "foo" {
   cidr = "172.16.0.0/16"
   gateway_id = cloudstack_private_gateway.foo.id
 }`
+
+const testAccCloudStackStaticRoute_nexthop = `
+resource "cloudstack_vpc" "bar" {
+  name = "terraform-vpc-nexthop"
+  cidr = "10.0.0.0/8"
+  vpc_offering = "Default VPC offering"
+  zone = "Sandbox-simulator"
+}
+
+resource "cloudstack_static_route" "bar" {
+  cidr = "192.168.0.0/16"
+  nexthop = "10.1.1.1"
+  vpc_id = cloudstack_vpc.bar.id
+}`
+
+// Test validation errors
+func TestAccCloudStackStaticRoute_validation(t *testing.T) {
+       resource.Test(t, resource.TestCase{
+               PreCheck:  func() { testAccPreCheck(t) },
+               Providers: testAccProviders,
+               Steps: []resource.TestStep{
+                       {
+                               Config:      
testAccCloudStackStaticRoute_noParameters,
+                               ExpectError: regexp.MustCompile(`You must 
supply either 'gateway_id' or 'nexthop'`),
+                       },
+                       {
+                               Config:      
testAccCloudStackStaticRoute_nexthopWithoutVpc,
+                               ExpectError: regexp.MustCompile(`all of 
.nexthop,vpc_id. must be specified`),
+                       },
+                       {
+                               Config:      
testAccCloudStackStaticRoute_vpcWithoutNexthop,
+                               ExpectError: regexp.MustCompile(`all of 
.nexthop,vpc_id. must be specified`),
+                       },
+               },
+       })
+}
+
+const testAccCloudStackStaticRoute_noParameters = `
+resource "cloudstack_static_route" "invalid" {
+  cidr = "192.168.0.0/16"
+}`
+
+const testAccCloudStackStaticRoute_nexthopWithoutVpc = `
+resource "cloudstack_static_route" "invalid" {
+  cidr = "192.168.0.0/16"
+  nexthop = "10.1.1.1"
+}`
+
+const testAccCloudStackStaticRoute_vpcWithoutNexthop = `
+resource "cloudstack_vpc" "test" {
+  name = "terraform-vpc-test"
+  cidr = "10.0.0.0/8"
+  vpc_offering = "Default VPC offering"
+  zone = "Sandbox-simulator"
+}
+
+resource "cloudstack_static_route" "invalid" {
+  cidr = "192.168.0.0/16"
+  vpc_id = cloudstack_vpc.test.id
+}`
diff --git a/website/docs/r/static_route.html.markdown 
b/website/docs/r/static_route.html.markdown
index dab12a9..b7ea24a 100644
--- a/website/docs/r/static_route.html.markdown
+++ b/website/docs/r/static_route.html.markdown
@@ -12,6 +12,8 @@ Creates a static route for the given private gateway or VPC.
 
 ## Example Usage
 
+Using a private gateway:
+
 ```hcl
 resource "cloudstack_static_route" "default" {
   cidr       = "10.0.0.0/16"
@@ -19,6 +21,16 @@ resource "cloudstack_static_route" "default" {
 }
 ```
 
+Using a nexthop IP address:
+
+```hcl
+resource "cloudstack_static_route" "with_nexthop" {
+  cidr    = "10.0.0.0/16"
+  nexthop = "192.168.1.1"
+  vpc_id  = "76f607e3-e8dc-4971-8831-b2a2b0cc4cb4"
+}
+```
+
 ## Argument Reference
 
 The following arguments are supported:
@@ -26,8 +38,17 @@ The following arguments are supported:
 * `cidr` - (Required) The CIDR for the static route. Changing this forces
     a new resource to be created.
 
-* `gateway_id` - (Required) The ID of the Private gateway. Changing this forces
-    a new resource to be created.
+* `gateway_id` - (Optional) The ID of the Private gateway. Changing this forces
+    a new resource to be created. Conflicts with `nexthop` and `vpc_id`.
+
+* `nexthop` - (Optional) The IP address of the nexthop for the static route.
+    Changing this forces a new resource to be created. Conflicts with 
`gateway_id`.
+    Must be used together with `vpc_id`. **Requires CloudStack 4.22.0+**.
+
+* `vpc_id` - (Optional) The ID of the VPC. Required when using `nexthop`.
+    Changing this forces a new resource to be created. Conflicts with 
`gateway_id`.
+
+**Note:** Either `gateway_id` or (`nexthop` + `vpc_id`) must be specified.
 
 ## Attributes Reference
 

Reply via email to