Skip to content

Commit b8eb9d9

Browse files
Fix ruleset field change detection using TypeSet for cidr_list
- Changed cidr_list from TypeList to TypeSet in ruleset schema to match other CloudStack resources (security_group_rule, firewall, egress_firewall) - Removed custom resourceCloudStackNetworkACLRulesetHash function in favor of default schema.HashResource for consistency - TypeSet for cidr_list provides automatic ordering independence, eliminating need for manual CIDR sorting in hash function - Updated all code paths to handle cidr_list as *schema.Set for ruleset and []interface{} for legacy rule field - Since ruleset is a new feature, no migration path needed This ensures that changes to cidr, port, protocol, action, ICMP fields, and traffic_type are properly detected when using ruleset blocks. Tests: - All 18 ACL tests pass - Comprehensive field change test validates CIDR, port, action, and ICMP changes
1 parent 7da964b commit b8eb9d9

File tree

2 files changed

+359
-57
lines changed

2 files changed

+359
-57
lines changed

cloudstack/resource_cloudstack_network_acl_rule.go

Lines changed: 123 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
package cloudstack
2121

2222
import (
23-
"bytes"
2423
"context"
2524
"fmt"
2625
"log"
@@ -192,7 +191,6 @@ func resourceCloudStackNetworkACLRule() *schema.Resource {
192191
Type: schema.TypeSet,
193192
Optional: true,
194193
ConflictsWith: []string{"rule"},
195-
Set: resourceCloudStackNetworkACLRulesetHash,
196194
Elem: &schema.Resource{
197195
Schema: map[string]*schema.Schema{
198196
"rule_number": {
@@ -207,9 +205,10 @@ func resourceCloudStackNetworkACLRule() *schema.Resource {
207205
},
208206

209207
"cidr_list": {
210-
Type: schema.TypeList,
208+
Type: schema.TypeSet,
211209
Required: true,
212210
Elem: &schema.Schema{Type: schema.TypeString},
211+
Set: schema.HashString,
213212
},
214213

215214
"protocol": {
@@ -272,21 +271,6 @@ func resourceCloudStackNetworkACLRule() *schema.Resource {
272271
}
273272
}
274273

275-
// resourceCloudStackNetworkACLRulesetHash computes the hash for a ruleset element
276-
// Only the rule_number is used for hashing since it uniquely identifies a rule
277-
// This prevents spurious diffs when fields with defaults are populated in state but not in config
278-
func resourceCloudStackNetworkACLRulesetHash(v interface{}) int {
279-
var buf bytes.Buffer
280-
m := v.(map[string]interface{})
281-
282-
// Only hash on rule_number since it's the unique identifier
283-
if v, ok := m["rule_number"]; ok {
284-
buf.WriteString(fmt.Sprintf("%d-", v.(int)))
285-
}
286-
287-
return schema.HashString(buf.String())
288-
}
289-
290274
// Helper functions for UUID handling to abstract differences between
291275
// 'rule' (uses uuids map) and 'ruleset' (uses uuid string)
292276

@@ -608,8 +592,15 @@ func createNetworkACLRule(d *schema.ResourceData, meta interface{}, rule map[str
608592

609593
// Set the CIDR list
610594
var cidrList []string
611-
for _, cidr := range rule["cidr_list"].([]interface{}) {
612-
cidrList = append(cidrList, cidr.(string))
595+
if cidrSet, ok := rule["cidr_list"].(*schema.Set); ok {
596+
for _, cidr := range cidrSet.List() {
597+
cidrList = append(cidrList, cidr.(string))
598+
}
599+
} else {
600+
// Fallback for 'rule' field which uses TypeList
601+
for _, cidr := range rule["cidr_list"].([]interface{}) {
602+
cidrList = append(cidrList, cidr.(string))
603+
}
613604
}
614605
p.SetCidrlist(cidrList)
615606
log.Printf("[DEBUG] Set cidr_list=%v", cidrList)
@@ -868,15 +859,26 @@ func processPortForRuleUnified(portKey string, rule map[string]interface{}, rule
868859
// Delete the known rule so only unknown rules remain in the ruleMap
869860
delete(ruleMap, id)
870861

871-
var cidrs []interface{}
872-
for _, cidr := range strings.Split(r.Cidrlist, ",") {
873-
cidrs = append(cidrs, cidr)
862+
// Create a list or set with all CIDR's depending on field type
863+
// Check if this is a ruleset rule (has uuid field) vs rule (has uuids field)
864+
_, isRuleset := rule["uuid"]
865+
if isRuleset {
866+
cidrs := &schema.Set{F: schema.HashString}
867+
for _, cidr := range strings.Split(r.Cidrlist, ",") {
868+
cidrs.Add(cidr)
869+
}
870+
rule["cidr_list"] = cidrs
871+
} else {
872+
var cidrs []interface{}
873+
for _, cidr := range strings.Split(r.Cidrlist, ",") {
874+
cidrs = append(cidrs, cidr)
875+
}
876+
rule["cidr_list"] = cidrs
874877
}
875878

876879
rule["action"] = strings.ToLower(r.Action)
877880
rule["protocol"] = r.Protocol
878881
rule["traffic_type"] = strings.ToLower(r.Traffictype)
879-
rule["cidr_list"] = cidrs
880882
rule["rule_number"] = r.Number
881883
rule["description"] = r.Reason
882884
// Set ICMP fields to 0 for non-ICMP protocols to avoid spurious diffs
@@ -978,10 +980,19 @@ func resourceCloudStackNetworkACLRuleRead(d *schema.ResourceData, meta interface
978980
// Delete the known rule so only unknown rules remain in the ruleMap
979981
delete(ruleMap, id)
980982

981-
// Create a list with all CIDR's
982-
var cidrs []interface{}
983-
for _, cidr := range strings.Split(r.Cidrlist, ",") {
984-
cidrs = append(cidrs, cidr)
983+
// Create a list or set with all CIDR's depending on field type
984+
if usingRuleset {
985+
cidrs := &schema.Set{F: schema.HashString}
986+
for _, cidr := range strings.Split(r.Cidrlist, ",") {
987+
cidrs.Add(cidr)
988+
}
989+
rule["cidr_list"] = cidrs
990+
} else {
991+
var cidrs []interface{}
992+
for _, cidr := range strings.Split(r.Cidrlist, ",") {
993+
cidrs = append(cidrs, cidr)
994+
}
995+
rule["cidr_list"] = cidrs
985996
}
986997

987998
// Update the values
@@ -990,7 +1001,6 @@ func resourceCloudStackNetworkACLRuleRead(d *schema.ResourceData, meta interface
9901001
rule["icmp_type"] = r.Icmptype
9911002
rule["icmp_code"] = r.Icmpcode
9921003
rule["traffic_type"] = strings.ToLower(r.Traffictype)
993-
rule["cidr_list"] = cidrs
9941004
rule["rule_number"] = r.Number
9951005
rule["description"] = r.Reason
9961006
if usingRuleset {
@@ -1017,17 +1027,25 @@ func resourceCloudStackNetworkACLRuleRead(d *schema.ResourceData, meta interface
10171027
// Delete the known rule so only unknown rules remain in the ruleMap
10181028
delete(ruleMap, id)
10191029

1020-
// Create a list with all CIDR's
1021-
var cidrs []interface{}
1022-
for _, cidr := range strings.Split(r.Cidrlist, ",") {
1023-
cidrs = append(cidrs, cidr)
1030+
// Create a list or set with all CIDR's depending on field type
1031+
if usingRuleset {
1032+
cidrs := &schema.Set{F: schema.HashString}
1033+
for _, cidr := range strings.Split(r.Cidrlist, ",") {
1034+
cidrs.Add(cidr)
1035+
}
1036+
rule["cidr_list"] = cidrs
1037+
} else {
1038+
var cidrs []interface{}
1039+
for _, cidr := range strings.Split(r.Cidrlist, ",") {
1040+
cidrs = append(cidrs, cidr)
1041+
}
1042+
rule["cidr_list"] = cidrs
10241043
}
10251044

10261045
// Update the values
10271046
rule["action"] = strings.ToLower(r.Action)
10281047
rule["protocol"] = r.Protocol
10291048
rule["traffic_type"] = strings.ToLower(r.Traffictype)
1030-
rule["cidr_list"] = cidrs
10311049
rule["rule_number"] = r.Number
10321050
rule["description"] = r.Reason
10331051
delete(rule, "port") // Remove port field for "all" protocol
@@ -1063,15 +1081,15 @@ func resourceCloudStackNetworkACLRuleRead(d *schema.ResourceData, meta interface
10631081
outOfBandRuleNumber := maxRuleNumber + 1
10641082

10651083
for uuid := range ruleMap {
1066-
// We need to create and add a placeholder value to a list as the
1067-
// cidr_list is a required field and thus needs a value
1068-
cidrs := []interface{}{uuid}
1069-
10701084
// Make a placeholder rule to hold the unknown UUID
10711085
// Format differs between 'rule' and 'ruleset'
10721086
var rule map[string]interface{}
10731087
if usingRuleset {
10741088
// For ruleset: use 'uuid' string and include rule_number
1089+
// cidr_list is a TypeSet for ruleset
1090+
cidrs := &schema.Set{F: schema.HashString}
1091+
cidrs.Add(uuid)
1092+
10751093
// Include all fields with defaults to avoid spurious diffs
10761094
rule = map[string]interface{}{
10771095
"cidr_list": cidrs,
@@ -1088,6 +1106,8 @@ func resourceCloudStackNetworkACLRuleRead(d *schema.ResourceData, meta interface
10881106
outOfBandRuleNumber++
10891107
} else {
10901108
// For rule: use 'uuids' map
1109+
// cidr_list is a TypeList for rule
1110+
cidrs := []interface{}{uuid}
10911111
rule = map[string]interface{}{
10921112
"cidr_list": cidrs,
10931113
"protocol": uuid,
@@ -1107,8 +1127,9 @@ func resourceCloudStackNetworkACLRuleRead(d *schema.ResourceData, meta interface
11071127
// For TypeSet, we need to be very careful about state updates
11081128
// The SDK has issues with properly clearing removed elements from TypeSet
11091129
// So we explicitly set to empty first, then set the new value
1110-
// Use the same hash function as defined in the schema to avoid spurious diffs
1111-
hashFunc := resourceCloudStackNetworkACLRulesetHash
1130+
// Use schema.HashResource to match the default hash function
1131+
rulesetResource := resourceCloudStackNetworkACLRule().Schema["ruleset"].Elem.(*schema.Resource)
1132+
hashFunc := schema.HashResource(rulesetResource)
11121133

11131134
// First, clear the ruleset completely
11141135
emptySet := schema.NewSet(hashFunc, []interface{}{})
@@ -1529,7 +1550,17 @@ func performNormalRuleUpdates(d *schema.ResourceData, meta interface{}, cs *clou
15291550

15301551
newRuleMap := newRule.(map[string]interface{})
15311552
log.Printf("[DEBUG] Comparing old rule %+v with new rule %+v", oldRuleMap, newRuleMap)
1532-
if rulesMatch(oldRuleMap, newRuleMap) {
1553+
1554+
// For ruleset rules, match by rule_number only
1555+
// For regular rules, use the full rulesMatch function
1556+
var matched bool
1557+
if isRulesetRule(oldRuleMap) && isRulesetRule(newRuleMap) {
1558+
matched = rulesetRulesMatchByNumber(oldRuleMap, newRuleMap)
1559+
} else {
1560+
matched = rulesMatch(oldRuleMap, newRuleMap)
1561+
}
1562+
1563+
if matched {
15331564
log.Printf("[DEBUG] Found matching new rule for old rule")
15341565

15351566
// Copy UUID from old rule to new rule
@@ -1609,6 +1640,20 @@ func performNormalRuleUpdates(d *schema.ResourceData, meta interface{}, cs *clou
16091640
return nil
16101641
}
16111642

1643+
// rulesetRulesMatchByNumber matches ruleset rules by rule_number only
1644+
// This allows changes to other fields (CIDR, port, protocol, etc.) to be detected as updates
1645+
func rulesetRulesMatchByNumber(oldRule, newRule map[string]interface{}) bool {
1646+
oldRuleNum, oldHasRuleNum := oldRule["rule_number"].(int)
1647+
newRuleNum, newHasRuleNum := newRule["rule_number"].(int)
1648+
1649+
// Both must have rule_number and they must match
1650+
if !oldHasRuleNum || !newHasRuleNum {
1651+
return false
1652+
}
1653+
1654+
return oldRuleNum == newRuleNum
1655+
}
1656+
16121657
func rulesMatch(oldRule, newRule map[string]interface{}) bool {
16131658
oldProtocol := oldRule["protocol"].(string)
16141659
newProtocol := newRule["protocol"].(string)
@@ -1725,20 +1770,34 @@ func ruleNeedsUpdate(oldRule, newRule map[string]interface{}) bool {
17251770
}
17261771
}
17271772

1728-
oldCidrs := oldRule["cidr_list"].([]interface{})
1729-
newCidrs := newRule["cidr_list"].([]interface{})
1730-
if len(oldCidrs) != len(newCidrs) {
1731-
log.Printf("[DEBUG] CIDR list length changed: %d -> %d", len(oldCidrs), len(newCidrs))
1732-
return true
1773+
// Handle cidr_list comparison - can be TypeSet (ruleset) or TypeList (rule)
1774+
var oldCidrStrs, newCidrStrs []string
1775+
1776+
// Extract old CIDRs
1777+
if oldSet, ok := oldRule["cidr_list"].(*schema.Set); ok {
1778+
for _, cidr := range oldSet.List() {
1779+
oldCidrStrs = append(oldCidrStrs, cidr.(string))
1780+
}
1781+
} else if oldList, ok := oldRule["cidr_list"].([]interface{}); ok {
1782+
for _, cidr := range oldList {
1783+
oldCidrStrs = append(oldCidrStrs, cidr.(string))
1784+
}
17331785
}
17341786

1735-
oldCidrStrs := make([]string, len(oldCidrs))
1736-
newCidrStrs := make([]string, len(newCidrs))
1737-
for i, cidr := range oldCidrs {
1738-
oldCidrStrs[i] = cidr.(string)
1787+
// Extract new CIDRs
1788+
if newSet, ok := newRule["cidr_list"].(*schema.Set); ok {
1789+
for _, cidr := range newSet.List() {
1790+
newCidrStrs = append(newCidrStrs, cidr.(string))
1791+
}
1792+
} else if newList, ok := newRule["cidr_list"].([]interface{}); ok {
1793+
for _, cidr := range newList {
1794+
newCidrStrs = append(newCidrStrs, cidr.(string))
1795+
}
17391796
}
1740-
for i, cidr := range newCidrs {
1741-
newCidrStrs[i] = cidr.(string)
1797+
1798+
if len(oldCidrStrs) != len(newCidrStrs) {
1799+
log.Printf("[DEBUG] CIDR list length changed: %d -> %d", len(oldCidrStrs), len(newCidrStrs))
1800+
return true
17421801
}
17431802

17441803
sort.Strings(oldCidrStrs)
@@ -1768,13 +1827,22 @@ func updateNetworkACLRule(cs *cloudstack.CloudStackClient, oldRule, newRule map[
17681827
p.SetAction(newRule["action"].(string))
17691828

17701829
var cidrList []string
1771-
for _, cidr := range newRule["cidr_list"].([]interface{}) {
1772-
cidrList = append(cidrList, cidr.(string))
1830+
if cidrSet, ok := newRule["cidr_list"].(*schema.Set); ok {
1831+
for _, cidr := range cidrSet.List() {
1832+
cidrList = append(cidrList, cidr.(string))
1833+
}
1834+
} else {
1835+
for _, cidr := range newRule["cidr_list"].([]interface{}) {
1836+
cidrList = append(cidrList, cidr.(string))
1837+
}
17731838
}
17741839
p.SetCidrlist(cidrList)
17751840

1776-
if desc, ok := newRule["description"].(string); ok && desc != "" {
1841+
// Set description from the new rule
1842+
if desc, ok := newRule["description"].(string); ok {
17771843
p.SetReason(desc)
1844+
} else {
1845+
p.SetReason("")
17781846
}
17791847

17801848
p.SetProtocol(newRule["protocol"].(string))

0 commit comments

Comments
 (0)