This is an automated email from the ASF dual-hosted git repository.

kiranchavala pushed a commit to branch main
in repository 
https://gitbox.apache.org/repos/asf/cloudstack-terraform-provider.git


The following commit(s) were added to refs/heads/main by this push:
     new 01840be  Add rule number to the terraform state (#245)
01840be is described below

commit 01840be12b9d0a74e8eb4c84aeeea22a7f81f449
Author: Pearl Dsilva <[email protected]>
AuthorDate: Tue Oct 21 00:58:38 2025 -0400

    Add rule number to the terraform state (#245)
    
    * Add rule number to the terraform state
    
    * handle force replacement of existing acl rules in case of migration from 
ports to port
    
    * fix order of rules
    
    * Fix state values set for cks cluster to prevent unnecessary cluster 
replacement
---
 .../resource_cloudstack_kubernetes_cluster.go      |  15 +-
 cloudstack/resource_cloudstack_network_acl_rule.go | 481 ++++++++++++++++++---
 2 files changed, 423 insertions(+), 73 deletions(-)

diff --git a/cloudstack/resource_cloudstack_kubernetes_cluster.go 
b/cloudstack/resource_cloudstack_kubernetes_cluster.go
index 7333c25..18f873a 100644
--- a/cloudstack/resource_cloudstack_kubernetes_cluster.go
+++ b/cloudstack/resource_cloudstack_kubernetes_cluster.go
@@ -393,14 +393,21 @@ func resourceCloudStackKubernetesClusterRead(d 
*schema.ResourceData, meta interf
        d.Set("control_nodes_size", cluster.Controlnodes)
        d.Set("size", cluster.Size)
        d.Set("autoscaling_enabled", cluster.Autoscalingenabled)
-       d.Set("min_size", cluster.Minsize)
-       d.Set("max_size", cluster.Maxsize)
+       if cluster.Autoscalingenabled {
+               d.Set("min_size", cluster.Minsize)
+               d.Set("max_size", cluster.Maxsize)
+       }
        d.Set("keypair", cluster.Keypair)
        d.Set("network_id", cluster.Networkid)
        d.Set("ip_address", cluster.Ipaddress)
        d.Set("state", cluster.State)
-       d.Set("account", cluster.Account)
-       d.Set("domain_id", cluster.Domainid)
+       if _, ok := d.GetOk("account"); ok {
+               d.Set("account", cluster.Account)
+       }
+       if _, ok := d.GetOk("domain_id"); ok {
+               d.Set("domain_id", cluster.Domainid)
+       }
+
        d.Set("etcd_nodes_size", cluster.Etcdnodes)
        d.Set("cni_configuration_id", cluster.Cniconfigurationid)
 
diff --git a/cloudstack/resource_cloudstack_network_acl_rule.go 
b/cloudstack/resource_cloudstack_network_acl_rule.go
index 699c7da..839f3e8 100644
--- a/cloudstack/resource_cloudstack_network_acl_rule.go
+++ b/cloudstack/resource_cloudstack_network_acl_rule.go
@@ -44,6 +44,63 @@ func resourceCloudStackNetworkACLRule() *schema.Resource {
                Importer: &schema.ResourceImporter{
                        State: resourceCloudStackNetworkACLRuleImport,
                },
+               CustomizeDiff: func(ctx context.Context, diff 
*schema.ResourceDiff, meta interface{}) error {
+                       // Force replacement for migration from deprecated 
'ports' to 'port' field
+                       if diff.HasChange("rule") {
+                               oldRules, newRules := diff.GetChange("rule")
+                               oldRulesList := oldRules.([]interface{})
+                               newRulesList := newRules.([]interface{})
+
+                               log.Printf("[DEBUG] CustomizeDiff: checking %d 
old rules -> %d new rules for migration", len(oldRulesList), len(newRulesList))
+
+                               // Check if ANY old rule uses deprecated 
'ports' field
+                               hasDeprecatedPorts := false
+                               for i, oldRule := range oldRulesList {
+                                       oldRuleMap := 
oldRule.(map[string]interface{})
+                                       protocol := 
oldRuleMap["protocol"].(string)
+
+                                       if protocol == "tcp" || protocol == 
"udp" {
+                                               if portsSet, hasPortsSet := 
oldRuleMap["ports"].(*schema.Set); hasPortsSet && portsSet.Len() > 0 {
+                                                       log.Printf("[DEBUG] 
CustomizeDiff: OLD rule %d has deprecated ports field with %d ports: %v", i, 
portsSet.Len(), portsSet.List())
+                                                       hasDeprecatedPorts = 
true
+                                                       break
+                                               }
+                                       }
+                               }
+
+                               // Check if ANY new rule uses new 'port' field
+                               hasNewPortFormat := false
+                               for i, newRule := range newRulesList {
+                                       newRuleMap := 
newRule.(map[string]interface{})
+                                       protocol := 
newRuleMap["protocol"].(string)
+
+                                       if protocol == "tcp" || protocol == 
"udp" {
+                                               if portStr, hasPort := 
newRuleMap["port"].(string); hasPort && portStr != "" {
+                                                       log.Printf("[DEBUG] 
CustomizeDiff: NEW rule %d has port field: %s", i, portStr)
+                                                       hasNewPortFormat = true
+                                                       break
+                                               }
+                                       }
+                               }
+
+                               // Force replacement if migrating from 
deprecated ports to new port format
+                               if hasDeprecatedPorts && hasNewPortFormat {
+                                       log.Printf("[DEBUG] CustomizeDiff: 
MIGRATION DETECTED - old rules use deprecated 'ports', new rules use 'port' - 
FORCING REPLACEMENT")
+                                       diff.ForceNew("rule")
+                                       return nil
+                               }
+
+                               // Also force replacement if old rules have 
deprecated ports but new rules don't use ports at all
+                               if hasDeprecatedPorts && !hasNewPortFormat {
+                                       log.Printf("[DEBUG] CustomizeDiff: 
POTENTIAL MIGRATION - old rules use deprecated 'ports' but new rules don't - 
FORCING REPLACEMENT")
+                                       diff.ForceNew("rule")
+                                       return nil
+                               }
+
+                               log.Printf("[DEBUG] CustomizeDiff: No migration 
detected - hasDeprecatedPorts=%t, hasNewPortFormat=%t", hasDeprecatedPorts, 
hasNewPortFormat)
+                       }
+                       return nil
+               },
 
                Schema: map[string]*schema.Schema{
                        "acl_id": {
@@ -188,6 +245,9 @@ func createNetworkACLRules(d *schema.ResourceData, meta 
interface{}, rules *[]in
        log.Printf("[DEBUG] Creating %d network ACL rules", len(nrs))
        var errs *multierror.Error
 
+       results := make([]map[string]interface{}, len(nrs))
+       var mu sync.Mutex
+
        var wg sync.WaitGroup
        wg.Add(len(nrs))
 
@@ -206,10 +266,12 @@ func createNetworkACLRules(d *schema.ResourceData, meta 
interface{}, rules *[]in
                        err := createNetworkACLRule(d, meta, rule)
                        if err != nil {
                                log.Printf("[ERROR] Failed to create rule #%d: 
%v", index+1, err)
+                               mu.Lock()
                                errs = multierror.Append(errs, fmt.Errorf("rule 
#%d: %v", index+1, err))
+                               mu.Unlock()
                        } else if len(rule["uuids"].(map[string]interface{})) > 
0 {
-                               log.Printf("[DEBUG] Successfully created rule 
#%d, adding to rules list", index+1)
-                               *rules = append(*rules, rule)
+                               log.Printf("[DEBUG] Successfully created rule 
#%d, storing at index %d", index+1, index)
+                               results[index] = rule
                        } else {
                                log.Printf("[WARN] Rule #%d created but has no 
UUIDs", index+1)
                        }
@@ -225,6 +287,13 @@ func createNetworkACLRules(d *schema.ResourceData, meta 
interface{}, rules *[]in
                return err
        }
 
+       for i, result := range results {
+               if result != nil {
+                       *rules = append(*rules, result)
+                       log.Printf("[DEBUG] Added rule #%d to final rules 
list", i+1)
+               }
+       }
+
        log.Printf("[DEBUG] Successfully created all rules")
        return nil
 }
@@ -307,6 +376,12 @@ func createNetworkACLRule(d *schema.ResourceData, meta 
interface{}, rule map[str
 
        // If protocol is TCP or UDP, create the rule (with or without port)
        if rule["protocol"].(string) == "tcp" || rule["protocol"].(string) == 
"udp" {
+               // Check if deprecated ports field is used and reject it
+               if portsSet, hasPortsSet := rule["ports"].(*schema.Set); 
hasPortsSet && portsSet.Len() > 0 {
+                       log.Printf("[ERROR] Attempt to create rule with 
deprecated ports field")
+                       return fmt.Errorf("The 'ports' field is no longer 
supported for creating new rules. Please use the 'port' field with separate 
rules for each port/range.")
+               }
+
                portStr, hasPort := rule["port"].(string)
 
                if hasPort && portStr != "" {
@@ -370,25 +445,26 @@ func createNetworkACLRule(d *schema.ResourceData, meta 
interface{}, rule map[str
 }
 
 func processTCPUDPRule(rule map[string]interface{}, ruleMap 
map[string]*cloudstack.NetworkACL, uuids map[string]interface{}, rules 
*[]interface{}) {
-       // Check for deprecated ports field first (for backward compatibility)
+       // Check for deprecated ports field first (for reading existing state 
during migration)
        ps, hasPortsSet := rule["ports"].(*schema.Set)
        portStr, hasPort := rule["port"].(string)
 
        if hasPortsSet && ps.Len() > 0 {
-               log.Printf("[DEBUG] Processing %d ports for TCP/UDP rule 
(deprecated field)", ps.Len())
+               log.Printf("[DEBUG] Processing deprecated ports field with %d 
ports during state read", ps.Len())
 
-               var ports []interface{}
+               // Process each port in the deprecated ports set during state 
read
                for _, port := range ps.List() {
-                       if processPortForRule(port.(string), rule, ruleMap, 
uuids) {
-                               ports = append(ports, port)
-                               log.Printf("[DEBUG] Added port %s to TCP/UDP 
rule", port.(string))
+                       portStr := port.(string)
+
+                       if processPortForRule(portStr, rule, ruleMap, uuids) {
+                               log.Printf("[DEBUG] Processed deprecated port 
%s during state read", portStr)
                        }
                }
 
-               if len(ports) > 0 {
-                       rule["ports"] = schema.NewSet(schema.HashString, ports)
+               // Only add the rule once with all processed ports
+               if len(uuids) > 0 {
                        *rules = append(*rules, rule)
-                       log.Printf("[DEBUG] Added TCP/UDP rule with deprecated 
ports to state: %+v", rule)
+                       log.Printf("[DEBUG] Added TCP/UDP rule with deprecated 
ports to state during read: %+v", rule)
                }
 
        } else if hasPort && portStr != "" {
@@ -427,6 +503,7 @@ func processTCPUDPRule(rule map[string]interface{}, ruleMap 
map[string]*cloudsta
                rule["protocol"] = r.Protocol
                rule["traffic_type"] = strings.ToLower(r.Traffictype)
                rule["cidr_list"] = cidrs
+               rule["rule_number"] = r.Number
                *rules = append(*rules, rule)
                log.Printf("[DEBUG] Added TCP/UDP rule with no port to state: 
%+v", rule)
        }
@@ -458,6 +535,7 @@ func processPortForRule(portStr string, rule 
map[string]interface{}, ruleMap map
        rule["protocol"] = r.Protocol
        rule["traffic_type"] = strings.ToLower(r.Traffictype)
        rule["cidr_list"] = cidrs
+       rule["rule_number"] = r.Number
 
        return true
 }
@@ -556,6 +634,7 @@ func resourceCloudStackNetworkACLRuleRead(d 
*schema.ResourceData, meta interface
                                rule["icmp_code"] = r.Icmpcode
                                rule["traffic_type"] = 
strings.ToLower(r.Traffictype)
                                rule["cidr_list"] = cidrs
+                               rule["rule_number"] = r.Number
                                rules = append(rules, rule)
                                log.Printf("[DEBUG] Added ICMP rule to state: 
%+v", rule)
                        }
@@ -589,6 +668,7 @@ func resourceCloudStackNetworkACLRuleRead(d 
*schema.ResourceData, meta interface
                                rule["protocol"] = r.Protocol
                                rule["traffic_type"] = 
strings.ToLower(r.Traffictype)
                                rule["cidr_list"] = cidrs
+                               rule["rule_number"] = r.Number
                                rules = append(rules, rule)
                                log.Printf("[DEBUG] Added ALL rule to state: 
%+v", rule)
                        }
@@ -648,6 +728,17 @@ func resourceCloudStackNetworkACLRuleUpdate(d 
*schema.ResourceData, meta interfa
                oldRules := o.([]interface{})
                newRules := n.([]interface{})
 
+               log.Printf("[DEBUG] Rule list changed: %d old rules -> %d new 
rules", len(oldRules), len(newRules))
+
+               // Check for migration from deprecated 'ports' to 'port' field
+               migrationDetected := isPortsMigration(oldRules, newRules)
+
+               if migrationDetected {
+                       log.Printf("[DEBUG] Migration detected - performing 
complete rule replacement")
+
+                       return performPortsMigration(d, meta, oldRules, 
newRules)
+               }
+
                log.Printf("[DEBUG] Rule list changed, performing efficient 
updates")
                err := updateNetworkACLRules(d, meta, oldRules, newRules)
                if err != nil {
@@ -760,13 +851,14 @@ func verifyNetworkACLRuleParams(d *schema.ResourceData, 
rule map[string]interfac
                // No additional test are needed
                log.Printf("[DEBUG] Protocol 'all' validated")
        case "tcp", "udp":
-               // Check if deprecated ports field is used (not allowed for new 
configurations)
+               // The deprecated 'ports' field is no longer supported in any 
scenario
                portsSet, hasPortsSet := rule["ports"].(*schema.Set)
                portStr, hasPort := rule["port"].(string)
 
+               // Block deprecated ports field completely
                if hasPortsSet && portsSet.Len() > 0 {
-                       log.Printf("[ERROR] Deprecated ports field used in new 
configuration")
-                       return fmt.Errorf("The 'ports' field is deprecated. Use 
'port' instead for new configurations.")
+                       log.Printf("[ERROR] Attempt to use deprecated ports 
field")
+                       return fmt.Errorf("The 'ports' field is no longer 
supported. Please use the 'port' field instead.")
                }
 
                // Validate the new port field if used
@@ -850,89 +942,142 @@ func updateNetworkACLRules(d *schema.ResourceData, meta 
interface{}, oldRules, n
        cs := meta.(*cloudstack.CloudStackClient)
        log.Printf("[DEBUG] Updating ACL rules: %d old rules, %d new rules", 
len(oldRules), len(newRules))
 
-       oldRuleMap := make(map[string]map[string]interface{})
-       newRuleMap := make(map[string]map[string]interface{})
+       log.Printf("[DEBUG] Performing normal rule updates")
+       return performNormalRuleUpdates(d, meta, cs, oldRules, newRules)
+}
+
+func performNormalRuleUpdates(d *schema.ResourceData, meta interface{}, cs 
*cloudstack.CloudStackClient, oldRules, newRules []interface{}) error {
+       rulesToUpdate := make(map[string]map[string]interface{}) // UUID -> new 
rule mapping
+       rulesToDelete := make([]map[string]interface{}, 0)
+       rulesToCreate := make([]map[string]interface{}, 0)
+
+       // Track which new rules match existing old rules
+       usedNewRules := make(map[int]bool)
+
+       // For each old rule, try to find a matching new rule
+       for _, oldRule := range oldRules {
+               oldRuleMap := oldRule.(map[string]interface{})
+               foundMatch := false
+
+               for newIdx, newRule := range newRules {
+                       if usedNewRules[newIdx] {
+                               continue
+                       }
+
+                       newRuleMap := newRule.(map[string]interface{})
+                       log.Printf("[DEBUG] Comparing old rule %+v with new 
rule %+v", oldRuleMap, newRuleMap)
+                       if rulesMatch(oldRuleMap, newRuleMap) {
+                               log.Printf("[DEBUG] Found matching new rule for 
old rule")
+
+                               if oldUUIDs, ok := 
oldRuleMap["uuids"].(map[string]interface{}); ok {
+                                       newRuleMap["uuids"] = oldUUIDs
+                               }
 
-       for _, rule := range oldRules {
-               ruleMap := rule.(map[string]interface{})
-               key := createRuleKey(ruleMap)
-               oldRuleMap[key] = ruleMap
-               log.Printf("[DEBUG] Old rule key: %s", key)
+                               if ruleNeedsUpdate(oldRuleMap, newRuleMap) {
+                                       log.Printf("[DEBUG] Rule needs 
updating")
+                                       if uuids, ok := 
oldRuleMap["uuids"].(map[string]interface{}); ok {
+                                               for _, uuid := range uuids {
+                                                       if uuid != nil {
+                                                               
rulesToUpdate[uuid.(string)] = newRuleMap
+                                                               break
+                                                       }
+                                               }
+                                       }
+                               }
+
+                               usedNewRules[newIdx] = true
+                               foundMatch = true
+                               break
+                       }
+               }
+
+               if !foundMatch {
+                       log.Printf("[DEBUG] Old rule has no match, will be 
deleted")
+                       rulesToDelete = append(rulesToDelete, oldRuleMap)
+               }
        }
 
-       for _, rule := range newRules {
-               ruleMap := rule.(map[string]interface{})
-               key := createRuleKey(ruleMap)
-               newRuleMap[key] = ruleMap
-               log.Printf("[DEBUG] New rule key: %s", key)
+       for newIdx, newRule := range newRules {
+               if !usedNewRules[newIdx] {
+                       newRuleMap := newRule.(map[string]interface{})
+                       log.Printf("[DEBUG] New rule has no match, will be 
created")
+                       rulesToCreate = append(rulesToCreate, newRuleMap)
+               }
        }
 
-       for key, oldRule := range oldRuleMap {
-               if _, exists := newRuleMap[key]; !exists {
-                       log.Printf("[DEBUG] Deleting rule: %s", key)
-                       err := deleteNetworkACLRule(d, meta, oldRule)
-                       if err != nil {
-                               return fmt.Errorf("failed to delete rule %s: 
%v", key, err)
-                       }
+       for _, ruleToDelete := range rulesToDelete {
+               log.Printf("[DEBUG] Deleting unmatched old rule")
+               err := deleteNetworkACLRule(d, meta, ruleToDelete)
+               if err != nil {
+                       return fmt.Errorf("failed to delete old rule: %v", err)
                }
        }
 
-       var rulesToCreate []interface{}
-       for key, newRule := range newRuleMap {
-               if _, exists := oldRuleMap[key]; !exists {
-                       log.Printf("[DEBUG] Creating new rule: %s", key)
-                       rulesToCreate = append(rulesToCreate, newRule)
+       for uuid, newRule := range rulesToUpdate {
+               log.Printf("[DEBUG] Updating rule with UUID %s", uuid)
+
+               tempOldRule := make(map[string]interface{})
+               tempOldRule["uuids"] = map[string]interface{}{"update": uuid}
+
+               err := updateNetworkACLRule(cs, tempOldRule, newRule)
+               if err != nil {
+                       return fmt.Errorf("failed to update rule UUID %s: %v", 
uuid, err)
                }
        }
 
        if len(rulesToCreate) > 0 {
+               log.Printf("[DEBUG] Creating %d new rules", len(rulesToCreate))
+
                var createdRules []interface{}
-               err := createNetworkACLRules(d, meta, &createdRules, 
rulesToCreate)
-               if err != nil {
-                       return fmt.Errorf("failed to create new rules: %v", err)
+               var rulesToCreateInterface []interface{}
+               for _, rule := range rulesToCreate {
+                       rulesToCreateInterface = append(rulesToCreateInterface, 
rule)
                }
-       }
 
-       for key, newRule := range newRuleMap {
-               if oldRule, exists := oldRuleMap[key]; exists {
-                       if ruleNeedsUpdate(oldRule, newRule) {
-                               log.Printf("[DEBUG] Updating rule: %s", key)
-                               err := updateNetworkACLRule(cs, oldRule, 
newRule)
-                               if err != nil {
-                                       return fmt.Errorf("failed to update 
rule %s: %v", key, err)
-                               }
-                       }
+               err := createNetworkACLRules(d, meta, &createdRules, 
rulesToCreateInterface)
+               if err != nil {
+                       return fmt.Errorf("failed to create new rules: %v", err)
                }
        }
 
        return nil
 }
 
-func createRuleKey(rule map[string]interface{}) string {
-       protocol := rule["protocol"].(string)
-       trafficType := rule["traffic_type"].(string)
-
-       if protocol == "icmp" {
-               icmpType := rule["icmp_type"].(int)
-               icmpCode := rule["icmp_code"].(int)
-               return fmt.Sprintf("%s-%s-icmp-%d-%d", protocol, trafficType, 
icmpType, icmpCode)
+func rulesMatch(oldRule, newRule map[string]interface{}) bool {
+       if oldRule["protocol"].(string) != newRule["protocol"].(string) ||
+               oldRule["traffic_type"].(string) != 
newRule["traffic_type"].(string) ||
+               oldRule["action"].(string) != newRule["action"].(string) {
+               return false
        }
 
-       if protocol == "all" {
-               return fmt.Sprintf("%s-%s-all", protocol, trafficType)
-       }
+       protocol := newRule["protocol"].(string)
 
        if protocol == "tcp" || protocol == "udp" {
-               portStr, hasPort := rule["port"].(string)
-               if hasPort && portStr != "" {
-                       return fmt.Sprintf("%s-%s-port-%s", protocol, 
trafficType, portStr)
-               } else {
-                       return fmt.Sprintf("%s-%s-noport", protocol, 
trafficType)
+               oldPort, oldHasPort := oldRule["port"].(string)
+               newPort, newHasPort := newRule["port"].(string)
+
+               if oldHasPort && newHasPort {
+                       return oldPort == newPort
                }
+
+               if oldHasPort != newHasPort {
+                       return false
+               }
+
+               return true
        }
 
-       // For numeric protocols
-       return fmt.Sprintf("%s-%s", protocol, trafficType)
+       switch protocol {
+       case "icmp":
+               return oldRule["icmp_type"].(int) == newRule["icmp_type"].(int) 
&&
+                       oldRule["icmp_code"].(int) == newRule["icmp_code"].(int)
+
+       case "all":
+               return true
+
+       default:
+               return true
+       }
 }
 
 func ruleNeedsUpdate(oldRule, newRule map[string]interface{}) bool {
@@ -1090,3 +1235,201 @@ func updateNetworkACLRule(cs 
*cloudstack.CloudStackClient, oldRule, newRule map[
 
        return nil
 }
+
+func hasDeprecatedPortsInOldRules(oldRules []interface{}) bool {
+       for _, oldRule := range oldRules {
+               oldRuleMap := oldRule.(map[string]interface{})
+               protocol := oldRuleMap["protocol"].(string)
+
+               if protocol == "tcp" || protocol == "udp" {
+                       if portsSet, hasPortsSet := 
oldRuleMap["ports"].(*schema.Set); hasPortsSet && portsSet.Len() > 0 {
+                               return true
+                       }
+               }
+       }
+       return false
+}
+
+func containsMixedPortFields(oldRules, newRules []interface{}) bool {
+       hasDeprecatedInOld := hasDeprecatedPortsInOldRules(oldRules)
+       hasNewInNew := hasPortFieldInNewRules(newRules)
+
+       hasDeprecatedInNew := hasDeprecatedPortsInOldRules(newRules)
+
+       // Migration detected if:
+       // 1. Old rules have deprecated ports OR
+       // 2. We have a mix of deprecated and new port fields anywhere
+       return hasDeprecatedInOld || (hasDeprecatedInNew && hasNewInNew)
+}
+
+// Checks if any new rule uses the new 'port' field
+func hasPortFieldInNewRules(newRules []interface{}) bool {
+       for _, newRule := range newRules {
+               newRuleMap := newRule.(map[string]interface{})
+               protocol := newRuleMap["protocol"].(string)
+
+               if protocol == "tcp" || protocol == "udp" {
+                       if portStr, hasPort := newRuleMap["port"].(string); 
hasPort && portStr != "" {
+                               return true
+                       }
+               }
+       }
+       return false
+}
+
+// Detects if we're migrating from deprecated 'ports' to 'port' field
+func isPortsMigration(oldRules, newRules []interface{}) bool {
+       log.Printf("[DEBUG] Migration detection: checking %d old rules and %d 
new rules", len(oldRules), len(newRules))
+
+       hasDeprecatedPorts := false
+       hasNewPortFormat := false
+
+       for i, oldRule := range oldRules {
+               oldRuleMap := oldRule.(map[string]interface{})
+               protocol := oldRuleMap["protocol"].(string)
+               log.Printf("[DEBUG] Migration detection: old rule %d has 
protocol %s", i, protocol)
+
+               if protocol == "tcp" || protocol == "udp" {
+                       if portsSet, hasPortsSet := 
oldRuleMap["ports"].(*schema.Set); hasPortsSet && portsSet.Len() > 0 {
+                               log.Printf("[DEBUG] Migration detection: old 
rule %d has deprecated ports field with %d ports", i, portsSet.Len())
+                               hasDeprecatedPorts = true
+                       }
+
+                       oldPort, oldHasPort := oldRuleMap["port"].(string)
+                       if !oldHasPort || oldPort == "" {
+                               log.Printf("[DEBUG] Migration detection: old 
rule %d has no port field, checking if new rules use port field", i)
+                               for j, newRule := range newRules {
+                                       newRuleMap := 
newRule.(map[string]interface{})
+                                       newProtocol := 
newRuleMap["protocol"].(string)
+                                       if newProtocol == protocol {
+                                               if newPortStr, newHasPort := 
newRuleMap["port"].(string); newHasPort && newPortStr != "" {
+                                                       log.Printf("[DEBUG] 
Migration detection: new rule %d has port field '%s' while old rule had none - 
potential migration", j, newPortStr)
+                                                       hasDeprecatedPorts = 
true
+                                                       break
+                                               }
+                                       }
+                               }
+                       }
+               }
+       }
+
+       for i, newRule := range newRules {
+               newRuleMap := newRule.(map[string]interface{})
+               protocol := newRuleMap["protocol"].(string)
+               log.Printf("[DEBUG] Migration detection: new rule %d has 
protocol %s", i, protocol)
+
+               if protocol == "tcp" || protocol == "udp" {
+                       if portStr, hasPort := newRuleMap["port"].(string); 
hasPort && portStr != "" {
+                               log.Printf("[DEBUG] Migration detection: new 
rule %d has port field with value: %s", i, portStr)
+                               hasNewPortFormat = true
+                       }
+
+                       if portsSet, hasPortsSet := 
newRuleMap["ports"].(*schema.Set); hasPortsSet && portsSet.Len() > 0 {
+                               log.Printf("[DEBUG] Migration detection: new 
rule %d still has deprecated ports, not a migration", i)
+                               return false
+                       }
+               }
+       }
+
+       migrationDetected := hasDeprecatedPorts && hasNewPortFormat
+       log.Printf("[DEBUG] Migration detection result: hasDeprecatedPorts=%t, 
hasNewPortFormat=%t, migrationDetected=%t", hasDeprecatedPorts, 
hasNewPortFormat, migrationDetected)
+
+       // Migration is detected if:
+       // 1. We have old rules with deprecated ports OR no port field AND
+       // 2. We have new rules with port format (no deprecated ports)
+       return migrationDetected
+}
+
+func performPortsMigration(d *schema.ResourceData, meta interface{}, oldRules, 
newRules []interface{}) error {
+       log.Printf("[DEBUG] Starting ports->port migration")
+       cs := meta.(*cloudstack.CloudStackClient)
+
+       // Build a map of all UUIDs that need to be deleted
+       uuidsToDelete := make([]string, 0)
+
+       for _, oldRule := range oldRules {
+               oldRuleMap := oldRule.(map[string]interface{})
+               uuids, ok := oldRuleMap["uuids"].(map[string]interface{})
+               if !ok {
+                       continue
+               }
+
+               for key, uuid := range uuids {
+                       if key != "%" && uuid != nil {
+                               uuidStr := uuid.(string)
+                               if uuidStr != "" {
+                                       uuidsToDelete = append(uuidsToDelete, 
uuidStr)
+                               }
+                       }
+               }
+       }
+
+       log.Printf("[DEBUG] Total UUIDs to delete: %d", len(uuidsToDelete))
+
+       // Delete all old rules by UUID and wait for completion
+       for _, uuidToDelete := range uuidsToDelete {
+               p := cs.NetworkACL.NewDeleteNetworkACLParams(uuidToDelete)
+               _, err := cs.NetworkACL.DeleteNetworkACL(p)
+
+               if err != nil {
+                       if strings.Contains(err.Error(), fmt.Sprintf(
+                               "Invalid parameter id value=%s due to incorrect 
long value format, "+
+                                       "or entity does not exist", 
uuidToDelete)) {
+                               continue
+                       }
+
+                       return fmt.Errorf("failed to delete old rule UUID %s 
during migration: %v", uuidToDelete, err)
+               }
+       }
+
+       // Wait a moment for CloudStack to process the deletions
+       if len(uuidsToDelete) > 0 {
+               log.Printf("[DEBUG] Waiting for CloudStack to process %d rule 
deletions", len(uuidsToDelete))
+               time.Sleep(3 * time.Second)
+
+               for _, uuidToCheck := range uuidsToDelete {
+                       listParams := cs.NetworkACL.NewListNetworkACLsParams()
+                       listParams.SetId(uuidToCheck)
+
+                       listResp, err := 
cs.NetworkACL.ListNetworkACLs(listParams)
+                       if err == nil && listResp.Count > 0 {
+                               time.Sleep(2 * time.Second)
+                               break
+                       }
+               }
+       }
+
+       // Create all new rules with fresh UUIDs
+       if len(newRules) > 0 {
+               log.Printf("[DEBUG] Creating %d new rules with port field", 
len(newRules))
+
+               var rulesToCreate []interface{}
+               for _, newRule := range newRules {
+                       newRuleMap := newRule.(map[string]interface{})
+
+                       cleanRule := make(map[string]interface{})
+                       for k, v := range newRuleMap {
+                               cleanRule[k] = v
+                       }
+                       cleanRule["uuids"] = make(map[string]interface{})
+
+                       rulesToCreate = append(rulesToCreate, cleanRule)
+               }
+
+               var createdRules []interface{}
+               err := createNetworkACLRules(d, meta, &createdRules, 
rulesToCreate)
+               if err != nil {
+                       return fmt.Errorf("failed to create new rules during 
migration: %v", err)
+               }
+
+               log.Printf("[DEBUG] Successfully created %d new rules during 
migration", len(createdRules))
+
+               if err := d.Set("rule", createdRules); err != nil {
+                       return fmt.Errorf("failed to update state with migrated 
rules: %v", err)
+               }
+               log.Printf("[DEBUG] Updated Terraform state with %d migrated 
rules", len(createdRules))
+       }
+
+       log.Printf("[DEBUG] Ports->port migration completed successfully")
+       return nil
+}

Reply via email to