@@ -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+
109237func 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+ }`
0 commit comments