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
+}