From aace8e3692ca86884cca4d74e4fd7a1cdf5d4c28 Mon Sep 17 00:00:00 2001 From: Ian Crouch Date: Thu, 22 May 2025 12:44:27 -0400 Subject: [PATCH 1/4] Add cloudstack_role data source and resource implementation - Implement data source for cloudstack_role with read functionality. - Create resource for managing cloudstack_role with CRUD operations. - Update documentation for cloudstack_role data source and resource. --- cloudstack/data_source_cloudstack_role.go | 93 ++++++++++ .../data_source_cloudstack_role_test.go | 58 +++++++ cloudstack/provider.go | 2 + cloudstack/resource_cloudstack_role.go | 160 ++++++++++++++++++ cloudstack/resource_cloudstack_role_test.go | 110 ++++++++++++ website/cloudstack.erb | 7 + website/docs/d/role.html.markdown | 48 ++++++ website/docs/r/role.html.markdown | 44 +++++ 8 files changed, 522 insertions(+) create mode 100644 cloudstack/data_source_cloudstack_role.go create mode 100644 cloudstack/data_source_cloudstack_role_test.go create mode 100644 cloudstack/resource_cloudstack_role.go create mode 100644 cloudstack/resource_cloudstack_role_test.go create mode 100644 website/docs/d/role.html.markdown create mode 100644 website/docs/r/role.html.markdown diff --git a/cloudstack/data_source_cloudstack_role.go b/cloudstack/data_source_cloudstack_role.go new file mode 100644 index 00000000..9d4a1edf --- /dev/null +++ b/cloudstack/data_source_cloudstack_role.go @@ -0,0 +1,93 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +package cloudstack + +import ( + "fmt" + "log" + + "github.com/apache/cloudstack-go/v2/cloudstack" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func dataSourceCloudstackRole() *schema.Resource { + return &schema.Resource{ + Read: dataSourceCloudstackRoleRead, + Schema: map[string]*schema.Schema{ + "filter": dataSourceFiltersSchema(), + + "id": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + + "name": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + + "type": { + Type: schema.TypeString, + Computed: true, + }, + + "description": { + Type: schema.TypeString, + Computed: true, + }, + + "is_public": { + Type: schema.TypeBool, + Computed: true, + }, + }, + } +} + +func dataSourceCloudstackRoleRead(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + var err error + var role *cloudstack.Role + + if id, ok := d.GetOk("id"); ok { + log.Printf("[DEBUG] Getting Role by ID: %s", id.(string)) + role, _, err = cs.Role.GetRoleByID(id.(string)) + } else if name, ok := d.GetOk("name"); ok { + log.Printf("[DEBUG] Getting Role by name: %s", name.(string)) + role, _, err = cs.Role.GetRoleByName(name.(string)) + } else { + return fmt.Errorf("Either 'id' or 'name' must be specified") + } + + if err != nil { + return err + } + + d.SetId(role.Id) + d.Set("name", role.Name) + d.Set("type", role.Type) + d.Set("description", role.Description) + d.Set("is_public", role.Ispublic) + + return nil +} diff --git a/cloudstack/data_source_cloudstack_role_test.go b/cloudstack/data_source_cloudstack_role_test.go new file mode 100644 index 00000000..2de8eb1f --- /dev/null +++ b/cloudstack/data_source_cloudstack_role_test.go @@ -0,0 +1,58 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +package cloudstack + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccDataSourceCloudStackRole_basic(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccDataSourceCloudStackRole_basic, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + "data.cloudstack_role.role", "name", "terraform-role"), + resource.TestCheckResourceAttr( + "data.cloudstack_role.role", "description", "terraform test role"), + resource.TestCheckResourceAttr( + "data.cloudstack_role.role", "is_public", "true"), + ), + }, + }, + }) +} + +const testAccDataSourceCloudStackRole_basic = ` +resource "cloudstack_role" "foo" { + name = "terraform-role" + description = "terraform test role" + is_public = true +} + +data "cloudstack_role" "role" { + name = "${cloudstack_role.foo.name}" +} +` diff --git a/cloudstack/provider.go b/cloudstack/provider.go index a71df0e5..71dea780 100644 --- a/cloudstack/provider.go +++ b/cloudstack/provider.go @@ -90,6 +90,7 @@ func Provider() *schema.Provider { "cloudstack_user": dataSourceCloudstackUser(), "cloudstack_vpn_connection": dataSourceCloudstackVPNConnection(), "cloudstack_pod": dataSourceCloudstackPod(), + "cloudstack_role": dataSourceCloudstackRole(), }, ResourcesMap: map[string]*schema.Resource{ @@ -131,6 +132,7 @@ func Provider() *schema.Provider { "cloudstack_account": resourceCloudStackAccount(), "cloudstack_user": resourceCloudStackUser(), "cloudstack_domain": resourceCloudStackDomain(), + "cloudstack_role": resourceCloudStackRole(), }, ConfigureFunc: providerConfigure, diff --git a/cloudstack/resource_cloudstack_role.go b/cloudstack/resource_cloudstack_role.go new file mode 100644 index 00000000..1f6e2bda --- /dev/null +++ b/cloudstack/resource_cloudstack_role.go @@ -0,0 +1,160 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +package cloudstack + +import ( + "fmt" + "log" + + "github.com/apache/cloudstack-go/v2/cloudstack" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func resourceCloudStackRole() *schema.Resource { + return &schema.Resource{ + Create: resourceCloudStackRoleCreate, + Read: resourceCloudStackRoleRead, + Update: resourceCloudStackRoleUpdate, + Delete: resourceCloudStackRoleDelete, + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + }, + "type": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "description": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "is_public": { + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + }, + } +} + +func resourceCloudStackRoleCreate(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + name := d.Get("name").(string) + + // Create a new parameter struct + p := cs.Role.NewCreateRoleParams(name) + + if roleType, ok := d.GetOk("type"); ok { + p.SetType(roleType.(string)) + } + + if description, ok := d.GetOk("description"); ok { + p.SetDescription(description.(string)) + } + + if isPublic, ok := d.GetOk("is_public"); ok { + p.SetIspublic(isPublic.(bool)) + } + + log.Printf("[DEBUG] Creating Role %s", name) + r, err := cs.Role.CreateRole(p) + + if err != nil { + return fmt.Errorf("Error creating Role: %s", err) + } + + log.Printf("[DEBUG] Role %s successfully created", name) + d.SetId(r.Id) + + return resourceCloudStackRoleRead(d, meta) +} + +func resourceCloudStackRoleRead(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + // Get the Role details + r, count, err := cs.Role.GetRoleByID(d.Id()) + if err != nil { + if count == 0 { + log.Printf("[DEBUG] Role %s does not exist", d.Id()) + d.SetId("") + return nil + } + return fmt.Errorf("Error getting Role: %s", err) + } + + d.Set("name", r.Name) + d.Set("type", r.Type) + d.Set("description", r.Description) + d.Set("is_public", r.Ispublic) + + return nil +} + +func resourceCloudStackRoleUpdate(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + // Create a new parameter struct + p := cs.Role.NewUpdateRoleParams(d.Id()) + + if d.HasChange("name") { + p.SetName(d.Get("name").(string)) + } + + if d.HasChange("type") { + p.SetType(d.Get("type").(string)) + } + + if d.HasChange("description") { + p.SetDescription(d.Get("description").(string)) + } + + if d.HasChange("is_public") { + p.SetIspublic(d.Get("is_public").(bool)) + } + + log.Printf("[DEBUG] Updating Role %s", d.Get("name").(string)) + _, err := cs.Role.UpdateRole(p) + + if err != nil { + return fmt.Errorf("Error updating Role: %s", err) + } + + return resourceCloudStackRoleRead(d, meta) +} + +func resourceCloudStackRoleDelete(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + // Create a new parameter struct + p := cs.Role.NewDeleteRoleParams(d.Id()) + + log.Printf("[DEBUG] Deleting Role %s", d.Get("name").(string)) + _, err := cs.Role.DeleteRole(p) + + if err != nil { + return fmt.Errorf("Error deleting Role: %s", err) + } + + return nil +} diff --git a/cloudstack/resource_cloudstack_role_test.go b/cloudstack/resource_cloudstack_role_test.go new file mode 100644 index 00000000..4edda08f --- /dev/null +++ b/cloudstack/resource_cloudstack_role_test.go @@ -0,0 +1,110 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +package cloudstack + +import ( + "fmt" + "testing" + + "github.com/apache/cloudstack-go/v2/cloudstack" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func TestAccCloudStackRole_basic(t *testing.T) { + var role cloudstack.Role + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackRoleDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCloudStackRole_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackRoleExists("cloudstack_role.foo", &role), + resource.TestCheckResourceAttr( + "cloudstack_role.foo", "name", "terraform-role"), + resource.TestCheckResourceAttr( + "cloudstack_role.foo", "description", "terraform test role"), + resource.TestCheckResourceAttr( + "cloudstack_role.foo", "is_public", "true"), + ), + }, + }, + }) +} + +func testAccCheckCloudStackRoleExists(n string, role *cloudstack.Role) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No Role ID is set") + } + + cs := testAccProvider.Meta().(*cloudstack.CloudStackClient) + r, _, err := cs.Role.GetRoleByID(rs.Primary.ID) + + if err != nil { + return err + } + + if r.Id != rs.Primary.ID { + return fmt.Errorf("Role not found") + } + + *role = *r + + return nil + } +} + +func testAccCheckCloudStackRoleDestroy(s *terraform.State) error { + cs := testAccProvider.Meta().(*cloudstack.CloudStackClient) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "cloudstack_role" { + continue + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No Role ID is set") + } + + _, _, err := cs.Role.GetRoleByID(rs.Primary.ID) + if err == nil { + return fmt.Errorf("Role %s still exists", rs.Primary.ID) + } + } + + return nil +} + +const testAccCloudStackRole_basic = ` +resource "cloudstack_role" "foo" { + name = "terraform-role" + description = "terraform test role" + is_public = true +} +` diff --git a/website/cloudstack.erb b/website/cloudstack.erb index f900196a..fbc70b4a 100644 --- a/website/cloudstack.erb +++ b/website/cloudstack.erb @@ -16,6 +16,9 @@ > cloudstack_template + > + cloudstack_role + @@ -121,6 +124,10 @@ > cloudstack_vpn_connection + + > + cloudstack_role + diff --git a/website/docs/d/role.html.markdown b/website/docs/d/role.html.markdown new file mode 100644 index 00000000..0804b03e --- /dev/null +++ b/website/docs/d/role.html.markdown @@ -0,0 +1,48 @@ +--- +subcategory: "" +layout: "cloudstack" +page_title: "CloudStack: cloudstack_role" +description: |- + Gets information about a role. +--- + +# cloudstack_role + +Use this data source to get information about a role for use in other resources. + +## Example Usage + +```hcl +data "cloudstack_role" "admin" { + name = "Admin" +} + +resource "cloudstack_account" "example" { + email = "example@example.com" + first_name = "John" + last_name = "Doe" + password = "password" + username = "johndoe" + account_type = 1 + role_id = data.cloudstack_role.admin.id +} +``` + +## Argument Reference + +The following arguments are supported: + +* `id` - (Optional) The ID of the role. +* `name` - (Optional) The name of the role. + +At least one of the above arguments is required. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The ID of the role. +* `name` - The name of the role. +* `type` - The type of the role. +* `description` - The description of the role. +* `is_public` - Whether the role is public or not. diff --git a/website/docs/r/role.html.markdown b/website/docs/r/role.html.markdown new file mode 100644 index 00000000..d05322c4 --- /dev/null +++ b/website/docs/r/role.html.markdown @@ -0,0 +1,44 @@ +--- +subcategory: "" +layout: "cloudstack" +page_title: "CloudStack: cloudstack_role" +description: |- + Creates a role. +--- + +# cloudstack_role + +Creates a role. + +## Example Usage + +```hcl +resource "cloudstack_role" "admin" { + name = "Admin" + type = "Admin" + description = "Administrator role" + is_public = true +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) The name of the role. +* `type` - (Optional) The type of the role. Defaults to the CloudStack default. +* `description` - (Optional) The description of the role. +* `is_public` - (Optional) Whether the role is public or not. Defaults to `false`. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The ID of the role. + +## Import + +Roles can be imported using the role ID, e.g. + +``` +terraform import cloudstack_role.admin 12345678-1234-1234-1234-123456789012 From be71510fe9d81384bc9fd6b7de37f3f587b3274d Mon Sep 17 00:00:00 2001 From: Ian Crouch Date: Thu, 22 May 2025 14:04:23 -0400 Subject: [PATCH 2/4] Update import paths to use terraform-plugin-testing package --- cloudstack/data_source_cloudstack_role_test.go | 2 +- cloudstack/resource_cloudstack_role_test.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cloudstack/data_source_cloudstack_role_test.go b/cloudstack/data_source_cloudstack_role_test.go index 2de8eb1f..a309cd89 100644 --- a/cloudstack/data_source_cloudstack_role_test.go +++ b/cloudstack/data_source_cloudstack_role_test.go @@ -22,7 +22,7 @@ package cloudstack import ( "testing" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" ) func TestAccDataSourceCloudStackRole_basic(t *testing.T) { diff --git a/cloudstack/resource_cloudstack_role_test.go b/cloudstack/resource_cloudstack_role_test.go index 4edda08f..35eacf54 100644 --- a/cloudstack/resource_cloudstack_role_test.go +++ b/cloudstack/resource_cloudstack_role_test.go @@ -24,8 +24,8 @@ import ( "testing" "github.com/apache/cloudstack-go/v2/cloudstack" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" - "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" ) func TestAccCloudStackRole_basic(t *testing.T) { From 55ff8c139dd28fbca333b973fb3d3b0053eb04af Mon Sep 17 00:00:00 2001 From: Ian Crouch Date: Thu, 22 May 2025 15:56:28 -0400 Subject: [PATCH 3/4] Enhance cloudstack_role data source and resource with filter support and improved documentation - Added filter support to the cloudstack_role data source for role retrieval. - Updated resource_cloudstack_role to require either role_id or type. - Enhanced documentation for both data source and resource with examples and argument descriptions. --- cloudstack/data_source_cloudstack_role.go | 92 ++++++++++++++++--- .../data_source_cloudstack_role_test.go | 5 +- cloudstack/resource_cloudstack_role.go | 31 +++++-- website/docs/d/role.html.markdown | 19 +++- website/docs/r/role.html.markdown | 14 ++- 5 files changed, 134 insertions(+), 27 deletions(-) diff --git a/cloudstack/data_source_cloudstack_role.go b/cloudstack/data_source_cloudstack_role.go index 9d4a1edf..2f93b347 100644 --- a/cloudstack/data_source_cloudstack_role.go +++ b/cloudstack/data_source_cloudstack_role.go @@ -20,8 +20,11 @@ package cloudstack import ( + "encoding/json" "fmt" "log" + "regexp" + "strings" "github.com/apache/cloudstack-go/v2/cloudstack" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -33,15 +36,14 @@ func dataSourceCloudstackRole() *schema.Resource { Schema: map[string]*schema.Schema{ "filter": dataSourceFiltersSchema(), + //Computed values "id": { Type: schema.TypeString, - Optional: true, Computed: true, }, "name": { Type: schema.TypeString, - Optional: true, Computed: true, }, @@ -65,24 +67,36 @@ func dataSourceCloudstackRole() *schema.Resource { func dataSourceCloudstackRoleRead(d *schema.ResourceData, meta interface{}) error { cs := meta.(*cloudstack.CloudStackClient) + p := cs.Role.NewListRolesParams() - var err error + csRoles, err := cs.Role.ListRoles(p) + if err != nil { + return fmt.Errorf("failed to list roles: %s", err) + } + + filters := d.Get("filter") var role *cloudstack.Role - if id, ok := d.GetOk("id"); ok { - log.Printf("[DEBUG] Getting Role by ID: %s", id.(string)) - role, _, err = cs.Role.GetRoleByID(id.(string)) - } else if name, ok := d.GetOk("name"); ok { - log.Printf("[DEBUG] Getting Role by name: %s", name.(string)) - role, _, err = cs.Role.GetRoleByName(name.(string)) - } else { - return fmt.Errorf("Either 'id' or 'name' must be specified") + for _, r := range csRoles.Roles { + match, err := applyRoleFilters(r, filters.(*schema.Set)) + if err != nil { + return err + } + if match { + role = r + break + } } - if err != nil { - return err + if role == nil { + return fmt.Errorf("no role is matching with the specified criteria") } + log.Printf("[DEBUG] Selected role: %s\n", role.Name) + + return roleDescriptionAttributes(d, role) +} +func roleDescriptionAttributes(d *schema.ResourceData, role *cloudstack.Role) error { d.SetId(role.Id) d.Set("name", role.Name) d.Set("type", role.Type) @@ -91,3 +105,55 @@ func dataSourceCloudstackRoleRead(d *schema.ResourceData, meta interface{}) erro return nil } + +func latestRole(roles []*cloudstack.Role) (*cloudstack.Role, error) { + // Since the Role struct doesn't have a Created field, + // we'll just return the first role in the list + if len(roles) > 0 { + return roles[0], nil + } + return nil, fmt.Errorf("no roles found") +} + +func applyRoleFilters(role *cloudstack.Role, filters *schema.Set) (bool, error) { + var roleJSON map[string]interface{} + k, _ := json.Marshal(role) + err := json.Unmarshal(k, &roleJSON) + if err != nil { + return false, err + } + + for _, f := range filters.List() { + m := f.(map[string]interface{}) + r, err := regexp.Compile(m["value"].(string)) + if err != nil { + return false, fmt.Errorf("invalid regex: %s", err) + } + updatedName := strings.ReplaceAll(m["name"].(string), "_", "") + + // Check if the field exists in the role JSON + roleField, ok := roleJSON[updatedName] + if !ok { + return false, fmt.Errorf("field %s does not exist in role", updatedName) + } + + // Convert the field to string for regex matching + var roleFieldStr string + switch v := roleField.(type) { + case string: + roleFieldStr = v + case bool: + roleFieldStr = fmt.Sprintf("%t", v) + case float64: + roleFieldStr = fmt.Sprintf("%g", v) + default: + roleFieldStr = fmt.Sprintf("%v", v) + } + + if !r.MatchString(roleFieldStr) { + return false, nil + } + } + + return true, nil +} diff --git a/cloudstack/data_source_cloudstack_role_test.go b/cloudstack/data_source_cloudstack_role_test.go index a309cd89..abc135d4 100644 --- a/cloudstack/data_source_cloudstack_role_test.go +++ b/cloudstack/data_source_cloudstack_role_test.go @@ -53,6 +53,9 @@ resource "cloudstack_role" "foo" { } data "cloudstack_role" "role" { - name = "${cloudstack_role.foo.name}" + filter { + name = "name" + value = "${cloudstack_role.foo.name}" + } } ` diff --git a/cloudstack/resource_cloudstack_role.go b/cloudstack/resource_cloudstack_role.go index 1f6e2bda..eea50b88 100644 --- a/cloudstack/resource_cloudstack_role.go +++ b/cloudstack/resource_cloudstack_role.go @@ -39,9 +39,10 @@ func resourceCloudStackRole() *schema.Resource { Required: true, }, "type": { - Type: schema.TypeString, - Optional: true, - Computed: true, + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "The type of the role, valid options are: Admin, ResourceAdmin, DomainAdmin, User", }, "description": { Type: schema.TypeString, @@ -49,9 +50,16 @@ func resourceCloudStackRole() *schema.Resource { Computed: true, }, "is_public": { - Type: schema.TypeBool, - Optional: true, - Default: false, + Type: schema.TypeBool, + Optional: true, + Default: true, + Description: "Indicates whether the role will be visible to all users (public) or only to root admins (private). Default is true.", + }, + "role_id": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Description: "ID of the role to be cloned from. Either role_id or type must be passed in.", }, }, } @@ -64,8 +72,17 @@ func resourceCloudStackRoleCreate(d *schema.ResourceData, meta interface{}) erro // Create a new parameter struct p := cs.Role.NewCreateRoleParams(name) - if roleType, ok := d.GetOk("type"); ok { + // Check if either role_id or type is provided + roleID, roleIDOk := d.GetOk("role_id") + roleType, roleTypeOk := d.GetOk("type") + + if roleIDOk { + p.SetRoleid(roleID.(string)) + } else if roleTypeOk { p.SetType(roleType.(string)) + } else { + // According to the API, either roleid or type must be passed in + return fmt.Errorf("either role_id or type must be specified") } if description, ok := d.GetOk("description"); ok { diff --git a/website/docs/d/role.html.markdown b/website/docs/d/role.html.markdown index 0804b03e..4d77eb73 100644 --- a/website/docs/d/role.html.markdown +++ b/website/docs/d/role.html.markdown @@ -14,7 +14,10 @@ Use this data source to get information about a role for use in other resources. ```hcl data "cloudstack_role" "admin" { - name = "Admin" + filter { + name = "name" + value = "Admin" + } } resource "cloudstack_account" "example" { @@ -32,10 +35,18 @@ resource "cloudstack_account" "example" { The following arguments are supported: -* `id` - (Optional) The ID of the role. -* `name` - (Optional) The name of the role. +* `filter` - (Required) One or more name/value pairs to filter off of. See the example below for usage. -At least one of the above arguments is required. +## Filter Example + +```hcl +data "cloudstack_role" "admin" { + filter { + name = "name" + value = "Admin" + } +} +``` ## Attributes Reference diff --git a/website/docs/r/role.html.markdown b/website/docs/r/role.html.markdown index d05322c4..afb7835f 100644 --- a/website/docs/r/role.html.markdown +++ b/website/docs/r/role.html.markdown @@ -13,12 +13,21 @@ Creates a role. ## Example Usage ```hcl +# Create a role with a specific type resource "cloudstack_role" "admin" { name = "Admin" type = "Admin" description = "Administrator role" is_public = true } + +# Create a role by cloning an existing role +resource "cloudstack_role" "custom_admin" { + name = "CustomAdmin" + role_id = "12345678-1234-1234-1234-123456789012" + description = "Custom administrator role cloned from an existing role" + is_public = false +} ``` ## Argument Reference @@ -26,9 +35,10 @@ resource "cloudstack_role" "admin" { The following arguments are supported: * `name` - (Required) The name of the role. -* `type` - (Optional) The type of the role. Defaults to the CloudStack default. +* `type` - (Optional) The type of the role. Valid options are: Admin, ResourceAdmin, DomainAdmin, User. Either `type` or `role_id` must be specified. * `description` - (Optional) The description of the role. -* `is_public` - (Optional) Whether the role is public or not. Defaults to `false`. +* `is_public` - (Optional) Whether the role is public or not. Defaults to `true`. +* `role_id` - (Optional) ID of the role to be cloned from. Either `role_id` or `type` must be specified. ## Attributes Reference From bbc1548cc90f940b2b5262fb85ff84dbf4416117 Mon Sep 17 00:00:00 2001 From: Ian Crouch Date: Mon, 26 May 2025 12:39:11 -0400 Subject: [PATCH 4/4] Fix TestAccCloudStackRole_basic test 1. Add required 'type' parameter to role resource configuration in both resource and data source tests 2. Modify testAccCheckCloudStackRoleDestroy function to handle potential panic when accessing l.Roles[0] --- .../data_source_cloudstack_role_test.go | 1 + cloudstack/resource_cloudstack_role_test.go | 21 ++++++++++++++++--- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/cloudstack/data_source_cloudstack_role_test.go b/cloudstack/data_source_cloudstack_role_test.go index abc135d4..4e289db2 100644 --- a/cloudstack/data_source_cloudstack_role_test.go +++ b/cloudstack/data_source_cloudstack_role_test.go @@ -50,6 +50,7 @@ resource "cloudstack_role" "foo" { name = "terraform-role" description = "terraform test role" is_public = true + type = "User" } data "cloudstack_role" "role" { diff --git a/cloudstack/resource_cloudstack_role_test.go b/cloudstack/resource_cloudstack_role_test.go index 35eacf54..8ca0e194 100644 --- a/cloudstack/resource_cloudstack_role_test.go +++ b/cloudstack/resource_cloudstack_role_test.go @@ -92,9 +92,23 @@ func testAccCheckCloudStackRoleDestroy(s *terraform.State) error { return fmt.Errorf("No Role ID is set") } - _, _, err := cs.Role.GetRoleByID(rs.Primary.ID) - if err == nil { - return fmt.Errorf("Role %s still exists", rs.Primary.ID) + // Use a defer/recover to catch the panic that might occur when trying to access l.Roles[0] + var err error + func() { + defer func() { + if r := recover(); r != nil { + // If a panic occurs, it means the role doesn't exist, which is what we want + err = nil + } + }() + r, _, e := cs.Role.GetRoleByID(rs.Primary.ID) + if e == nil && r != nil && r.Id == rs.Primary.ID { + err = fmt.Errorf("Role %s still exists", rs.Primary.ID) + } + }() + + if err != nil { + return err } } @@ -106,5 +120,6 @@ resource "cloudstack_role" "foo" { name = "terraform-role" description = "terraform test role" is_public = true + type = "User" } `