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