Skip to content

Commit 660a29e

Browse files
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
1 parent 16915b6 commit 660a29e

5 files changed

Lines changed: 174 additions & 8 deletions

File tree

.github/workflows/acceptance.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ permissions:
3030

3131
env:
3232
CLOUDSTACK_API_URL: http://localhost:8080/client/api
33-
CLOUDSTACK_VERSIONS: "['4.19.0.1', '4.19.1.3', '4.19.2.0', '4.19.3.0', '4.20.1.0']"
33+
CLOUDSTACK_VERSIONS: "['4.19.0.1', '4.19.1.3', '4.19.2.0', '4.19.3.0', '4.20.1.0', '4.22.0.0']"
3434

3535
jobs:
3636
prepare-matrix:

cloudstack/provider_test.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,11 @@ import (
2323
"context"
2424
"os"
2525
"regexp"
26+
"strconv"
27+
"strings"
2628
"testing"
2729

30+
"github.com/apache/cloudstack-go/v2/cloudstack"
2831
"github.com/hashicorp/terraform-plugin-framework/providerserver"
2932
"github.com/hashicorp/terraform-plugin-go/tfprotov6"
3033
"github.com/hashicorp/terraform-plugin-mux/tf5to6server"
@@ -145,3 +148,50 @@ func testAccPreCheck(t *testing.T) {
145148
t.Fatal("CLOUDSTACK_SECRET_KEY must be set for acceptance tests")
146149
}
147150
}
151+
152+
// testAccPreCheckStaticRouteNexthop checks if the CloudStack version supports
153+
// the nexthop parameter for static routes (requires 4.22.0+)
154+
func testAccPreCheckStaticRouteNexthop(t *testing.T) {
155+
testAccPreCheck(t)
156+
cs := testAccProvider.Meta().(*cloudstack.CloudStackClient)
157+
158+
// Check the API capabilities to get CloudStack version
159+
p := cs.Configuration.NewListCapabilitiesParams()
160+
caps, err := cs.Configuration.ListCapabilities(p)
161+
if err != nil {
162+
t.Skipf("Unable to check CloudStack capabilities: %v", err)
163+
return
164+
}
165+
166+
// Check CloudStack version - nexthop support was added in 4.22.0
167+
if caps != nil && caps.Capabilities != nil && caps.Capabilities.Cloudstackversion != "" {
168+
version := caps.Capabilities.Cloudstackversion
169+
170+
// Parse version string (e.g., "4.22.0.0" -> major=4, minor=22)
171+
// Convert to numeric value: major * 1000 + minor (e.g., 4.22 -> 4022)
172+
parts := strings.Split(version, ".")
173+
if len(parts) >= 2 {
174+
major := 0
175+
minor := 0
176+
177+
// Parse major version - extract first numeric part
178+
majorStr := regexp.MustCompile(`^\d+`).FindString(parts[0])
179+
if majorStr != "" {
180+
major, _ = strconv.Atoi(majorStr)
181+
}
182+
183+
// Parse minor version - extract first numeric part
184+
minorStr := regexp.MustCompile(`^\d+`).FindString(parts[1])
185+
if minorStr != "" {
186+
minor, _ = strconv.Atoi(minorStr)
187+
}
188+
189+
versionNum := major*1000 + minor
190+
const minVersionNum = 4022 // 4.22.0
191+
192+
if versionNum < minVersionNum {
193+
t.Skipf("Static route nexthop parameter not supported in CloudStack version %s (requires 4.22.0+)", version)
194+
}
195+
}
196+
}
197+
}

cloudstack/resource_cloudstack_static_route.go

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,26 @@ func resourceCloudStackStaticRoute() *schema.Resource {
4242
},
4343

4444
"gateway_id": {
45-
Type: schema.TypeString,
46-
Required: true,
47-
ForceNew: true,
45+
Type: schema.TypeString,
46+
Optional: true,
47+
ForceNew: true,
48+
ConflictsWith: []string{"nexthop", "vpc_id"},
49+
},
50+
51+
"nexthop": {
52+
Type: schema.TypeString,
53+
Optional: true,
54+
ForceNew: true,
55+
ConflictsWith: []string{"gateway_id"},
56+
RequiredWith: []string{"vpc_id"},
57+
},
58+
59+
"vpc_id": {
60+
Type: schema.TypeString,
61+
Optional: true,
62+
ForceNew: true,
63+
ConflictsWith: []string{"gateway_id"},
64+
RequiredWith: []string{"nexthop"},
4865
},
4966
},
5067
}
@@ -58,11 +75,20 @@ func resourceCloudStackStaticRouteCreate(d *schema.ResourceData, meta interface{
5875
d.Get("cidr").(string),
5976
)
6077

78+
// Set either gateway_id or nexthop+vpc_id (they are mutually exclusive)
6179
if v, ok := d.GetOk("gateway_id"); ok {
6280
p.SetGatewayid(v.(string))
6381
}
6482

65-
// Create the new private gateway
83+
if v, ok := d.GetOk("nexthop"); ok {
84+
p.SetNexthop(v.(string))
85+
}
86+
87+
if v, ok := d.GetOk("vpc_id"); ok {
88+
p.SetVpcid(v.(string))
89+
}
90+
91+
// Create the new static route
6692
r, err := cs.VPC.CreateStaticRoute(p)
6793
if err != nil {
6894
return fmt.Errorf("Error creating static route for %s: %s", d.Get("cidr").(string), err)
@@ -76,7 +102,7 @@ func resourceCloudStackStaticRouteCreate(d *schema.ResourceData, meta interface{
76102
func resourceCloudStackStaticRouteRead(d *schema.ResourceData, meta interface{}) error {
77103
cs := meta.(*cloudstack.CloudStackClient)
78104

79-
// Get the virtual machine details
105+
// Get the static route details
80106
r, count, err := cs.VPC.GetStaticRouteByID(d.Id())
81107
if err != nil {
82108
if count == 0 {
@@ -90,6 +116,19 @@ func resourceCloudStackStaticRouteRead(d *schema.ResourceData, meta interface{})
90116

91117
d.Set("cidr", r.Cidr)
92118

119+
// Set gateway_id if it's not empty (indicates this route uses a gateway)
120+
if r.Vpcgatewayid != "" {
121+
d.Set("gateway_id", r.Vpcgatewayid)
122+
}
123+
124+
// Set nexthop and vpc_id if nexthop is not empty (indicates this route uses nexthop)
125+
if r.Nexthop != "" {
126+
d.Set("nexthop", r.Nexthop)
127+
if r.Vpcid != "" {
128+
d.Set("vpc_id", r.Vpcid)
129+
}
130+
}
131+
93132
return nil
94133
}
95134

cloudstack/resource_cloudstack_static_route_test.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,32 @@ func TestAccCloudStackStaticRoute_basic(t *testing.T) {
4242
testAccCheckCloudStackStaticRouteExists(
4343
"cloudstack_static_route.foo", &staticroute),
4444
testAccCheckCloudStackStaticRouteAttributes(&staticroute),
45+
resource.TestCheckResourceAttr(
46+
"cloudstack_static_route.foo", "cidr", "172.16.0.0/16"),
47+
),
48+
},
49+
},
50+
})
51+
}
52+
53+
func TestAccCloudStackStaticRoute_nexthop(t *testing.T) {
54+
var staticroute cloudstack.StaticRoute
55+
56+
resource.Test(t, resource.TestCase{
57+
PreCheck: func() { testAccPreCheckStaticRouteNexthop(t) },
58+
Providers: testAccProviders,
59+
CheckDestroy: testAccCheckCloudStackStaticRouteDestroy,
60+
Steps: []resource.TestStep{
61+
{
62+
Config: testAccCloudStackStaticRoute_nexthop,
63+
Check: resource.ComposeTestCheckFunc(
64+
testAccCheckCloudStackStaticRouteExists(
65+
"cloudstack_static_route.bar", &staticroute),
66+
testAccCheckCloudStackStaticRouteNexthopAttributes(&staticroute),
67+
resource.TestCheckResourceAttr(
68+
"cloudstack_static_route.bar", "cidr", "192.168.0.0/16"),
69+
resource.TestCheckResourceAttr(
70+
"cloudstack_static_route.bar", "nexthop", "10.1.1.1"),
4571
),
4672
},
4773
},
@@ -89,6 +115,22 @@ func testAccCheckCloudStackStaticRouteAttributes(
89115
}
90116
}
91117

118+
func testAccCheckCloudStackStaticRouteNexthopAttributes(
119+
staticroute *cloudstack.StaticRoute) resource.TestCheckFunc {
120+
return func(s *terraform.State) error {
121+
122+
if staticroute.Cidr != "192.168.0.0/16" {
123+
return fmt.Errorf("Bad CIDR: %s", staticroute.Cidr)
124+
}
125+
126+
if staticroute.Nexthop != "10.1.1.1" {
127+
return fmt.Errorf("Bad nexthop: %s", staticroute.Nexthop)
128+
}
129+
130+
return nil
131+
}
132+
}
133+
92134
func testAccCheckCloudStackStaticRouteDestroy(s *terraform.State) error {
93135
cs := testAccProvider.Meta().(*cloudstack.CloudStackClient)
94136

@@ -136,3 +178,17 @@ resource "cloudstack_static_route" "foo" {
136178
cidr = "172.16.0.0/16"
137179
gateway_id = cloudstack_private_gateway.foo.id
138180
}`
181+
182+
const testAccCloudStackStaticRoute_nexthop = `
183+
resource "cloudstack_vpc" "bar" {
184+
name = "terraform-vpc-nexthop"
185+
cidr = "10.0.0.0/8"
186+
vpc_offering = "Default VPC offering"
187+
zone = "Sandbox-simulator"
188+
}
189+
190+
resource "cloudstack_static_route" "bar" {
191+
cidr = "192.168.0.0/16"
192+
nexthop = "10.1.1.1"
193+
vpc_id = cloudstack_vpc.bar.id
194+
}`

website/docs/r/static_route.html.markdown

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,22 +12,43 @@ Creates a static route for the given private gateway or VPC.
1212

1313
## Example Usage
1414

15+
Using a private gateway:
16+
1517
```hcl
1618
resource "cloudstack_static_route" "default" {
1719
cidr = "10.0.0.0/16"
1820
gateway_id = "76f607e3-e8dc-4971-8831-b2a2b0cc4cb4"
1921
}
2022
```
2123

24+
Using a nexthop IP address:
25+
26+
```hcl
27+
resource "cloudstack_static_route" "with_nexthop" {
28+
cidr = "10.0.0.0/16"
29+
nexthop = "192.168.1.1"
30+
vpc_id = "76f607e3-e8dc-4971-8831-b2a2b0cc4cb4"
31+
}
32+
```
33+
2234
## Argument Reference
2335

2436
The following arguments are supported:
2537

2638
* `cidr` - (Required) The CIDR for the static route. Changing this forces
2739
a new resource to be created.
2840

29-
* `gateway_id` - (Required) The ID of the Private gateway. Changing this forces
30-
a new resource to be created.
41+
* `gateway_id` - (Optional) The ID of the Private gateway. Changing this forces
42+
a new resource to be created. Conflicts with `nexthop` and `vpc_id`.
43+
44+
* `nexthop` - (Optional) The IP address of the nexthop for the static route.
45+
Changing this forces a new resource to be created. Conflicts with `gateway_id`.
46+
Must be used together with `vpc_id`.
47+
48+
* `vpc_id` - (Optional) The ID of the VPC. Required when using `nexthop`.
49+
Changing this forces a new resource to be created. Conflicts with `gateway_id`.
50+
51+
**Note:** Either `gateway_id` or (`nexthop` + `vpc_id`) must be specified.
3152

3253
## Attributes Reference
3354

0 commit comments

Comments
 (0)