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

sudo87 pushed a commit to branch fix/role-permission-reorder-drift
in repository 
https://gitbox.apache.org/repos/asf/cloudstack-terraform-provider.git

commit e1f2374757758e9b254f498c069ce8978e3c16dd
Author: Manoj Kumar <[email protected]>
AuthorDate: Fri Aug 28 17:30:58 2026 +0530

    Fix role_permission reorder collision and authoritative drift detection
    
    Reordering permission list entries deleted and recreated them one at a
    time in the same pass, which could collide with a not-yet-processed
    entry still holding the target rule and fail with API error 531.
    reconcileCloudStackRolePermissions now deletes every stale permission
    first, then creates/updates the desired set in a second pass.
    
    authoritative = true also never surfaced drift: Read only considered
    permissions declared in config, so an out-of-band permission produced
    no diff and Update (where deletion happens) never ran. Read now
    appends undeclared permissions to state when authoritative is true so
    they show up as a plan diff.
    
    Non-authoritative shrink-deletion previously read its "previously
    managed" set from the same permission list Read populates, so a
    permission surfaced only for authoritative drift visibility could get
    misread as user-declared-then-removed and deleted even after flipping
    authoritative back to false. Track genuinely-managed IDs in a separate
    managed_ids field so drift visibility and shrink-deletion don't share
    state.
---
 cloudstack/resource_cloudstack_role_permission.go | 111 +++++++++++++++-------
 1 file changed, 78 insertions(+), 33 deletions(-)

diff --git a/cloudstack/resource_cloudstack_role_permission.go 
b/cloudstack/resource_cloudstack_role_permission.go
index 1769f8d..a0760d9 100644
--- a/cloudstack/resource_cloudstack_role_permission.go
+++ b/cloudstack/resource_cloudstack_role_permission.go
@@ -57,6 +57,11 @@ func resourceCloudStackRolePermission() *schema.Resource {
                                Default:     false,
                                Description: "Whether permissions not declared 
in this resource should be deleted.",
                        },
+                       "managed_ids": {
+                               Type:     schema.TypeSet,
+                               Computed: true,
+                               Elem:     &schema.Schema{Type: 
schema.TypeString},
+                       },
                        "permission": {
                                Type:        schema.TypeList,
                                Optional:    true,
@@ -99,11 +104,15 @@ func resourceCloudStackRolePermissionCreate(d 
*schema.ResourceData, meta interfa
        roleLock.Lock()
        defer roleLock.Unlock()
 
-       if err := reconcileCloudStackRolePermissions(d, meta, nil); err != nil {
-               return err
+       reconcileErr := reconcileCloudStackRolePermissions(d, meta, nil)
+       if readErr := resourceCloudStackRolePermissionRead(d, meta); readErr != 
nil {
+               if reconcileErr != nil {
+                       return reconcileErr
+               }
+               return readErr
        }
 
-       return resourceCloudStackRolePermissionRead(d, meta)
+       return reconcileErr
 }
 
 func resourceCloudStackRolePermissionRead(d *schema.ResourceData, meta 
interface{}) error {
@@ -150,6 +159,28 @@ func resourceCloudStackRolePermissionRead(d 
*schema.ResourceData, meta interface
                }))
        }
 
+       managedIDs := make([]interface{}, 0, len(used))
+       for id := range used {
+               managedIDs = append(managedIDs, id)
+       }
+       if err := d.Set("managed_ids", managedIDs); err != nil {
+               return fmt.Errorf("Error setting Role Permission managed IDs: 
%s", err)
+       }
+
+       if d.Get("authoritative").(bool) {
+               for _, rp := range rolePermissions {
+                       if used[rp.Id] {
+                               continue
+                       }
+                       readPermissions = append(readPermissions, 
rolePermissionState(rolePermissionSpec{
+                               ID:          rp.Id,
+                               Rule:        rp.Rule,
+                               Permission:  rp.Permission,
+                               Description: rp.Description,
+                       }))
+               }
+       }
+
        if err := d.Set("permission", readPermissions); err != nil {
                return fmt.Errorf("Error setting Role Permissions: %s", err)
        }
@@ -168,17 +199,23 @@ func resourceCloudStackRolePermissionUpdate(d 
*schema.ResourceData, meta interfa
        roleLock.Lock()
        defer roleLock.Unlock()
 
-       var oldPermissions []rolePermissionSpec
-       if d.HasChange("permission") {
-               oldRaw, _ := d.GetChange("permission")
-               oldPermissions = rolePermissionSpecs(oldRaw.([]interface{}))
+       oldManagedIDs := make(map[string]bool)
+       if d.HasChange("managed_ids") {
+               oldRaw, _ := d.GetChange("managed_ids")
+               for _, id := range oldRaw.(*schema.Set).List() {
+                       oldManagedIDs[id.(string)] = true
+               }
        }
 
-       if err := reconcileCloudStackRolePermissions(d, meta, oldPermissions); 
err != nil {
-               return err
+       reconcileErr := reconcileCloudStackRolePermissions(d, meta, 
oldManagedIDs)
+       if readErr := resourceCloudStackRolePermissionRead(d, meta); readErr != 
nil {
+               if reconcileErr != nil {
+                       return reconcileErr
+               }
+               return readErr
        }
 
-       return resourceCloudStackRolePermissionRead(d, meta)
+       return reconcileErr
 }
 
 func resourceCloudStackRolePermissionDelete(d *schema.ResourceData, meta 
interface{}) error {
@@ -228,7 +265,7 @@ func resourceCloudStackRolePermissionDelete(d 
*schema.ResourceData, meta interfa
        return nil
 }
 
-func reconcileCloudStackRolePermissions(d *schema.ResourceData, meta 
interface{}, oldPermissions []rolePermissionSpec) error {
+func reconcileCloudStackRolePermissions(d *schema.ResourceData, meta 
interface{}, oldManagedIDs map[string]bool) error {
        cs := meta.(*cloudstack.CloudStackClient)
        roleID := d.Get("role_id").(string)
 
@@ -242,28 +279,44 @@ func reconcileCloudStackRolePermissions(d 
*schema.ResourceData, meta interface{}
                rolePermissionsByID[rp.Id] = rp
        }
 
+       desiredPermissions := 
rolePermissionSpecs(d.Get("permission").([]interface{}))
+
        used := make(map[string]bool)
        deleted := make(map[string]bool)
-       managedIDs := make([]string, 0)
-       managedIDSet := make(map[string]bool)
+       matches := make([]*cloudstack.RolePermission, len(desiredPermissions))
+       toDelete := make([]string, 0)
 
-       for _, desired := range 
rolePermissionSpecs(d.Get("permission").([]interface{})) {
+       for i, desired := range desiredPermissions {
                rp := rolePermissionsByID[desired.ID]
                if rp != nil && (rp.Rule != desired.Rule || rp.Description != 
desired.Description) {
-                       if exactMatch := 
findMatchingRolePermission(rolePermissions, desired, used); exactMatch != nil {
-                               rp = exactMatch
-                       } else {
-                               if err := deleteCloudStackRolePermission(cs, 
rp.Id); err != nil {
-                                       return err
-                               }
-                               deleted[rp.Id] = true
-                               used[rp.Id] = true
-                               rp = nil
-                       }
-               } else if rp == nil {
+                       rp = nil
+               }
+               if rp == nil {
                        rp = findMatchingRolePermission(rolePermissions, 
desired, used)
                }
+               if rp == nil {
+                       if stale := rolePermissionsByID[desired.ID]; stale != 
nil && !used[stale.Id] {
+                               toDelete = append(toDelete, stale.Id)
+                               used[stale.Id] = true
+                       }
+                       continue
+               }
+               used[rp.Id] = true
+               matches[i] = rp
+       }
+
+       for _, id := range toDelete {
+               if err := deleteCloudStackRolePermission(cs, id); err != nil {
+                       return err
+               }
+               deleted[id] = true
+       }
+
+       managedIDs := make([]string, 0, len(desiredPermissions))
+       managedIDSet := make(map[string]bool)
 
+       for i, desired := range desiredPermissions {
+               rp := matches[i]
                if rp == nil {
                        rp, err = createCloudStackRolePermission(cs, roleID, 
desired)
                        if err != nil {
@@ -275,18 +328,10 @@ func reconcileCloudStackRolePermissions(d 
*schema.ResourceData, meta interface{}
                        }
                }
 
-               used[rp.Id] = true
                managedIDs = append(managedIDs, rp.Id)
                managedIDSet[rp.Id] = true
        }
 
-       oldManagedIDs := make(map[string]bool)
-       for _, oldPermission := range oldPermissions {
-               if oldPermission.ID != "" {
-                       oldManagedIDs[oldPermission.ID] = true
-               }
-       }
-
        if d.Get("authoritative").(bool) {
                for _, rp := range rolePermissions {
                        if managedIDSet[rp.Id] || deleted[rp.Id] {

Reply via email to