This is an automated email from the ASF dual-hosted git repository.
sureshanaparti 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 13756c1 Fix reordering of role permissions (inc. external) (#340)
13756c1 is described below
commit 13756c17d42be5b187f49c12fe7e4905f565be52
Author: bddvlpr <[email protected]>
AuthorDate: Tue Sep 15 09:18:08 2026 +0200
Fix reordering of role permissions (inc. external) (#340)
---
cloudstack/resource_cloudstack_role_permission.go | 91 ++++---
.../resource_cloudstack_role_permission_test.go | 269 +++++++++++++++++++++
2 files changed, 321 insertions(+), 39 deletions(-)
diff --git a/cloudstack/resource_cloudstack_role_permission.go
b/cloudstack/resource_cloudstack_role_permission.go
index 1769f8d..c6f73d0 100644
--- a/cloudstack/resource_cloudstack_role_permission.go
+++ b/cloudstack/resource_cloudstack_role_permission.go
@@ -118,30 +118,31 @@ func resourceCloudStackRolePermissionRead(d
*schema.ResourceData, meta interface
return fmt.Errorf("Error listing Role Permissions: %s", err)
}
- permissionsByID := make(map[string]*cloudstack.RolePermission)
- for _, rp := range rolePermissions {
- permissionsByID[rp.Id] = rp
- }
-
var missing bool
- var readPermissions []interface{}
+ var missingPermissions []interface{}
used := make(map[string]bool)
+ desiredPermissions :=
rolePermissionSpecs(d.Get("permission").([]interface{}))
+ matchedPermissions := matchCloudStackRolePermissions(rolePermissions,
desiredPermissions)
- for _, desired := range
rolePermissionSpecs(d.Get("permission").([]interface{})) {
- var rp *cloudstack.RolePermission
- if desired.ID != "" {
- rp = permissionsByID[desired.ID]
- }
- if rp == nil {
- rp = findMatchingRolePermission(rolePermissions,
desired, used)
- }
+ for i, desired := range desiredPermissions {
+ rp := matchedPermissions[i]
if rp == nil {
missing = true
- readPermissions = append(readPermissions,
rolePermissionState(desired))
+ missingPermissions = append(missingPermissions,
rolePermissionState(desired))
continue
}
used[rp.Id] = true
+ }
+
+ // Keep managed permissions in the order returned by CloudStack.
Otherwise an
+ // out-of-band reorder is hidden by refresh and Terraform cannot
restore the
+ // order declared in the configuration.
+ readPermissions := make([]interface{}, 0,
len(used)+len(missingPermissions))
+ for _, rp := range rolePermissions {
+ if !used[rp.Id] {
+ continue
+ }
readPermissions = append(readPermissions,
rolePermissionState(rolePermissionSpec{
ID: rp.Id,
Rule: rp.Rule,
@@ -149,6 +150,7 @@ func resourceCloudStackRolePermissionRead(d
*schema.ResourceData, meta interface
Description: rp.Description,
}))
}
+ readPermissions = append(readPermissions, missingPermissions...)
if err := d.Set("permission", readPermissions); err != nil {
return fmt.Errorf("Error setting Role Permissions: %s", err)
@@ -242,28 +244,13 @@ func reconcileCloudStackRolePermissions(d
*schema.ResourceData, meta interface{}
rolePermissionsByID[rp.Id] = rp
}
- used := make(map[string]bool)
- deleted := make(map[string]bool)
managedIDs := make([]string, 0)
managedIDSet := make(map[string]bool)
+ desiredPermissions :=
rolePermissionSpecs(d.Get("permission").([]interface{}))
+ matchedPermissions := matchCloudStackRolePermissions(rolePermissions,
desiredPermissions)
- for _, desired := range
rolePermissionSpecs(d.Get("permission").([]interface{})) {
- 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 = findMatchingRolePermission(rolePermissions,
desired, used)
- }
-
+ for i, desired := range desiredPermissions {
+ rp := matchedPermissions[i]
if rp == nil {
rp, err = createCloudStackRolePermission(cs, roleID,
desired)
if err != nil {
@@ -275,7 +262,6 @@ func reconcileCloudStackRolePermissions(d
*schema.ResourceData, meta interface{}
}
}
- used[rp.Id] = true
managedIDs = append(managedIDs, rp.Id)
managedIDSet[rp.Id] = true
}
@@ -289,17 +275,16 @@ func reconcileCloudStackRolePermissions(d
*schema.ResourceData, meta interface{}
if d.Get("authoritative").(bool) {
for _, rp := range rolePermissions {
- if managedIDSet[rp.Id] || deleted[rp.Id] {
+ if managedIDSet[rp.Id] {
continue
}
if err := deleteCloudStackRolePermission(cs, rp.Id);
err != nil {
return err
}
- deleted[rp.Id] = true
}
} else {
for oldID := range oldManagedIDs {
- if managedIDSet[oldID] || deleted[oldID] {
+ if managedIDSet[oldID] {
continue
}
if rolePermissionsByID[oldID] == nil {
@@ -308,7 +293,6 @@ func reconcileCloudStackRolePermissions(d
*schema.ResourceData, meta interface{}
if err := deleteCloudStackRolePermission(cs, oldID);
err != nil {
return err
}
- deleted[oldID] = true
}
}
@@ -441,6 +425,35 @@ func findMatchingRolePermission(rolePermissions
[]*cloudstack.RolePermission, de
return nil
}
+func matchCloudStackRolePermissions(rolePermissions
[]*cloudstack.RolePermission, desiredPermissions []rolePermissionSpec)
[]*cloudstack.RolePermission {
+ permissionsByID := make(map[string]*cloudstack.RolePermission,
len(rolePermissions))
+ for _, rp := range rolePermissions {
+ permissionsByID[rp.Id] = rp
+ }
+
+ matchedPermissions := make([]*cloudstack.RolePermission,
len(desiredPermissions))
+ used := make(map[string]bool)
+
+ for i, desired := range desiredPermissions {
+ var rp *cloudstack.RolePermission
+ if desired.ID != "" {
+ candidate := permissionsByID[desired.ID]
+ if candidate != nil && !used[candidate.Id] &&
candidate.Rule == desired.Rule && candidate.Description == desired.Description {
+ rp = candidate
+ }
+ }
+ if rp == nil {
+ rp = findMatchingRolePermission(rolePermissions,
desired, used)
+ }
+ if rp != nil {
+ used[rp.Id] = true
+ }
+ matchedPermissions[i] = rp
+ }
+
+ return matchedPermissions
+}
+
func rolePermissionLock(roleID string) *sync.Mutex {
lock, _ := rolePermissionLocks.LoadOrStore(roleID, &sync.Mutex{})
return lock.(*sync.Mutex)
diff --git a/cloudstack/resource_cloudstack_role_permission_test.go
b/cloudstack/resource_cloudstack_role_permission_test.go
index 18eeee6..6196a05 100644
--- a/cloudstack/resource_cloudstack_role_permission_test.go
+++ b/cloudstack/resource_cloudstack_role_permission_test.go
@@ -61,6 +61,8 @@ func TestAccCloudStackRolePermission_basic(t *testing.T) {
}
func TestAccCloudStackRolePermission_orderAfterRecreate(t *testing.T) {
+ var wildcardRuleID string
+
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
@@ -71,6 +73,7 @@ func TestAccCloudStackRolePermission_orderAfterRecreate(t
*testing.T) {
Check: resource.ComposeTestCheckFunc(
testAccCheckCloudStackRolePermissionExists("cloudstack_role_permission.foo"),
testAccCheckCloudStackRolePermissionOrder("cloudstack_role_permission.foo",
[]string{"listZones", "*"}),
+
testAccCaptureCloudStackRolePermissionID("cloudstack_role_permission.foo", "*",
&wildcardRuleID),
),
},
{
@@ -78,13 +81,131 @@ func TestAccCloudStackRolePermission_orderAfterRecreate(t
*testing.T) {
Check: resource.ComposeTestCheckFunc(
testAccCheckCloudStackRolePermissionExists("cloudstack_role_permission.foo"),
testAccCheckCloudStackRolePermissionOrder("cloudstack_role_permission.foo",
[]string{"*"}),
+
testAccCheckCloudStackRolePermissionID("cloudstack_role_permission.foo", "*",
&wildcardRuleID),
),
},
+ {
+ Config:
testAccCloudStackRolePermission_orderWithoutSpecificRule,
+ PlanOnly: true,
+ },
{
Config:
testAccCloudStackRolePermission_orderWithSpecificRule,
Check: resource.ComposeTestCheckFunc(
testAccCheckCloudStackRolePermissionExists("cloudstack_role_permission.foo"),
testAccCheckCloudStackRolePermissionOrder("cloudstack_role_permission.foo",
[]string{"listZones", "*"}),
+
testAccCheckCloudStackRolePermissionID("cloudstack_role_permission.foo", "*",
&wildcardRuleID),
+ ),
+ },
+ {
+ Config:
testAccCloudStackRolePermission_orderWithSpecificRule,
+ PlanOnly: true,
+ },
+ },
+ })
+}
+
+func TestMatchCloudStackRolePermissions_shiftedIDsAfterRemoval(t *testing.T) {
+ rolePermissions := []*cloudstack.RolePermission{
+ {Id: "detach-id", Rule: "detachIso", Permission: "allow"},
+ {Id: "start-id", Rule: "startSystemVm", Permission: "allow"},
+ {Id: "wildcard-id", Rule: "*", Permission: "deny"},
+ }
+ desiredPermissions := []rolePermissionSpec{
+ {ID: "removed-attach-id", Rule: "detachIso", Permission:
"allow"},
+ {ID: "detach-id", Rule: "startSystemVm", Permission: "allow"},
+ {ID: "start-id", Rule: "*", Permission: "deny"},
+ }
+
+ matchedPermissions := matchCloudStackRolePermissions(rolePermissions,
desiredPermissions)
+ assertRolePermissionIDs(t, matchedPermissions, []string{"detach-id",
"start-id", "wildcard-id"})
+}
+
+func TestMatchCloudStackRolePermissions_shiftedIDsAfterInsertion(t *testing.T)
{
+ rolePermissions := []*cloudstack.RolePermission{
+ {Id: "detach-id", Rule: "detachIso", Permission: "allow"},
+ {Id: "start-id", Rule: "startSystemVm", Permission: "allow"},
+ }
+ desiredPermissions := []rolePermissionSpec{
+ {ID: "detach-id", Rule: "attachIso", Permission: "allow"},
+ {ID: "start-id", Rule: "detachIso", Permission: "allow"},
+ {Rule: "startSystemVm", Permission: "allow"},
+ }
+
+ matchedPermissions := matchCloudStackRolePermissions(rolePermissions,
desiredPermissions)
+ assertRolePermissionIDs(t, matchedPermissions, []string{"",
"detach-id", "start-id"})
+}
+
+func assertRolePermissionIDs(t *testing.T, rolePermissions
[]*cloudstack.RolePermission, expectedIDs []string) {
+ t.Helper()
+
+ if len(rolePermissions) != len(expectedIDs) {
+ t.Fatalf("Expected %d matched Role Permissions, got %d",
len(expectedIDs), len(rolePermissions))
+ }
+
+ for i, expectedID := range expectedIDs {
+ var actualID string
+ if rolePermissions[i] != nil {
+ actualID = rolePermissions[i].Id
+ }
+ if actualID != expectedID {
+ t.Errorf("Expected matched Role Permission %d to have
ID %q, got %q", i, expectedID, actualID)
+ }
+ }
+}
+
+func TestAccCloudStackRolePermission_orderChange(t *testing.T) {
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() { testAccPreCheck(t) },
+ Providers: testAccProviders,
+ CheckDestroy: testAccCheckCloudStackRolePermissionDestroy,
+ Steps: []resource.TestStep{
+ {
+ Config:
testAccCloudStackRolePermission_orderWithSpecificRule,
+ // The check deliberately changes the API-side
order to verify drift detection.
+ ExpectNonEmptyPlan: true,
+ Check: resource.ComposeTestCheckFunc(
+
testAccCheckCloudStackRolePermissionExists("cloudstack_role_permission.foo"),
+
testAccCheckCloudStackRolePermissionOrder("cloudstack_role_permission.foo",
[]string{"listZones", "*"}),
+
testAccReorderCloudStackRolePermissions("cloudstack_role_permission.foo",
[]string{"*", "listZones"}),
+
testAccCheckCloudStackRolePermissionOrder("cloudstack_role_permission.foo",
[]string{"*", "listZones"}),
+ ),
+ },
+ {
+ Config:
testAccCloudStackRolePermission_orderWithSpecificRule,
+ Check: resource.ComposeTestCheckFunc(
+
testAccCheckCloudStackRolePermissionExists("cloudstack_role_permission.foo"),
+
testAccCheckCloudStackRolePermissionOrder("cloudstack_role_permission.foo",
[]string{"listZones", "*"}),
+ ),
+ },
+ {
+ Config:
testAccCloudStackRolePermission_orderReversed,
+ Check: resource.ComposeTestCheckFunc(
+
testAccCheckCloudStackRolePermissionExists("cloudstack_role_permission.foo"),
+
testAccCheckCloudStackRolePermissionOrder("cloudstack_role_permission.foo",
[]string{"*", "listZones"}),
+ ),
+ },
+ },
+ })
+}
+
+func TestAccCloudStackRolePermission_orderChangeThreeRules(t *testing.T) {
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() { testAccPreCheck(t) },
+ Providers: testAccProviders,
+ CheckDestroy: testAccCheckCloudStackRolePermissionDestroy,
+ Steps: []resource.TestStep{
+ {
+ Config:
testAccCloudStackRolePermission_orderThreeRules,
+ Check: resource.ComposeTestCheckFunc(
+
testAccCheckCloudStackRolePermissionExists("cloudstack_role_permission.foo"),
+
testAccCheckCloudStackRolePermissionOrder("cloudstack_role_permission.foo",
[]string{"listZones", "listVirtualMachines", "*"}),
+ ),
+ },
+ {
+ Config:
testAccCloudStackRolePermission_orderThreeRulesReordered,
+ Check: resource.ComposeTestCheckFunc(
+
testAccCheckCloudStackRolePermissionExists("cloudstack_role_permission.foo"),
+
testAccCheckCloudStackRolePermissionOrder("cloudstack_role_permission.foo",
[]string{"*", "listZones", "listVirtualMachines"}),
),
},
},
@@ -168,6 +289,81 @@ func testAccCheckCloudStackRolePermissionOrder(n string,
rules []string) resourc
}
}
+func testAccReorderCloudStackRolePermissions(n string, rules []string)
resource.TestCheckFunc {
+ return func(s *terraform.State) error {
+ rolePermissions, err := testAccListCloudStackRolePermissions(s,
n)
+ if err != nil {
+ return err
+ }
+
+ ruleIDs := make([]string, 0, len(rules))
+ for _, rule := range rules {
+ var ruleID string
+ for _, rp := range rolePermissions {
+ if rp.Rule == rule {
+ ruleID = rp.Id
+ break
+ }
+ }
+ if ruleID == "" {
+ return fmt.Errorf("Role Permission rule %q not
found", rule)
+ }
+ ruleIDs = append(ruleIDs, ruleID)
+ }
+
+ cs := testAccProvider.Meta().(*cloudstack.CloudStackClient)
+ p :=
cs.Role.NewUpdateRolePermissionParams(s.RootModule().Resources[n].Primary.Attributes["role_id"])
+ p.SetRuleorder(ruleIDs)
+ if _, err := cs.Role.UpdateRolePermission(p); err != nil {
+ return fmt.Errorf("Error ordering Role Permissions:
%s", err)
+ }
+
+ return nil
+ }
+}
+
+func testAccCaptureCloudStackRolePermissionID(n, rule string, ruleID *string)
resource.TestCheckFunc {
+ return func(s *terraform.State) error {
+ rolePermissions, err := testAccListCloudStackRolePermissions(s,
n)
+ if err != nil {
+ return err
+ }
+
+ for _, rp := range rolePermissions {
+ if rp.Rule == rule {
+ *ruleID = rp.Id
+ return nil
+ }
+ }
+
+ return fmt.Errorf("Role Permission rule %q not found", rule)
+ }
+}
+
+func testAccCheckCloudStackRolePermissionID(n, rule string, expectedRuleID
*string) resource.TestCheckFunc {
+ return func(s *terraform.State) error {
+ if *expectedRuleID == "" {
+ return fmt.Errorf("No expected Role Permission ID is
set for rule %q", rule)
+ }
+
+ rolePermissions, err := testAccListCloudStackRolePermissions(s,
n)
+ if err != nil {
+ return err
+ }
+
+ for _, rp := range rolePermissions {
+ if rp.Rule == rule {
+ if rp.Id != *expectedRuleID {
+ return fmt.Errorf("Expected Role
Permission rule %q to keep ID %s, got %s", rule, *expectedRuleID, rp.Id)
+ }
+ return nil
+ }
+ }
+
+ return fmt.Errorf("Role Permission rule %q not found", rule)
+ }
+}
+
func testAccCreateCloudStackRolePermission(n, rule, permission, description
string, ruleID *string) resource.TestCheckFunc {
return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[n]
@@ -344,6 +540,79 @@ resource "cloudstack_role_permission" "foo" {
}
`
+const testAccCloudStackRolePermission_orderReversed = `
+resource "cloudstack_role" "foo" {
+ name = "terraform-role"
+ type = "User"
+}
+
+resource "cloudstack_role_permission" "foo" {
+ role_id = cloudstack_role.foo.id
+
+ permission {
+ rule = "*"
+ permission = "deny"
+ }
+
+ permission {
+ rule = "listZones"
+ permission = "allow"
+ }
+}
+`
+
+const testAccCloudStackRolePermission_orderThreeRules = `
+resource "cloudstack_role" "foo" {
+ name = "terraform-role"
+ type = "User"
+}
+
+resource "cloudstack_role_permission" "foo" {
+ role_id = cloudstack_role.foo.id
+
+ permission {
+ rule = "listZones"
+ permission = "allow"
+ }
+
+ permission {
+ rule = "listVirtualMachines"
+ permission = "allow"
+ }
+
+ permission {
+ rule = "*"
+ permission = "deny"
+ }
+}
+`
+
+const testAccCloudStackRolePermission_orderThreeRulesReordered = `
+resource "cloudstack_role" "foo" {
+ name = "terraform-role"
+ type = "User"
+}
+
+resource "cloudstack_role_permission" "foo" {
+ role_id = cloudstack_role.foo.id
+
+ permission {
+ rule = "*"
+ permission = "deny"
+ }
+
+ permission {
+ rule = "listZones"
+ permission = "allow"
+ }
+
+ permission {
+ rule = "listVirtualMachines"
+ permission = "allow"
+ }
+}
+`
+
const testAccCloudStackRolePermission_subset = `
resource "cloudstack_role" "foo" {
name = "terraform-role"