2020package cloudstack
2121
2222import (
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,17 +859,31 @@ 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
881- rule ["description" ] = r .Reason
883+ // Only set description if it's not empty to avoid spurious diffs
884+ if r .Reason != "" {
885+ rule ["description" ] = r .Reason
886+ }
882887 // Set ICMP fields to 0 for non-ICMP protocols to avoid spurious diffs
883888 rule ["icmp_type" ] = 0
884889 rule ["icmp_code" ] = 0
@@ -978,10 +983,19 @@ func resourceCloudStackNetworkACLRuleRead(d *schema.ResourceData, meta interface
978983 // Delete the known rule so only unknown rules remain in the ruleMap
979984 delete (ruleMap , id )
980985
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 )
986+ // Create a list or set with all CIDR's depending on field type
987+ if usingRuleset {
988+ cidrs := & schema.Set {F : schema .HashString }
989+ for _ , cidr := range strings .Split (r .Cidrlist , "," ) {
990+ cidrs .Add (cidr )
991+ }
992+ rule ["cidr_list" ] = cidrs
993+ } else {
994+ var cidrs []interface {}
995+ for _ , cidr := range strings .Split (r .Cidrlist , "," ) {
996+ cidrs = append (cidrs , cidr )
997+ }
998+ rule ["cidr_list" ] = cidrs
985999 }
9861000
9871001 // Update the values
@@ -990,9 +1004,11 @@ func resourceCloudStackNetworkACLRuleRead(d *schema.ResourceData, meta interface
9901004 rule ["icmp_type" ] = r .Icmptype
9911005 rule ["icmp_code" ] = r .Icmpcode
9921006 rule ["traffic_type" ] = strings .ToLower (r .Traffictype )
993- rule ["cidr_list" ] = cidrs
9941007 rule ["rule_number" ] = r .Number
995- rule ["description" ] = r .Reason
1008+ // Only set description if it's not empty to avoid spurious diffs
1009+ if r .Reason != "" {
1010+ rule ["description" ] = r .Reason
1011+ }
9961012 if usingRuleset {
9971013 rule ["uuid" ] = id
9981014 }
@@ -1017,19 +1033,30 @@ func resourceCloudStackNetworkACLRuleRead(d *schema.ResourceData, meta interface
10171033 // Delete the known rule so only unknown rules remain in the ruleMap
10181034 delete (ruleMap , id )
10191035
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 )
1036+ // Create a list or set with all CIDR's depending on field type
1037+ if usingRuleset {
1038+ cidrs := & schema.Set {F : schema .HashString }
1039+ for _ , cidr := range strings .Split (r .Cidrlist , "," ) {
1040+ cidrs .Add (cidr )
1041+ }
1042+ rule ["cidr_list" ] = cidrs
1043+ } else {
1044+ var cidrs []interface {}
1045+ for _ , cidr := range strings .Split (r .Cidrlist , "," ) {
1046+ cidrs = append (cidrs , cidr )
1047+ }
1048+ rule ["cidr_list" ] = cidrs
10241049 }
10251050
10261051 // Update the values
10271052 rule ["action" ] = strings .ToLower (r .Action )
10281053 rule ["protocol" ] = r .Protocol
10291054 rule ["traffic_type" ] = strings .ToLower (r .Traffictype )
1030- rule ["cidr_list" ] = cidrs
10311055 rule ["rule_number" ] = r .Number
1032- rule ["description" ] = r .Reason
1056+ // Only set description if it's not empty to avoid spurious diffs
1057+ if r .Reason != "" {
1058+ rule ["description" ] = r .Reason
1059+ }
10331060 delete (rule , "port" ) // Remove port field for "all" protocol
10341061 // Set ICMP fields to 0 for non-ICMP protocols to avoid spurious diffs
10351062 rule ["icmp_type" ] = 0
@@ -1063,15 +1090,15 @@ func resourceCloudStackNetworkACLRuleRead(d *schema.ResourceData, meta interface
10631090 outOfBandRuleNumber := maxRuleNumber + 1
10641091
10651092 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-
10701093 // Make a placeholder rule to hold the unknown UUID
10711094 // Format differs between 'rule' and 'ruleset'
10721095 var rule map [string ]interface {}
10731096 if usingRuleset {
10741097 // For ruleset: use 'uuid' string and include rule_number
1098+ // cidr_list is a TypeSet for ruleset
1099+ cidrs := & schema.Set {F : schema .HashString }
1100+ cidrs .Add (uuid )
1101+
10751102 // Include all fields with defaults to avoid spurious diffs
10761103 rule = map [string ]interface {}{
10771104 "cidr_list" : cidrs ,
@@ -1088,6 +1115,8 @@ func resourceCloudStackNetworkACLRuleRead(d *schema.ResourceData, meta interface
10881115 outOfBandRuleNumber ++
10891116 } else {
10901117 // For rule: use 'uuids' map
1118+ // cidr_list is a TypeList for rule
1119+ cidrs := []interface {}{uuid }
10911120 rule = map [string ]interface {}{
10921121 "cidr_list" : cidrs ,
10931122 "protocol" : uuid ,
@@ -1107,8 +1136,9 @@ func resourceCloudStackNetworkACLRuleRead(d *schema.ResourceData, meta interface
11071136 // For TypeSet, we need to be very careful about state updates
11081137 // The SDK has issues with properly clearing removed elements from TypeSet
11091138 // 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
1139+ // Use schema.HashResource to match the default hash function
1140+ rulesetResource := resourceCloudStackNetworkACLRule ().Schema ["ruleset" ].Elem .(* schema.Resource )
1141+ hashFunc := schema .HashResource (rulesetResource )
11121142
11131143 // First, clear the ruleset completely
11141144 emptySet := schema .NewSet (hashFunc , []interface {}{})
@@ -1529,7 +1559,17 @@ func performNormalRuleUpdates(d *schema.ResourceData, meta interface{}, cs *clou
15291559
15301560 newRuleMap := newRule .(map [string ]interface {})
15311561 log .Printf ("[DEBUG] Comparing old rule %+v with new rule %+v" , oldRuleMap , newRuleMap )
1532- if rulesMatch (oldRuleMap , newRuleMap ) {
1562+
1563+ // For ruleset rules, match by rule_number only
1564+ // For regular rules, use the full rulesMatch function
1565+ var matched bool
1566+ if isRulesetRule (oldRuleMap ) && isRulesetRule (newRuleMap ) {
1567+ matched = rulesetRulesMatchByNumber (oldRuleMap , newRuleMap )
1568+ } else {
1569+ matched = rulesMatch (oldRuleMap , newRuleMap )
1570+ }
1571+
1572+ if matched {
15331573 log .Printf ("[DEBUG] Found matching new rule for old rule" )
15341574
15351575 // Copy UUID from old rule to new rule
@@ -1609,6 +1649,20 @@ func performNormalRuleUpdates(d *schema.ResourceData, meta interface{}, cs *clou
16091649 return nil
16101650}
16111651
1652+ // rulesetRulesMatchByNumber matches ruleset rules by rule_number only
1653+ // This allows changes to other fields (CIDR, port, protocol, etc.) to be detected as updates
1654+ func rulesetRulesMatchByNumber (oldRule , newRule map [string ]interface {}) bool {
1655+ oldRuleNum , oldHasRuleNum := oldRule ["rule_number" ].(int )
1656+ newRuleNum , newHasRuleNum := newRule ["rule_number" ].(int )
1657+
1658+ // Both must have rule_number and they must match
1659+ if ! oldHasRuleNum || ! newHasRuleNum {
1660+ return false
1661+ }
1662+
1663+ return oldRuleNum == newRuleNum
1664+ }
1665+
16121666func rulesMatch (oldRule , newRule map [string ]interface {}) bool {
16131667 oldProtocol := oldRule ["protocol" ].(string )
16141668 newProtocol := newRule ["protocol" ].(string )
@@ -1725,20 +1779,34 @@ func ruleNeedsUpdate(oldRule, newRule map[string]interface{}) bool {
17251779 }
17261780 }
17271781
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
1782+ // Handle cidr_list comparison - can be TypeSet (ruleset) or TypeList (rule)
1783+ var oldCidrStrs , newCidrStrs []string
1784+
1785+ // Extract old CIDRs
1786+ if oldSet , ok := oldRule ["cidr_list" ].(* schema.Set ); ok {
1787+ for _ , cidr := range oldSet .List () {
1788+ oldCidrStrs = append (oldCidrStrs , cidr .(string ))
1789+ }
1790+ } else if oldList , ok := oldRule ["cidr_list" ].([]interface {}); ok {
1791+ for _ , cidr := range oldList {
1792+ oldCidrStrs = append (oldCidrStrs , cidr .(string ))
1793+ }
17331794 }
17341795
1735- oldCidrStrs := make ([]string , len (oldCidrs ))
1736- newCidrStrs := make ([]string , len (newCidrs ))
1737- for i , cidr := range oldCidrs {
1738- oldCidrStrs [i ] = cidr .(string )
1796+ // Extract new CIDRs
1797+ if newSet , ok := newRule ["cidr_list" ].(* schema.Set ); ok {
1798+ for _ , cidr := range newSet .List () {
1799+ newCidrStrs = append (newCidrStrs , cidr .(string ))
1800+ }
1801+ } else if newList , ok := newRule ["cidr_list" ].([]interface {}); ok {
1802+ for _ , cidr := range newList {
1803+ newCidrStrs = append (newCidrStrs , cidr .(string ))
1804+ }
17391805 }
1740- for i , cidr := range newCidrs {
1741- newCidrStrs [i ] = cidr .(string )
1806+
1807+ if len (oldCidrStrs ) != len (newCidrStrs ) {
1808+ log .Printf ("[DEBUG] CIDR list length changed: %d -> %d" , len (oldCidrStrs ), len (newCidrStrs ))
1809+ return true
17421810 }
17431811
17441812 sort .Strings (oldCidrStrs )
@@ -1768,13 +1836,22 @@ func updateNetworkACLRule(cs *cloudstack.CloudStackClient, oldRule, newRule map[
17681836 p .SetAction (newRule ["action" ].(string ))
17691837
17701838 var cidrList []string
1771- for _ , cidr := range newRule ["cidr_list" ].([]interface {}) {
1772- cidrList = append (cidrList , cidr .(string ))
1839+ if cidrSet , ok := newRule ["cidr_list" ].(* schema.Set ); ok {
1840+ for _ , cidr := range cidrSet .List () {
1841+ cidrList = append (cidrList , cidr .(string ))
1842+ }
1843+ } else {
1844+ for _ , cidr := range newRule ["cidr_list" ].([]interface {}) {
1845+ cidrList = append (cidrList , cidr .(string ))
1846+ }
17731847 }
17741848 p .SetCidrlist (cidrList )
17751849
1776- if desc , ok := newRule ["description" ].(string ); ok && desc != "" {
1850+ // Always set description (even if empty) to allow clearing it
1851+ if desc , ok := newRule ["description" ].(string ); ok {
17771852 p .SetReason (desc )
1853+ } else {
1854+ p .SetReason ("" )
17781855 }
17791856
17801857 p .SetProtocol (newRule ["protocol" ].(string ))
0 commit comments