From d8db099cd3cf8317db75e0522c3948556fddb586 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20Fl=C3=B6tzinger?= Date: Thu, 25 Jun 2026 09:51:21 +0200 Subject: [PATCH 1/4] chore: add .worktrees to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index b6b0f366..514d1d25 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,4 @@ website/vendor !command/test-fixtures/**/.terraform/ .terraform.lock.hcl provider.tf +.worktrees/ From b66e42fd594a87965fa8680664ba8ab8c22984ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20Fl=C3=B6tzinger?= Date: Thu, 25 Jun 2026 10:07:47 +0200 Subject: [PATCH 2/4] feat: add dest_cidr_list support to cloudstack_egress_firewall Adds the destcidrlist parameter to egress firewall rules, allowing users to restrict the destination of egress traffic. The field is optional and maps to the CloudStack API's destcidrlist parameter. Fixes #296 --- .../resource_cloudstack_egress_firewall.go | 43 +++++++++++++++++++ website/docs/r/egress_firewall.html.markdown | 4 +- 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/cloudstack/resource_cloudstack_egress_firewall.go b/cloudstack/resource_cloudstack_egress_firewall.go index e2a83e4c..9c942d8a 100644 --- a/cloudstack/resource_cloudstack_egress_firewall.go +++ b/cloudstack/resource_cloudstack_egress_firewall.go @@ -70,6 +70,13 @@ func resourceCloudStackEgressFirewall() *schema.Resource { Set: schema.HashString, }, + "dest_cidr_list": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + }, + "protocol": { Type: schema.TypeString, Required: true, @@ -194,6 +201,15 @@ func createEgressFirewallRule(d *schema.ResourceData, meta interface{}, rule map p.SetCidrlist(cidrList) } + // Set the destination CIDR list + var destCidrList []string + if rs := rule["dest_cidr_list"].(*schema.Set); rs.Len() > 0 { + for _, cidr := range rule["dest_cidr_list"].(*schema.Set).List() { + destCidrList = append(destCidrList, cidr.(string)) + } + p.SetDestcidrlist(destCidrList) + } + // If the protocol is ICMP set the needed ICMP parameters if rule["protocol"].(string) == "icmp" { p.SetIcmptype(rule["icmp_type"].(int)) @@ -319,11 +335,20 @@ func resourceCloudStackEgressFirewallRead(d *schema.ResourceData, meta interface cidrs.Add(cidr) } + // Create a set with all destination CIDR's + destCidrs := &schema.Set{F: schema.HashString} + if r.Destcidrlist != "" { + for _, cidr := range strings.Split(r.Destcidrlist, ",") { + destCidrs.Add(cidr) + } + } + // Update the values rule["protocol"] = r.Protocol rule["icmp_type"] = r.Icmptype rule["icmp_code"] = r.Icmpcode rule["cidr_list"] = cidrs + rule["dest_cidr_list"] = destCidrs rules.Add(rule) } @@ -357,9 +382,18 @@ func resourceCloudStackEgressFirewallRead(d *schema.ResourceData, meta interface cidrs.Add(cidr) } + // Create a set with all destination CIDR's + destCidrs := &schema.Set{F: schema.HashString} + if r.Destcidrlist != "" { + for _, cidr := range strings.Split(r.Destcidrlist, ",") { + destCidrs.Add(cidr) + } + } + // Update the values rule["protocol"] = r.Protocol rule["cidr_list"] = cidrs + rule["dest_cidr_list"] = destCidrs ports.Add(port) } @@ -395,6 +429,15 @@ func resourceCloudStackEgressFirewallRead(d *schema.ResourceData, meta interface rule["cidr_list"] = cidrs } + // Create a set with all destination CIDR's + destCidrs := &schema.Set{F: schema.HashString} + if r.Destcidrlist != "" { + for _, cidr := range strings.Split(r.Destcidrlist, ",") { + destCidrs.Add(cidr) + } + } + rule["dest_cidr_list"] = destCidrs + // Update the values rule["protocol"] = r.Protocol rules.Add(rule) diff --git a/website/docs/r/egress_firewall.html.markdown b/website/docs/r/egress_firewall.html.markdown index 10badd17..a338119d 100644 --- a/website/docs/r/egress_firewall.html.markdown +++ b/website/docs/r/egress_firewall.html.markdown @@ -43,7 +43,9 @@ The following arguments are supported: The `rule` block supports: -* `cidr_list` - (Required) A CIDR list to allow access to the given ports. +* `cidr_list` - (Optional) A CIDR list to allow access to the given ports. + +* `dest_cidr_list` - (Optional) A CIDR list to restrict the destination of egress traffic. * `protocol` - (Required) The name of the protocol to allow. Valid options are: `tcp`, `udp` and `icmp`. From ef6f217de9532df0afbef7615fd0e374bcb2ed0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20Fl=C3=B6tzinger?= Date: Thu, 25 Jun 2026 15:24:30 +0200 Subject: [PATCH 3/4] refactor: simplify egress firewall read with cidrSetFromList helper Restructure resourceCloudStackEgressFirewallRead to use a switch on the protocol and extract the repeated CIDR set-building into a single cidrSetFromList helper. The helper returns an empty set for an empty list, restoring the guard against empty Destcidrlist consistently across all branches. Verified with the egress firewall acceptance tests against the cloudstack simulator. --- .gitignore | 1 + .../resource_cloudstack_egress_firewall.go | 145 +++++++----------- 2 files changed, 56 insertions(+), 90 deletions(-) diff --git a/.gitignore b/.gitignore index 514d1d25..2c88f78d 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,4 @@ website/vendor .terraform.lock.hcl provider.tf .worktrees/ +.env diff --git a/cloudstack/resource_cloudstack_egress_firewall.go b/cloudstack/resource_cloudstack_egress_firewall.go index 9c942d8a..beec0bd0 100644 --- a/cloudstack/resource_cloudstack_egress_firewall.go +++ b/cloudstack/resource_cloudstack_egress_firewall.go @@ -202,8 +202,8 @@ func createEgressFirewallRule(d *schema.ResourceData, meta interface{}, rule map } // Set the destination CIDR list - var destCidrList []string - if rs := rule["dest_cidr_list"].(*schema.Set); rs.Len() > 0 { + if rs, ok := rule["dest_cidr_list"].(*schema.Set); ok && rs.Len() > 0 { + var destCidrList []string for _, cidr := range rule["dest_cidr_list"].(*schema.Set).List() { destCidrList = append(destCidrList, cidr.(string)) } @@ -280,6 +280,19 @@ func createEgressFirewallRule(d *schema.ResourceData, meta interface{}, rule map return nil } +// cidrSetFromList builds a schema.Set of CIDRs from a comma-separated list, +// returning an empty set when the list is empty. +func cidrSetFromList(list string) *schema.Set { + set := &schema.Set{F: schema.HashString} + if list == "" { + return set + } + for _, cidr := range strings.Split(list, ",") { + set.Add(cidr) + } + return set +} + func resourceCloudStackEgressFirewallRead(d *schema.ResourceData, meta interface{}) error { cs := meta.(*cloudstack.CloudStackClient) @@ -313,7 +326,10 @@ func resourceCloudStackEgressFirewallRead(d *schema.ResourceData, meta interface rule := rule.(map[string]interface{}) uuids := rule["uuids"].(map[string]interface{}) - if rule["protocol"].(string) == "icmp" { + protocol := strings.ToLower(rule["protocol"].(string)) + + switch protocol { + case "icmp": id, ok := uuids["icmp"] if !ok { continue @@ -329,82 +345,14 @@ func resourceCloudStackEgressFirewallRead(d *schema.ResourceData, meta interface // Delete the known rule so only unknown rules remain in the ruleMap delete(ruleMap, id.(string)) - // Create a set with all CIDR's - cidrs := &schema.Set{F: schema.HashString} - for _, cidr := range strings.Split(r.Cidrlist, ",") { - cidrs.Add(cidr) - } - - // Create a set with all destination CIDR's - destCidrs := &schema.Set{F: schema.HashString} - if r.Destcidrlist != "" { - for _, cidr := range strings.Split(r.Destcidrlist, ",") { - destCidrs.Add(cidr) - } - } - // Update the values rule["protocol"] = r.Protocol rule["icmp_type"] = r.Icmptype rule["icmp_code"] = r.Icmpcode - rule["cidr_list"] = cidrs - rule["dest_cidr_list"] = destCidrs + rule["cidr_list"] = cidrSetFromList(r.Cidrlist) + rule["dest_cidr_list"] = cidrSetFromList(r.Destcidrlist) rules.Add(rule) - } - - // If protocol is not ICMP, loop through all ports - if rule["protocol"].(string) != "icmp" && strings.ToLower(rule["protocol"].(string)) != "all" { - if ps := rule["ports"].(*schema.Set); ps.Len() > 0 { - - // Create an empty schema.Set to hold all ports - ports := &schema.Set{F: schema.HashString} - - // Loop through all ports and retrieve their info - for _, port := range ps.List() { - id, ok := uuids[port.(string)] - if !ok { - continue - } - - // Get the rule - r, ok := ruleMap[id.(string)] - if !ok { - delete(uuids, port.(string)) - continue - } - - // Delete the known rule so only unknown rules remain in the ruleMap - delete(ruleMap, id.(string)) - - // Create a set with all CIDR's - cidrs := &schema.Set{F: schema.HashString} - for _, cidr := range strings.Split(r.Cidrlist, ",") { - cidrs.Add(cidr) - } - - // Create a set with all destination CIDR's - destCidrs := &schema.Set{F: schema.HashString} - if r.Destcidrlist != "" { - for _, cidr := range strings.Split(r.Destcidrlist, ",") { - destCidrs.Add(cidr) - } - } - - // Update the values - rule["protocol"] = r.Protocol - rule["cidr_list"] = cidrs - rule["dest_cidr_list"] = destCidrs - ports.Add(port) - } - - // If there is at least one port found, add this rule to the rules set - if ports.Len() > 0 { - rule["ports"] = ports - rules.Add(rule) - } - } - } - if strings.ToLower(rule["protocol"].(string)) == "all" { + case "all": id, ok := uuids["all"] if !ok { continue @@ -420,27 +368,44 @@ func resourceCloudStackEgressFirewallRead(d *schema.ResourceData, meta interface // Delete the known rule so only unknown rules remain in the ruleMap delete(ruleMap, id.(string)) - // Create a set with all CIDR's - if _, ok := rule["cidr_list"]; ok { - cidrs := &schema.Set{F: schema.HashString} - for _, cidr := range strings.Split(r.Cidrlist, ",") { - cidrs.Add(cidr) + // Update the values + rule["protocol"] = r.Protocol + rule["cidr_list"] = cidrSetFromList(r.Cidrlist) + rule["dest_cidr_list"] = cidrSetFromList(r.Destcidrlist) + rules.Add(rule) + default: + // Create an empty schema.Set to hold all ports + ports := &schema.Set{F: schema.HashString} + + // Loop through all ports and retrieve their info + for _, port := range rule["ports"].(*schema.Set).List() { + id, ok := uuids[port.(string)] + if !ok { + continue } - rule["cidr_list"] = cidrs - } - // Create a set with all destination CIDR's - destCidrs := &schema.Set{F: schema.HashString} - if r.Destcidrlist != "" { - for _, cidr := range strings.Split(r.Destcidrlist, ",") { - destCidrs.Add(cidr) + // Get the rule + r, ok := ruleMap[id.(string)] + if !ok { + delete(uuids, port.(string)) + continue } + + // Delete the known rule so only unknown rules remain in the ruleMap + delete(ruleMap, id.(string)) + + // Update the values + rule["protocol"] = r.Protocol + rule["cidr_list"] = cidrSetFromList(r.Cidrlist) + rule["dest_cidr_list"] = cidrSetFromList(r.Destcidrlist) + ports.Add(port) } - rule["dest_cidr_list"] = destCidrs - // Update the values - rule["protocol"] = r.Protocol - rules.Add(rule) + // If there is at least one port found, add this rule to the rules set + if ports.Len() > 0 { + rule["ports"] = ports + rules.Add(rule) + } } } } From dd4d1736f104ca46729a364b562595532fb74afc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20Fl=C3=B6tzinger?= Date: Thu, 25 Jun 2026 15:39:55 +0200 Subject: [PATCH 4/4] docs: add dest_cidr_list example and 'all' protocol to egress firewall --- website/docs/r/egress_firewall.html.markdown | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/website/docs/r/egress_firewall.html.markdown b/website/docs/r/egress_firewall.html.markdown index a338119d..dde74062 100644 --- a/website/docs/r/egress_firewall.html.markdown +++ b/website/docs/r/egress_firewall.html.markdown @@ -17,9 +17,10 @@ resource "cloudstack_egress_firewall" "default" { network_id = "6eb22f91-7454-4107-89f4-36afcdf33021" rule { - cidr_list = ["10.0.0.0/8"] - protocol = "tcp" - ports = ["80", "1000-2000"] + cidr_list = ["10.0.0.0/8"] + dest_cidr_list = ["192.168.0.0/16"] + protocol = "tcp" + ports = ["80", "1000-2000"] } } ``` @@ -48,7 +49,7 @@ The `rule` block supports: * `dest_cidr_list` - (Optional) A CIDR list to restrict the destination of egress traffic. * `protocol` - (Required) The name of the protocol to allow. Valid options are: - `tcp`, `udp` and `icmp`. + `tcp`, `udp`, `icmp` and `all`. * `icmp_type` - (Optional) The ICMP type to allow. This can only be specified if the protocol is ICMP.