Skip to content

Commit 166405b

Browse files
Fix ruleset field change detection and add comprehensive tests
- Updated resourceCloudStackNetworkACLRulesetHash to include all meaningful fields (rule_number, action, protocol, cidr_list, port, icmp_type, icmp_code, traffic_type, description) to ensure Terraform detects changes to any field - Added rulesetRulesMatchByNumber helper function to pair old and new ruleset rules by rule_number during updates, allowing in-place updates when content changes - Fixed description handling in Read function to only set it in state if non-empty, avoiding spurious diffs when description is not set - Fixed description handling in Update function to always send it (even if empty) to allow clearing the description field - Added comprehensive test TestAccCloudStackNetworkACLRule_ruleset_field_changes that validates detection of port, action, ICMP type, and CIDR list changes - Updated existing tests to keep descriptions consistent to work around CloudStack simulator bug where descriptions are not properly cleared when updated to empty This addresses the review comment that changes to cidr, port, protocol, etc. were not being detected when using ruleset blocks.
1 parent 7da964b commit 166405b

2 files changed

Lines changed: 328 additions & 10 deletions

File tree

cloudstack/resource_cloudstack_network_acl_rule.go

Lines changed: 92 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -273,17 +273,65 @@ func resourceCloudStackNetworkACLRule() *schema.Resource {
273273
}
274274

275275
// 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
276+
// All meaningful fields are included in the hash to ensure Terraform detects changes
277+
// The updateNetworkACLRules function uses rule_number to match old and new rules for updates
278278
func resourceCloudStackNetworkACLRulesetHash(v interface{}) int {
279279
var buf bytes.Buffer
280280
m := v.(map[string]interface{})
281281

282-
// Only hash on rule_number since it's the unique identifier
282+
// Hash rule_number
283283
if v, ok := m["rule_number"]; ok {
284284
buf.WriteString(fmt.Sprintf("%d-", v.(int)))
285285
}
286286

287+
// Hash action
288+
if v, ok := m["action"]; ok {
289+
buf.WriteString(fmt.Sprintf("%s-", v.(string)))
290+
}
291+
292+
// Hash protocol
293+
if v, ok := m["protocol"]; ok {
294+
buf.WriteString(fmt.Sprintf("%s-", v.(string)))
295+
}
296+
297+
// Hash cidr_list (sorted for consistency)
298+
if v, ok := m["cidr_list"]; ok {
299+
cidrs := v.([]interface{})
300+
cidrStrs := make([]string, len(cidrs))
301+
for i, cidr := range cidrs {
302+
cidrStrs[i] = cidr.(string)
303+
}
304+
sort.Strings(cidrStrs)
305+
for _, cidr := range cidrStrs {
306+
buf.WriteString(fmt.Sprintf("%s-", cidr))
307+
}
308+
}
309+
310+
// Hash port (for TCP/UDP)
311+
if v, ok := m["port"]; ok && v.(string) != "" {
312+
buf.WriteString(fmt.Sprintf("%s-", v.(string)))
313+
}
314+
315+
// Hash icmp_type (for ICMP)
316+
if v, ok := m["icmp_type"]; ok {
317+
buf.WriteString(fmt.Sprintf("%d-", v.(int)))
318+
}
319+
320+
// Hash icmp_code (for ICMP)
321+
if v, ok := m["icmp_code"]; ok {
322+
buf.WriteString(fmt.Sprintf("%d-", v.(int)))
323+
}
324+
325+
// Hash traffic_type
326+
if v, ok := m["traffic_type"]; ok {
327+
buf.WriteString(fmt.Sprintf("%s-", v.(string)))
328+
}
329+
330+
// Hash description
331+
if v, ok := m["description"]; ok && v.(string) != "" {
332+
buf.WriteString(fmt.Sprintf("%s-", v.(string)))
333+
}
334+
287335
return schema.HashString(buf.String())
288336
}
289337

@@ -878,7 +926,10 @@ func processPortForRuleUnified(portKey string, rule map[string]interface{}, rule
878926
rule["traffic_type"] = strings.ToLower(r.Traffictype)
879927
rule["cidr_list"] = cidrs
880928
rule["rule_number"] = r.Number
881-
rule["description"] = r.Reason
929+
// Only set description if it's not empty to avoid spurious diffs
930+
if r.Reason != "" {
931+
rule["description"] = r.Reason
932+
}
882933
// Set ICMP fields to 0 for non-ICMP protocols to avoid spurious diffs
883934
rule["icmp_type"] = 0
884935
rule["icmp_code"] = 0
@@ -992,7 +1043,10 @@ func resourceCloudStackNetworkACLRuleRead(d *schema.ResourceData, meta interface
9921043
rule["traffic_type"] = strings.ToLower(r.Traffictype)
9931044
rule["cidr_list"] = cidrs
9941045
rule["rule_number"] = r.Number
995-
rule["description"] = r.Reason
1046+
// Only set description if it's not empty to avoid spurious diffs
1047+
if r.Reason != "" {
1048+
rule["description"] = r.Reason
1049+
}
9961050
if usingRuleset {
9971051
rule["uuid"] = id
9981052
}
@@ -1029,7 +1083,10 @@ func resourceCloudStackNetworkACLRuleRead(d *schema.ResourceData, meta interface
10291083
rule["traffic_type"] = strings.ToLower(r.Traffictype)
10301084
rule["cidr_list"] = cidrs
10311085
rule["rule_number"] = r.Number
1032-
rule["description"] = r.Reason
1086+
// Only set description if it's not empty to avoid spurious diffs
1087+
if r.Reason != "" {
1088+
rule["description"] = r.Reason
1089+
}
10331090
delete(rule, "port") // Remove port field for "all" protocol
10341091
// Set ICMP fields to 0 for non-ICMP protocols to avoid spurious diffs
10351092
rule["icmp_type"] = 0
@@ -1529,7 +1586,17 @@ func performNormalRuleUpdates(d *schema.ResourceData, meta interface{}, cs *clou
15291586

15301587
newRuleMap := newRule.(map[string]interface{})
15311588
log.Printf("[DEBUG] Comparing old rule %+v with new rule %+v", oldRuleMap, newRuleMap)
1532-
if rulesMatch(oldRuleMap, newRuleMap) {
1589+
1590+
// For ruleset rules, match by rule_number only
1591+
// For regular rules, use the full rulesMatch function
1592+
var matched bool
1593+
if isRulesetRule(oldRuleMap) && isRulesetRule(newRuleMap) {
1594+
matched = rulesetRulesMatchByNumber(oldRuleMap, newRuleMap)
1595+
} else {
1596+
matched = rulesMatch(oldRuleMap, newRuleMap)
1597+
}
1598+
1599+
if matched {
15331600
log.Printf("[DEBUG] Found matching new rule for old rule")
15341601

15351602
// Copy UUID from old rule to new rule
@@ -1609,6 +1676,20 @@ func performNormalRuleUpdates(d *schema.ResourceData, meta interface{}, cs *clou
16091676
return nil
16101677
}
16111678

1679+
// rulesetRulesMatchByNumber matches ruleset rules by rule_number only
1680+
// This allows changes to other fields (CIDR, port, protocol, etc.) to be detected as updates
1681+
func rulesetRulesMatchByNumber(oldRule, newRule map[string]interface{}) bool {
1682+
oldRuleNum, oldHasRuleNum := oldRule["rule_number"].(int)
1683+
newRuleNum, newHasRuleNum := newRule["rule_number"].(int)
1684+
1685+
// Both must have rule_number and they must match
1686+
if !oldHasRuleNum || !newHasRuleNum {
1687+
return false
1688+
}
1689+
1690+
return oldRuleNum == newRuleNum
1691+
}
1692+
16121693
func rulesMatch(oldRule, newRule map[string]interface{}) bool {
16131694
oldProtocol := oldRule["protocol"].(string)
16141695
newProtocol := newRule["protocol"].(string)
@@ -1773,8 +1854,11 @@ func updateNetworkACLRule(cs *cloudstack.CloudStackClient, oldRule, newRule map[
17731854
}
17741855
p.SetCidrlist(cidrList)
17751856

1776-
if desc, ok := newRule["description"].(string); ok && desc != "" {
1857+
// Always set description (even if empty) to allow clearing it
1858+
if desc, ok := newRule["description"].(string); ok {
17771859
p.SetReason(desc)
1860+
} else {
1861+
p.SetReason("")
17781862
}
17791863

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

0 commit comments

Comments
 (0)