Skip to content

Commit cbb254b

Browse files
authored
Merge pull request #204 from apache/port-forward-add-end-ports
Allow specifying private end port & public end port for port forward rules
2 parents 540f8d0 + 5142e90 commit cbb254b

File tree

3 files changed

+209
-2
lines changed

3 files changed

+209
-2
lines changed

cloudstack/resource_cloudstack_port_forward.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,11 +73,23 @@ func resourceCloudStackPortForward() *schema.Resource {
7373
Required: true,
7474
},
7575

76+
"private_end_port": {
77+
Type: schema.TypeInt,
78+
Optional: true,
79+
Computed: true,
80+
},
81+
7682
"public_port": {
7783
Type: schema.TypeInt,
7884
Required: true,
7985
},
8086

87+
"public_end_port": {
88+
Type: schema.TypeInt,
89+
Optional: true,
90+
Computed: true,
91+
},
92+
8193
"virtual_machine_id": {
8294
Type: schema.TypeString,
8395
Required: true,
@@ -175,6 +187,12 @@ func createPortForward(d *schema.ResourceData, meta interface{}, forward map[str
175187
// Create a new parameter struct
176188
p := cs.Firewall.NewCreatePortForwardingRuleParams(d.Id(), forward["private_port"].(int),
177189
forward["protocol"].(string), forward["public_port"].(int), vm.Id)
190+
if val, ok := forward["private_end_port"]; ok && val != nil && val.(int) != 0 {
191+
p.SetPrivateendport(val.(int))
192+
}
193+
if val, ok := forward["public_end_port"]; ok && val != nil && val.(int) != 0 {
194+
p.SetPublicendport(val.(int))
195+
}
178196

179197
if vmGuestIP, ok := forward["vm_guest_ip"]; ok && vmGuestIP.(string) != "" {
180198
p.SetVmguestip(vmGuestIP.(string))
@@ -288,6 +306,21 @@ func resourceCloudStackPortForwardRead(d *schema.ResourceData, meta interface{})
288306
forward["protocol"] = f.Protocol
289307
forward["private_port"] = privPort
290308
forward["public_port"] = pubPort
309+
// Only set end ports if they differ from start ports (indicating a range)
310+
if f.Privateendport != "" && f.Privateendport != f.Privateport {
311+
privEndPort, err := strconv.Atoi(f.Privateendport)
312+
if err != nil {
313+
return err
314+
}
315+
forward["private_end_port"] = privEndPort
316+
}
317+
if f.Publicendport != "" && f.Publicendport != f.Publicport {
318+
pubEndPort, err := strconv.Atoi(f.Publicendport)
319+
if err != nil {
320+
return err
321+
}
322+
forward["public_end_port"] = pubEndPort
323+
}
291324
forward["virtual_machine_id"] = f.Virtualmachineid
292325

293326
// This one is a bit tricky. We only want to update this optional value

cloudstack/resource_cloudstack_port_forward_test.go

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,16 @@ func TestAccCloudStackPortForward_basic(t *testing.T) {
4141
testAccCheckCloudStackPortForwardsExist("cloudstack_port_forward.foo"),
4242
resource.TestCheckResourceAttr(
4343
"cloudstack_port_forward.foo", "forward.#", "1"),
44+
resource.TestCheckResourceAttrSet(
45+
"cloudstack_port_forward.foo", "forward.0.uuid"),
46+
resource.TestCheckResourceAttr(
47+
"cloudstack_port_forward.foo", "forward.0.protocol", "tcp"),
48+
resource.TestCheckResourceAttr(
49+
"cloudstack_port_forward.foo", "forward.0.private_port", "443"),
50+
resource.TestCheckResourceAttr(
51+
"cloudstack_port_forward.foo", "forward.0.public_port", "8443"),
52+
resource.TestCheckResourceAttrSet(
53+
"cloudstack_port_forward.foo", "forward.0.virtual_machine_id"),
4454
),
4555
},
4656
},
@@ -68,6 +78,41 @@ func TestAccCloudStackPortForward_update(t *testing.T) {
6878
testAccCheckCloudStackPortForwardsExist("cloudstack_port_forward.foo"),
6979
resource.TestCheckResourceAttr(
7080
"cloudstack_port_forward.foo", "forward.#", "2"),
81+
// Validate first forward rule
82+
resource.TestCheckResourceAttrSet(
83+
"cloudstack_port_forward.foo", "forward.0.uuid"),
84+
resource.TestCheckResourceAttr(
85+
"cloudstack_port_forward.foo", "forward.0.protocol", "tcp"),
86+
resource.TestCheckResourceAttrSet(
87+
"cloudstack_port_forward.foo", "forward.0.virtual_machine_id"),
88+
// Validate second forward rule
89+
resource.TestCheckResourceAttrSet(
90+
"cloudstack_port_forward.foo", "forward.1.uuid"),
91+
resource.TestCheckResourceAttr(
92+
"cloudstack_port_forward.foo", "forward.1.protocol", "tcp"),
93+
resource.TestCheckResourceAttrSet(
94+
"cloudstack_port_forward.foo", "forward.1.virtual_machine_id"),
95+
),
96+
},
97+
},
98+
})
99+
}
100+
101+
func TestAccCloudStackPortForward_portRange(t *testing.T) {
102+
resource.Test(t, resource.TestCase{
103+
PreCheck: func() { testAccPreCheck(t) },
104+
Providers: testAccProviders,
105+
CheckDestroy: testAccCheckCloudStackPortForwardDestroy,
106+
Steps: []resource.TestStep{
107+
{
108+
Config: testAccCloudStackPortForward_portRange,
109+
Check: resource.ComposeTestCheckFunc(
110+
testAccCheckCloudStackPortForwardsExist("cloudstack_port_forward.foo"),
111+
resource.TestCheckResourceAttr(
112+
"cloudstack_port_forward.foo", "forward.#", "2"),
113+
testAccCheckCloudStackPortForwardAttributes("cloudstack_port_forward.foo"),
114+
// Note: We don't check specific indices since sets are unordered
115+
// The testAccCheckCloudStackPortForwardAttributes function handles validation
71116
),
72117
},
73118
},
@@ -106,6 +151,89 @@ func testAccCheckCloudStackPortForwardsExist(n string) resource.TestCheckFunc {
106151
}
107152
}
108153

154+
func testAccCheckCloudStackPortForwardAttributes(n string) resource.TestCheckFunc {
155+
return func(s *terraform.State) error {
156+
rs, ok := s.RootModule().Resources[n]
157+
if !ok {
158+
return fmt.Errorf("Not found: %s", n)
159+
}
160+
161+
if rs.Primary.ID == "" {
162+
return fmt.Errorf("No port forward ID is set")
163+
}
164+
165+
// Verify we have 2 forward rules
166+
if rs.Primary.Attributes["forward.#"] != "2" {
167+
return fmt.Errorf("Expected 2 forward rules, got %s", rs.Primary.Attributes["forward.#"])
168+
}
169+
170+
var foundTCPRange, foundUDPSingle bool
171+
172+
// Check both forward rules to find the expected configurations
173+
for i := 0; i < 2; i++ {
174+
protocolKey := fmt.Sprintf("forward.%d.protocol", i)
175+
privatePortKey := fmt.Sprintf("forward.%d.private_port", i)
176+
privateEndPortKey := fmt.Sprintf("forward.%d.private_end_port", i)
177+
publicPortKey := fmt.Sprintf("forward.%d.public_port", i)
178+
publicEndPortKey := fmt.Sprintf("forward.%d.public_end_port", i)
179+
uuidKey := fmt.Sprintf("forward.%d.uuid", i)
180+
181+
protocol := rs.Primary.Attributes[protocolKey]
182+
privatePort := rs.Primary.Attributes[privatePortKey]
183+
privateEndPort := rs.Primary.Attributes[privateEndPortKey]
184+
publicPort := rs.Primary.Attributes[publicPortKey]
185+
publicEndPort := rs.Primary.Attributes[publicEndPortKey]
186+
uuid := rs.Primary.Attributes[uuidKey]
187+
188+
// Verify basic required fields exist
189+
if protocol == "" {
190+
return fmt.Errorf("Missing protocol for forward rule %d", i)
191+
}
192+
if privatePort == "" {
193+
return fmt.Errorf("Missing private_port for forward rule %d", i)
194+
}
195+
if publicPort == "" {
196+
return fmt.Errorf("Missing public_port for forward rule %d", i)
197+
}
198+
if uuid == "" {
199+
return fmt.Errorf("Missing uuid for forward rule %d", i)
200+
}
201+
202+
// Check for TCP rule with port range (8080-8090)
203+
if protocol == "tcp" && privatePort == "8080" && publicPort == "8080" {
204+
if privateEndPort != "8090" {
205+
return fmt.Errorf("Expected TCP rule to have private_end_port=8090, got %s", privateEndPort)
206+
}
207+
if publicEndPort != "8090" {
208+
return fmt.Errorf("Expected TCP rule to have public_end_port=8090, got %s", publicEndPort)
209+
}
210+
foundTCPRange = true
211+
}
212+
213+
// Check for UDP rule with single port (9000)
214+
if protocol == "udp" && privatePort == "9000" && publicPort == "9000" {
215+
// For single port rules, end ports should be empty, "0", or equal to start ports
216+
if privateEndPort != "" && privateEndPort != "0" && privateEndPort != "9000" {
217+
return fmt.Errorf("Expected UDP rule to have empty, '0', or matching private_end_port, got %s", privateEndPort)
218+
}
219+
if publicEndPort != "" && publicEndPort != "0" && publicEndPort != "9000" {
220+
return fmt.Errorf("Expected UDP rule to have empty, '0', or matching public_end_port, got %s", publicEndPort)
221+
}
222+
foundUDPSingle = true
223+
}
224+
}
225+
226+
if !foundTCPRange {
227+
return fmt.Errorf("Expected to find TCP rule with port range 8080-8090")
228+
}
229+
if !foundUDPSingle {
230+
return fmt.Errorf("Expected to find UDP rule with single port 9000")
231+
}
232+
233+
return nil
234+
}
235+
}
236+
109237
func testAccCheckCloudStackPortForwardDestroy(s *terraform.State) error {
110238
cs := testAccProvider.Meta().(*cloudstack.CloudStackClient)
111239

@@ -201,3 +329,43 @@ resource "cloudstack_port_forward" "foo" {
201329
virtual_machine_id = cloudstack_instance.foobar.id
202330
}
203331
}`
332+
333+
const testAccCloudStackPortForward_portRange = `
334+
resource "cloudstack_network" "foo" {
335+
name = "terraform-network"
336+
display_text = "terraform-network"
337+
cidr = "10.1.1.0/24"
338+
network_offering = "DefaultIsolatedNetworkOfferingWithSourceNatService"
339+
source_nat_ip = true
340+
zone = "Sandbox-simulator"
341+
}
342+
343+
resource "cloudstack_instance" "foobar" {
344+
name = "terraform-test"
345+
display_name = "terraform-updated"
346+
service_offering= "Medium Instance"
347+
network_id = cloudstack_network.foo.id
348+
template = "CentOS 5.6 (64-bit) no GUI (Simulator)"
349+
zone = "Sandbox-simulator"
350+
expunge = true
351+
}
352+
353+
resource "cloudstack_port_forward" "foo" {
354+
ip_address_id = cloudstack_network.foo.source_nat_ip_id
355+
356+
forward {
357+
protocol = "tcp"
358+
private_port = 8080
359+
private_end_port = 8090
360+
public_port = 8080
361+
public_end_port = 8090
362+
virtual_machine_id = cloudstack_instance.foobar.id
363+
}
364+
365+
forward {
366+
protocol = "udp"
367+
private_port = 9000
368+
public_port = 9000
369+
virtual_machine_id = cloudstack_instance.foobar.id
370+
}
371+
}`

website/docs/r/port_forward.html.markdown

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,15 @@ The `forward` block supports:
4747
* `protocol` - (Required) The name of the protocol to allow. Valid options are:
4848
`tcp` and `udp`.
4949

50-
* `private_port` - (Required) The private port to forward to.
50+
* `private_port` - (Required) The starting port of port forwarding rule's private port range.
5151

52-
* `public_port` - (Required) The public port to forward from.
52+
* `private_end_port` - (Optional) The ending port of port forwarding rule's private port range.
53+
If not specified, the private port will be used as the end port.
54+
55+
* `public_port` - (Required) The starting port of port forwarding rule's public port range.
56+
57+
* `public_end_port` - (Optional) The ending port of port forwarding rule's public port range.
58+
If not specified, the public port will be used as the end port.
5359

5460
* `virtual_machine_id` - (Required) The ID of the virtual machine to forward to.
5561

0 commit comments

Comments
 (0)