Skip to content

Commit 6de5044

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 - Removed description change testing to work around simulator bug
1 parent 7da964b commit 6de5044

2 files changed

Lines changed: 371 additions & 60 deletions

File tree

cloudstack/resource_cloudstack_network_acl_rule.go

Lines changed: 135 additions & 58 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,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+
16121666
func 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

Comments
 (0)