diff --git a/cloudstack/data_source_cloudstack_role.go b/cloudstack/data_source_cloudstack_role.go new file mode 100644 index 00000000..2f93b347 --- /dev/null +++ b/cloudstack/data_source_cloudstack_role.go @@ -0,0 +1,159 @@ +// +// 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 ( + "encoding/json" + "fmt" + "log" + "regexp" + "strings" + + "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(), + + //Computed values + "id": { + Type: schema.TypeString, + Computed: true, + }, + + "name": { + Type: schema.TypeString, + 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) + p := cs.Role.NewListRolesParams() + + 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 + + for _, r := range csRoles.Roles { + match, err := applyRoleFilters(r, filters.(*schema.Set)) + if err != nil { + return err + } + if match { + role = r + break + } + } + + 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) + d.Set("description", role.Description) + d.Set("is_public", role.Ispublic) + + 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 new file mode 100644 index 00000000..4e289db2 --- /dev/null +++ b/cloudstack/data_source_cloudstack_role_test.go @@ -0,0 +1,62 @@ +// +// 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-testing/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 + type = "User" +} + +data "cloudstack_role" "role" { + filter { + name = "name" + value = "${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..eea50b88 --- /dev/null +++ b/cloudstack/resource_cloudstack_role.go @@ -0,0 +1,177 @@ +// +// 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: "The type of the role, valid options are: Admin, ResourceAdmin, DomainAdmin, User", + }, + "description": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "is_public": { + 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.", + }, + }, + } +} + +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) + + // 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 { + 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..8ca0e194 --- /dev/null +++ b/cloudstack/resource_cloudstack_role_test.go @@ -0,0 +1,125 @@ +// +// 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-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/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") + } + + // 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 + } + } + + return nil +} + +const testAccCloudStackRole_basic = ` +resource "cloudstack_role" "foo" { + name = "terraform-role" + description = "terraform test role" + is_public = true + type = "User" +} +` 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..4d77eb73 --- /dev/null +++ b/website/docs/d/role.html.markdown @@ -0,0 +1,59 @@ +--- +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" { + filter { + name = "name" + value = "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: + +* `filter` - (Required) One or more name/value pairs to filter off of. See the example below for usage. + +## Filter Example + +```hcl +data "cloudstack_role" "admin" { + filter { + name = "name" + value = "Admin" + } +} +``` + +## 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..afb7835f --- /dev/null +++ b/website/docs/r/role.html.markdown @@ -0,0 +1,54 @@ +--- +subcategory: "" +layout: "cloudstack" +page_title: "CloudStack: cloudstack_role" +description: |- + Creates a role. +--- + +# cloudstack_role + +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 + +The following arguments are supported: + +* `name` - (Required) The name of the role. +* `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 `true`. +* `role_id` - (Optional) ID of the role to be cloned from. Either `role_id` or `type` must be specified. + +## 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