@@ -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
278278func 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+
16121693func 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