From 5780ac9095aa62096530df0ca76d786ee9d049ba Mon Sep 17 00:00:00 2001 From: Sivaanand Murugesan Date: Mon, 18 Nov 2024 12:44:42 +0530 Subject: [PATCH 1/2] PLT-1455: Added support for role management via terrafrom (#542) * PLT-1480:Added permission attribute support (#539) * PLT-1483: Added role management suppport in terraform. (#540) * PLT-1482:Added import support for resource role (#541) * PLT-1482:Added import support for resource role * added import support * updated sdk * reviewable fix --- docs/data-sources/role.md | 15 +- docs/resources/role.md | 76 ++++++++ .../spectrocloud_role/data-source.tf | 14 +- .../resources/spectrocloud_role/providers.tf | 28 +++ .../resources/spectrocloud_role/resource.tf | 16 ++ .../terraform.template.tfvars | 4 + go.mod | 2 +- go.sum | 4 +- spectrocloud/data_source_role.go | 25 ++- spectrocloud/provider.go | 1 + spectrocloud/resource_role.go | 165 ++++++++++++++++++ spectrocloud/resource_role_test.go | 80 +++++++++ templates/resources/role.md.tmpl | 52 ++++++ 13 files changed, 472 insertions(+), 10 deletions(-) create mode 100644 docs/resources/role.md create mode 100644 examples/resources/spectrocloud_role/providers.tf create mode 100644 examples/resources/spectrocloud_role/resource.tf create mode 100644 examples/resources/spectrocloud_role/terraform.template.tfvars create mode 100644 spectrocloud/resource_role.go create mode 100644 spectrocloud/resource_role_test.go create mode 100644 templates/resources/role.md.tmpl diff --git a/docs/data-sources/role.md b/docs/data-sources/role.md index 54a2b780..eb810997 100644 --- a/docs/data-sources/role.md +++ b/docs/data-sources/role.md @@ -13,11 +13,19 @@ description: |- ## Example Usage ```terraform -data "spectrocloud_role" "role1" { - name = "Project Editor" +data "spectrocloud_role" "role" { + name = "Resource Cluster Admin" # (alternatively) - # id = "5fd0ca727c411c71b55a359c" + # id = "66fbea622947f81fb62294ac" +} + +output "role_id" { + value = data.spectrocloud_role.role.id +} + +output "role_permissions" { + value = data.spectrocloud_role.role.permissions } ``` @@ -31,3 +39,4 @@ data "spectrocloud_role" "role1" { ### Read-Only - `id` (String) The ID of this resource. +- `permissions` (Set of String) List of permissions associated with the role. diff --git a/docs/resources/role.md b/docs/resources/role.md new file mode 100644 index 00000000..e84df7ac --- /dev/null +++ b/docs/resources/role.md @@ -0,0 +1,76 @@ +--- +page_title: "spectrocloud_role Resource - terraform-provider-spectrocloud" +subcategory: "" +description: |- + The role resource allows you to manage roles in Palette. +--- + +# spectrocloud_role (Resource) + + The role resource allows you to manage roles in Palette. + +You can learn more about managing roles in Palette by reviewing the [Roles](https://docs.spectrocloud.com/glossary-all/#role) guide. + +## Example Usage + +```terraform +variable "roles" { + type = list(string) + default = ["Cluster Admin", "Cluster Profile Editor"] +} + +# Data source loop to retrieve multiple roles +data "spectrocloud_role" "roles" { + for_each = toset(var.roles) + name = each.key +} + +resource "spectrocloud_role" "custom_role" { + name = "Test Cluster Role" + type = "project" + permissions = flatten([for role in data.spectrocloud_role.roles : role.permissions]) +} +``` + +``` +### Importing existing role state & config + +```hcl +# import existing user example + import { + to = spectrocloud_role.test_role + id = "{roleUID}" + } + +# To generate TF configuration. + terraform plan -generate-config-out=test_role.tf + +# To import State file + terraform import spectrocloud_role.test_role {roleUID} +``` + + +## Schema + +### Required + +- `name` (String) The name of the role. +- `permissions` (Set of String) The permission's assigned to the role. + +### Optional + +- `timeouts` (Block, Optional) (see [below for nested schema](#nestedblock--timeouts)) +- `type` (String) The role type. Allowed values are `project` or `tenant` or `project` + +### Read-Only + +- `id` (String) The ID of this resource. + + +### Nested Schema for `timeouts` + +Optional: + +- `create` (String) +- `delete` (String) +- `update` (String) \ No newline at end of file diff --git a/examples/data-sources/spectrocloud_role/data-source.tf b/examples/data-sources/spectrocloud_role/data-source.tf index cfe3bd33..dcf48362 100644 --- a/examples/data-sources/spectrocloud_role/data-source.tf +++ b/examples/data-sources/spectrocloud_role/data-source.tf @@ -1,6 +1,14 @@ -data "spectrocloud_role" "role1" { - name = "Project Editor" +data "spectrocloud_role" "role" { + name = "Resource Cluster Admin" # (alternatively) - # id = "5fd0ca727c411c71b55a359c" + # id = "66fbea622947f81fb62294ac" } + +output "role_id" { + value = data.spectrocloud_role.role.id +} + +output "role_permissions" { + value = data.spectrocloud_role.role.permissions +} \ No newline at end of file diff --git a/examples/resources/spectrocloud_role/providers.tf b/examples/resources/spectrocloud_role/providers.tf new file mode 100644 index 00000000..f3bdb2e0 --- /dev/null +++ b/examples/resources/spectrocloud_role/providers.tf @@ -0,0 +1,28 @@ +terraform { + required_providers { + spectrocloud = { + version = ">= 0.1" + source = "spectrocloud/spectrocloud" + } + } +} + +variable "sc_host" { + description = "Spectro Cloud Endpoint" + default = "api.spectrocloud.com" +} + +variable "sc_api_key" { + description = "Spectro Cloud API key" +} + +variable "sc_project_name" { + description = "Spectro Cloud Project (e.g: Default)" + default = "Default" +} + +provider "spectrocloud" { + host = var.sc_host + api_key = var.sc_api_key + project_name = var.sc_project_name +} diff --git a/examples/resources/spectrocloud_role/resource.tf b/examples/resources/spectrocloud_role/resource.tf new file mode 100644 index 00000000..9e00feec --- /dev/null +++ b/examples/resources/spectrocloud_role/resource.tf @@ -0,0 +1,16 @@ +variable "roles" { + type = list(string) + default = ["Cluster Admin", "Cluster Profile Editor"] +} + +# Data source loop to retrieve multiple roles +data "spectrocloud_role" "roles" { + for_each = toset(var.roles) + name = each.key +} + +resource "spectrocloud_role" "custom_role" { + name = "Test Cluster Role" + type = "project" + permissions = flatten([for role in data.spectrocloud_role.roles : role.permissions]) +} \ No newline at end of file diff --git a/examples/resources/spectrocloud_role/terraform.template.tfvars b/examples/resources/spectrocloud_role/terraform.template.tfvars new file mode 100644 index 00000000..c7e9d50b --- /dev/null +++ b/examples/resources/spectrocloud_role/terraform.template.tfvars @@ -0,0 +1,4 @@ +# Spectro Cloud credentials +sc_host = "{Enter Spectro Cloud API Host}" #e.g: api.spectrocloud.com (for SaaS) +sc_api_key = "{Enter Spectro Cloud API Key}" +sc_project_name = "{Enter Spectro Cloud Project Name}" #e.g: Default \ No newline at end of file diff --git a/go.mod b/go.mod index 24e2cff1..93bd9cda 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/robfig/cron v1.2.0 github.com/spectrocloud/gomi v1.14.1-0.20240214074114-c19394812368 github.com/spectrocloud/hapi v1.14.1-0.20240214071352-81f589b1d86d - github.com/spectrocloud/palette-sdk-go v0.0.0-20241022161234-3782615736bb + github.com/spectrocloud/palette-sdk-go v0.0.0-20241114040951-b4855be46579 github.com/stretchr/testify v1.9.0 gotest.tools v2.2.0+incompatible k8s.io/api v0.23.5 diff --git a/go.sum b/go.sum index f234f5f9..775b0be9 100644 --- a/go.sum +++ b/go.sum @@ -600,8 +600,8 @@ github.com/spectrocloud/gomi v1.14.1-0.20240214074114-c19394812368 h1:eY0BOyEbGu github.com/spectrocloud/gomi v1.14.1-0.20240214074114-c19394812368/go.mod h1:LlZ9We4kDaELYi7Is0SVmnySuDhwphJLS6ZT4wXxFIk= github.com/spectrocloud/hapi v1.14.1-0.20240214071352-81f589b1d86d h1:OMRbHxMJ1a+G1BYzvUYuMM0wLkYJPdnEOFx16faQ/UY= github.com/spectrocloud/hapi v1.14.1-0.20240214071352-81f589b1d86d/go.mod h1:MktpRPnSXDTHsQrFSD+daJFQ1zMLSR+1gWOL31jVvWE= -github.com/spectrocloud/palette-sdk-go v0.0.0-20241022161234-3782615736bb h1:LVeVFAMVdZRhtn1VY3DnDi32ts90r8/RXP5+1RZBZEA= -github.com/spectrocloud/palette-sdk-go v0.0.0-20241022161234-3782615736bb/go.mod h1:dSlNvDS0qwUWTbrYI6P8x981mcbbRHFrBg67v5zl81U= +github.com/spectrocloud/palette-sdk-go v0.0.0-20241114040951-b4855be46579 h1:C8daKBQJbK2DfoIEaHYNXTXaoSNasqMSVnKnc4Q3WyI= +github.com/spectrocloud/palette-sdk-go v0.0.0-20241114040951-b4855be46579/go.mod h1:dSlNvDS0qwUWTbrYI6P8x981mcbbRHFrBg67v5zl81U= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= diff --git a/spectrocloud/data_source_role.go b/spectrocloud/data_source_role.go index 848ae584..8e5baa40 100644 --- a/spectrocloud/data_source_role.go +++ b/spectrocloud/data_source_role.go @@ -2,6 +2,7 @@ package spectrocloud import ( "context" + "github.com/spectrocloud/palette-sdk-go/api/models" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -23,6 +24,15 @@ func dataSourceRole() *schema.Resource { Computed: true, Optional: true, }, + "permissions": { + Type: schema.TypeSet, + Computed: true, + Set: schema.HashString, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Description: "List of permissions associated with the role. ", + }, }, } } @@ -30,15 +40,28 @@ func dataSourceRole() *schema.Resource { func dataSourceRoleRead(_ context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { c := getV1ClientWithResourceContext(m, "") var diags diag.Diagnostics + var role *models.V1Role + var err error + if i, ok := d.GetOk("id"); ok { + role, err = c.GetRoleByID(i.(string)) + if err != nil { + return diag.FromErr(err) + } + } if v, ok := d.GetOk("name"); ok { - role, err := c.GetRole(v.(string)) + role, err = c.GetRole(v.(string)) if err != nil { return diag.FromErr(err) } + } + if role != nil { d.SetId(role.Metadata.UID) if err := d.Set("name", role.Metadata.Name); err != nil { return diag.FromErr(err) } + if err := d.Set("permissions", role.Spec.Permissions); err != nil { + return diag.FromErr(err) + } } return diags } diff --git a/spectrocloud/provider.go b/spectrocloud/provider.go index 2433011e..4925ab7d 100644 --- a/spectrocloud/provider.go +++ b/spectrocloud/provider.go @@ -140,6 +140,7 @@ func New(_ string) func() *schema.Provider { "spectrocloud_workspace": resourceWorkspace(), "spectrocloud_alert": resourceAlert(), "spectrocloud_ssh_key": resourceSSHKey(), + "spectrocloud_role": resourceRole(), }, DataSourcesMap: map[string]*schema.Resource{ "spectrocloud_user": dataSourceUser(), diff --git a/spectrocloud/resource_role.go b/spectrocloud/resource_role.go new file mode 100644 index 00000000..21bd31e7 --- /dev/null +++ b/spectrocloud/resource_role.go @@ -0,0 +1,165 @@ +package spectrocloud + +import ( + "context" + "fmt" + "github.com/spectrocloud/palette-sdk-go/api/models" + "time" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" +) + +func resourceRole() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceRoleCreate, + ReadContext: resourceRoleRead, + UpdateContext: resourceRoleUpdate, + DeleteContext: resourceRoleDelete, + Importer: &schema.ResourceImporter{ + StateContext: resourceRoleImport, + }, + Description: "The role resource allows you to manage roles in Palette.", + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(10 * time.Minute), + Update: schema.DefaultTimeout(10 * time.Minute), + Delete: schema.DefaultTimeout(10 * time.Minute), + }, + SchemaVersion: 2, + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + Description: "The name of the role.", + }, + "type": { + Type: schema.TypeString, + Optional: true, + Default: "project", + ValidateFunc: validation.StringInSlice([]string{"project", "tenant", "resource"}, false), + Description: "The role type. Allowed values are `project` or `tenant` or `project`", + }, + "permissions": { + Type: schema.TypeSet, + Required: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Description: "The permission's assigned to the role.", + }, + }, + } +} + +func resourceRoleCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + c := getV1ClientWithResourceContext(m, "tenant") + var diags diag.Diagnostics + role := toRole(d) + uid, err := c.CreateRole(role) + if err != nil { + return diag.FromErr(err) + } + d.SetId(uid) + return diags +} + +func resourceRoleRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + c := getV1ClientWithResourceContext(m, "tenant") + var diags diag.Diagnostics + role, err := c.GetRoleByID(d.Id()) + if err != nil { + return diag.FromErr(err) + } + err = flattenRole(d, role) + if err != nil { + return diag.FromErr(err) + } + return diags +} + +func resourceRoleUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + c := getV1ClientWithResourceContext(m, "tenant") + var diags diag.Diagnostics + role := toRole(d) + err := c.UpdateRole(role, d.Id()) + if err != nil { + return diag.FromErr(err) + } + return diags +} + +func resourceRoleDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + c := getV1ClientWithResourceContext(m, "tenant") + var diags diag.Diagnostics + err := c.DeleteRole(d.Id()) + if err != nil { + return diag.FromErr(err) + } + return diags +} + +func resourceRoleImport(ctx context.Context, d *schema.ResourceData, m interface{}) ([]*schema.ResourceData, error) { + c := getV1ClientWithResourceContext(m, "tenant") + _, err := c.GetRoleByID(d.Id()) + if err != nil { + return nil, err + } + + diags := resourceRoleRead(ctx, d, m) + if diags.HasError() { + return nil, fmt.Errorf("could not read role for import: %v", diags) + } + return []*schema.ResourceData{d}, nil +} + +func convertInterfaceSliceToStringSlice(input []interface{}) ([]string, error) { + var output []string + for _, item := range input { + str, ok := item.(string) + if !ok { + return nil, fmt.Errorf("item %v is not a string", item) + } + output = append(output, str) + } + return output, nil +} + +func toRole(d *schema.ResourceData) *models.V1Role { + name := d.Get("name").(string) + roleType := d.Get("type").(string) + permission, _ := convertInterfaceSliceToStringSlice(d.Get("permissions").(*schema.Set).List()) + return &models.V1Role{ + Metadata: &models.V1ObjectMeta{ + Annotations: map[string]string{ + "scope": roleType, + }, + LastModifiedTimestamp: models.V1Time{}, + Name: name, + }, + Spec: &models.V1RoleSpec{ + Permissions: permission, + Scope: models.V1Scope(roleType), + Type: "user", + }, + Status: &models.V1RoleStatus{ + IsEnabled: true, + }, + } +} + +func flattenRole(d *schema.ResourceData, role *models.V1Role) error { + var err error + err = d.Set("name", role.Metadata.Name) + if err != nil { + return err + } + err = d.Set("type", role.Spec.Scope) + if err != nil { + return err + } + err = d.Set("permissions", role.Spec.Permissions) + if err != nil { + return err + } + return nil +} diff --git a/spectrocloud/resource_role_test.go b/spectrocloud/resource_role_test.go new file mode 100644 index 00000000..40cbad1b --- /dev/null +++ b/spectrocloud/resource_role_test.go @@ -0,0 +1,80 @@ +package spectrocloud + +import ( + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/spectrocloud/palette-sdk-go/api/models" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestToRole(t *testing.T) { + + d := resourceRole().TestResourceData() + err := d.Set("name", "test-role") + if err != nil { + return + } + err = d.Set("type", "project") + if err != nil { + return + } + err = d.Set("permissions", []interface{}{"bbb"}) + if err != nil { + return + } + + role := toRole(d) + + expected := &models.V1Role{ + Metadata: &models.V1ObjectMeta{ + Annotations: map[string]string{ + "scope": "project", + }, + Name: "test-role", + }, + Spec: &models.V1RoleSpec{ + Permissions: []string{"bbb"}, + Scope: models.V1Scope("project"), + Type: "user", + }, + Status: &models.V1RoleStatus{ + IsEnabled: true, + }, + } + + assert.Equal(t, expected, role) +} + +func TestFlattenRole(t *testing.T) { + d := schema.TestResourceDataRaw(t, map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + }, + "type": { + Type: schema.TypeString, + Required: true, + }, + "permissions": { + Type: schema.TypeSet, + Required: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + }, map[string]interface{}{}) + + role := &models.V1Role{ + Metadata: &models.V1ObjectMeta{ + Name: "test-role", + }, + Spec: &models.V1RoleSpec{ + Permissions: []string{"read", "write"}, + Scope: models.V1Scope("admin"), + }, + } + + err := flattenRole(d, role) + assert.NoError(t, err) + assert.Equal(t, "test-role", d.Get("name")) + assert.Equal(t, "admin", d.Get("type")) + assert.ElementsMatch(t, []interface{}{"read", "write"}, d.Get("permissions").(*schema.Set).List()) +} diff --git a/templates/resources/role.md.tmpl b/templates/resources/role.md.tmpl new file mode 100644 index 00000000..ffd3e236 --- /dev/null +++ b/templates/resources/role.md.tmpl @@ -0,0 +1,52 @@ +--- +page_title: "{{.Name}} {{.Type}} - {{.ProviderName}}" +subcategory: "" +description: |- +{{ .Description | plainmarkdown | trimspace | prefixlines " " }} +--- + +# {{.Name}} ({{.Type}}) + +{{ .Description | plainmarkdown | trimspace | prefixlines " " }} + +You can learn more about managing roles in Palette by reviewing the [Roles](https://docs.spectrocloud.com/glossary-all/#role) guide. + +## Example Usage + +```terraform +variable "roles" { + type = list(string) + default = ["Cluster Admin", "Cluster Profile Editor"] +} + +# Data source loop to retrieve multiple roles +data "spectrocloud_role" "roles" { + for_each = toset(var.roles) + name = each.key +} + +resource "spectrocloud_role" "custom_role" { + name = "Test Cluster Role" + type = "project" + permissions = flatten([for role in data.spectrocloud_role.roles : role.permissions]) +} +``` + +``` +### Importing existing role state & config + +```hcl +# import existing user example + import { + to = spectrocloud_role.test_role + id = "{roleUID}" + } + +# To generate TF configuration. + terraform plan -generate-config-out=test_role.tf + +# To import State file + terraform import spectrocloud_role.test_role {roleUID} +``` + +{{ .SchemaMarkdown | trimspace }} \ No newline at end of file From 0fe74d14ab7a9e8c95af3e3b3cafe4efa509be73 Mon Sep 17 00:00:00 2001 From: Sivaanand Murugesan Date: Mon, 18 Nov 2024 13:05:16 +0530 Subject: [PATCH 2/2] PLT-1454: Added User Management support in terraform (#538) * PLT-1470 (#536) * PLT-1470: Added data source support for team. * initial draft * schema design completed * draft-2 user managements * completed initial drafts * PLT-1454: Added user management and import support. * PLT-1454: Added unit test for user management * added documentation * refreshed sdk * Update resource_user_import.go * updated sdk * reviewable fix --- docs/data-sources/team.md | 42 + docs/resources/user.md | 157 ++++ .../spectrocloud_team/data-source.tf | 14 + .../spectrocloud_team/providers.tf | 28 + .../terraform.template.tfvars | 4 + .../spectrocloud_user/data_source.tf | 49 ++ .../resources/spectrocloud_user/providers.tf | 14 + .../resources/spectrocloud_user/resource.tf | 53 ++ .../terraform.template.tfvars | 4 + .../resources/spectrocloud_user/variables.tf | 43 ++ spectrocloud/data_source_team.go | 66 ++ spectrocloud/provider.go | 3 + spectrocloud/resource_user.go | 721 ++++++++++++++++++ spectrocloud/resource_user_import.go | 24 + spectrocloud/resource_user_test.go | 204 +++++ templates/resources/user.md.tmpl | 92 +++ 16 files changed, 1518 insertions(+) create mode 100644 docs/data-sources/team.md create mode 100644 docs/resources/user.md create mode 100644 examples/data-sources/spectrocloud_team/data-source.tf create mode 100644 examples/data-sources/spectrocloud_team/providers.tf create mode 100644 examples/data-sources/spectrocloud_team/terraform.template.tfvars create mode 100644 examples/resources/spectrocloud_user/data_source.tf create mode 100644 examples/resources/spectrocloud_user/providers.tf create mode 100644 examples/resources/spectrocloud_user/resource.tf create mode 100644 examples/resources/spectrocloud_user/terraform.template.tfvars create mode 100644 examples/resources/spectrocloud_user/variables.tf create mode 100644 spectrocloud/data_source_team.go create mode 100644 spectrocloud/resource_user.go create mode 100644 spectrocloud/resource_user_import.go create mode 100644 spectrocloud/resource_user_test.go create mode 100644 templates/resources/user.md.tmpl diff --git a/docs/data-sources/team.md b/docs/data-sources/team.md new file mode 100644 index 00000000..bdeb3e36 --- /dev/null +++ b/docs/data-sources/team.md @@ -0,0 +1,42 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "spectrocloud_team Data Source - terraform-provider-spectrocloud" +subcategory: "" +description: |- + +--- + +# spectrocloud_team (Data Source) + + + +## Example Usage + +```terraform +data "spectrocloud_team" "team1" { + name = "team2" + + # (alternatively) + # id = "5fd0ca727c411c71b55a359c" +} + +output "team-id" { + value = data.spectrocloud_team.team1.id +} + +output "team-role-ids" { + value = data.spectrocloud_team.team1.role_ids +} +``` + + +## Schema + +### Optional + +- `id` (String) The unique ID of the team. If provided, `name` cannot be used. +- `name` (String) The name of the team. If provided, `id` cannot be used. + +### Read-Only + +- `role_ids` (List of String) The roles id's assigned to the team. diff --git a/docs/resources/user.md b/docs/resources/user.md new file mode 100644 index 00000000..aa97e7de --- /dev/null +++ b/docs/resources/user.md @@ -0,0 +1,157 @@ +--- +page_title: "spectrocloud_user Resource - terraform-provider-spectrocloud" +subcategory: "" +description: |- + Create and manage projects in Palette. +--- + +# spectrocloud_user (Resource) + + Create and manage projects in Palette. + +You can learn more about managing users in Palette by reviewing the [Users](https://docs.spectrocloud.com/user-management/) guide. + +## Example Usage + +An example of creating a user resource with assigned teams and custom roles in Palette. + +```hcl +resource "spectrocloud_user" "user-test"{ + first_name = "tf" + last_name = "test" + email = "test-tf@spectrocloud.com" + team_ids = [data.spectrocloud_team.team2.id] + project_role { + project_id = data.spectrocloud_project.default.id + role_ids = [for r in data.spectrocloud_role.app_roles : r.id] + } + project_role { + project_id = data.spectrocloud_project.ranjith.id + role_ids = [for r in data.spectrocloud_role.app_roles : r.id] + } + + tenant_role = [for t in data.spectrocloud_role.tenant_roles : t.id] + + workspace_role { + project_id = data.spectrocloud_project.default.id + workspace { + id = data.spectrocloud_workspace.workspace.id + role_ids = [for w in data.spectrocloud_role.workspace_roles : w.id] + } + workspace { + id = data.spectrocloud_workspace.workspace2.id + role_ids = ["66fbea622947f81fc26983e6"] + } + } + + resource_role { + project_ids = [data.spectrocloud_project.default.id, data.spectrocloud_project.ranjith.id] + filter_ids = [data.spectrocloud_filter.filter.id] + role_ids = [for r in data.spectrocloud_role.resource_roles : r.id] + } + + resource_role { + project_ids = [data.spectrocloud_project.ranjith.id] + filter_ids = [data.spectrocloud_filter.filter.id] + role_ids = [for re in data.spectrocloud_role.resource_roles_editor : re.id] + } + +} +``` + +The example below demonstrates how to create an user with only assigned teams. + +```hcl +resource "spectrocloud_user" "user-test"{ + first_name = "tf" + last_name = "test" + email = "test-tf@spectrocloud.com" + team_ids = [data.spectrocloud_team.team2.id] +} + + +``` + +### Importing existing user states + +```hcl +# import existing user example + import { + to = spectrocloud_user.test_user + id = "{userUID}" + } + +# To generate TF configuration. + terraform plan -generate-config-out=test_user.tf + +# To import State file + terraform import spectrocloud_user.test_user {userUID} +``` + + + +## Schema + +### Required + +- `email` (String) The email of the user. +- `first_name` (String) The first name of the user. +- `last_name` (String) The last name of the user. + +### Optional + +- `project_role` (Block Set) List of project roles to be associated with the user. (see [below for nested schema](#nestedblock--project_role)) +- `resource_role` (Block Set) (see [below for nested schema](#nestedblock--resource_role)) +- `team_ids` (List of String) The team id's assigned to the user. +- `tenant_role` (Set of String) List of tenant role ids to be associated with the user. +- `timeouts` (Block, Optional) (see [below for nested schema](#nestedblock--timeouts)) +- `workspace_role` (Block Set) List of workspace roles to be associated with the user. (see [below for nested schema](#nestedblock--workspace_role)) + +### Read-Only + +- `id` (String) The ID of this resource. + + +### Nested Schema for `project_role` + +Required: + +- `project_id` (String) Project id to be associated with the user. +- `role_ids` (Set of String) List of project role ids to be associated with the user. + + + +### Nested Schema for `resource_role` + +Required: + +- `filter_ids` (Set of String) List of filter ids. +- `project_ids` (Set of String) Project id's to be associated with the user. +- `role_ids` (Set of String) List of resource role ids to be associated with the user. + + + +### Nested Schema for `timeouts` + +Optional: + +- `create` (String) +- `delete` (String) +- `update` (String) + + + +### Nested Schema for `workspace_role` + +Required: + +- `project_id` (String) Project id to be associated with the user. +- `workspace` (Block Set, Min: 1) List of workspace roles to be associated with the user. (see [below for nested schema](#nestedblock--workspace_role--workspace)) + + +### Nested Schema for `workspace_role.workspace` + +Required: + +- `id` (String) Workspace id to be associated with the user. +- `role_ids` (Set of String) List of workspace role ids to be associated with the user. \ No newline at end of file diff --git a/examples/data-sources/spectrocloud_team/data-source.tf b/examples/data-sources/spectrocloud_team/data-source.tf new file mode 100644 index 00000000..e208a80f --- /dev/null +++ b/examples/data-sources/spectrocloud_team/data-source.tf @@ -0,0 +1,14 @@ +data "spectrocloud_team" "team1" { + name = "team2" + + # (alternatively) + # id = "5fd0ca727c411c71b55a359c" +} + +output "team-id" { + value = data.spectrocloud_team.team1.id +} + +output "team-role-ids" { + value = data.spectrocloud_team.team1.role_ids +} \ No newline at end of file diff --git a/examples/data-sources/spectrocloud_team/providers.tf b/examples/data-sources/spectrocloud_team/providers.tf new file mode 100644 index 00000000..f3bdb2e0 --- /dev/null +++ b/examples/data-sources/spectrocloud_team/providers.tf @@ -0,0 +1,28 @@ +terraform { + required_providers { + spectrocloud = { + version = ">= 0.1" + source = "spectrocloud/spectrocloud" + } + } +} + +variable "sc_host" { + description = "Spectro Cloud Endpoint" + default = "api.spectrocloud.com" +} + +variable "sc_api_key" { + description = "Spectro Cloud API key" +} + +variable "sc_project_name" { + description = "Spectro Cloud Project (e.g: Default)" + default = "Default" +} + +provider "spectrocloud" { + host = var.sc_host + api_key = var.sc_api_key + project_name = var.sc_project_name +} diff --git a/examples/data-sources/spectrocloud_team/terraform.template.tfvars b/examples/data-sources/spectrocloud_team/terraform.template.tfvars new file mode 100644 index 00000000..c7e9d50b --- /dev/null +++ b/examples/data-sources/spectrocloud_team/terraform.template.tfvars @@ -0,0 +1,4 @@ +# Spectro Cloud credentials +sc_host = "{Enter Spectro Cloud API Host}" #e.g: api.spectrocloud.com (for SaaS) +sc_api_key = "{Enter Spectro Cloud API Key}" +sc_project_name = "{Enter Spectro Cloud Project Name}" #e.g: Default \ No newline at end of file diff --git a/examples/resources/spectrocloud_user/data_source.tf b/examples/resources/spectrocloud_user/data_source.tf new file mode 100644 index 00000000..b0d50e68 --- /dev/null +++ b/examples/resources/spectrocloud_user/data_source.tf @@ -0,0 +1,49 @@ + +data "spectrocloud_project" "default" { + name = "Default" +} + +data "spectrocloud_project" "ranjith" { + name = "ranjith" +} + +data "spectrocloud_role" "app_roles" { + for_each = toset(var.app_role_var) + name = each.key +} + +data "spectrocloud_role" "tenant_roles" { + for_each = toset(var.tenant_role_var) + name = each.key +} + +data "spectrocloud_workspace" "workspace" { + name = "test-ws-tf" +} + +data "spectrocloud_workspace" "workspace2" { + name = "test-ws-2" +} + +data "spectrocloud_role" "workspace_roles" { + for_each = toset(var.workspace_role_var) + name = each.key +} + +data "spectrocloud_filter" "filter" { + name = "test-tf" +} + +data "spectrocloud_role" "resource_roles" { + for_each = toset(var.resource_role_var) + name = each.key +} + +data "spectrocloud_role" "resource_roles_editor" { + for_each = toset(var.resource_role_editor_var) + name = each.key +} + +data "spectrocloud_team" "team2" { + name = "team2" +} \ No newline at end of file diff --git a/examples/resources/spectrocloud_user/providers.tf b/examples/resources/spectrocloud_user/providers.tf new file mode 100644 index 00000000..4c109161 --- /dev/null +++ b/examples/resources/spectrocloud_user/providers.tf @@ -0,0 +1,14 @@ +terraform { + required_providers { + spectrocloud = { + version = ">= 0.1" + source = "spectrocloud/spectrocloud" + } + } +} + +provider "spectrocloud" { + host = var.sc_host + api_key = var.sc_api_key + project_name = var.sc_project_name +} diff --git a/examples/resources/spectrocloud_user/resource.tf b/examples/resources/spectrocloud_user/resource.tf new file mode 100644 index 00000000..3e5ea1b2 --- /dev/null +++ b/examples/resources/spectrocloud_user/resource.tf @@ -0,0 +1,53 @@ +resource "spectrocloud_user" "user-test" { + first_name = "tf" + last_name = "test" + email = "test-tf@spectrocloud.com" + team_ids = [data.spectrocloud_team.team2.id] + project_role { + project_id = data.spectrocloud_project.default.id + role_ids = [for r in data.spectrocloud_role.app_roles : r.id] + } + project_role { + project_id = data.spectrocloud_project.ranjith.id + role_ids = [for r in data.spectrocloud_role.app_roles : r.id] + } + + tenant_role = [for t in data.spectrocloud_role.tenant_roles : t.id] + + workspace_role { + project_id = data.spectrocloud_project.default.id + workspace { + id = data.spectrocloud_workspace.workspace.id + role_ids = [for w in data.spectrocloud_role.workspace_roles : w.id] + } + workspace { + id = data.spectrocloud_workspace.workspace2.id + role_ids = ["66fbea622947f81fc26983e6"] + } + } + + resource_role { + project_ids = [data.spectrocloud_project.default.id, data.spectrocloud_project.ranjith.id] + filter_ids = [data.spectrocloud_filter.filter.id] + role_ids = [for r in data.spectrocloud_role.resource_roles : r.id] + } + + resource_role { + project_ids = [data.spectrocloud_project.ranjith.id] + filter_ids = [data.spectrocloud_filter.filter.id] + role_ids = [for re in data.spectrocloud_role.resource_roles_editor : re.id] + } + +} + +# import existing user example +#import { +# to = spectrocloud_user.test_user +# id = "66fcb5fe19eb6dc880776d59" +#} + +# To generate TF configuration. +#terraform plan -generate-config-out=test_user.tf + +# To import State file +#terraform import spectrocloud_user.test_user 672c5ae21adfa1c28c9e37c9 \ No newline at end of file diff --git a/examples/resources/spectrocloud_user/terraform.template.tfvars b/examples/resources/spectrocloud_user/terraform.template.tfvars new file mode 100644 index 00000000..c7e9d50b --- /dev/null +++ b/examples/resources/spectrocloud_user/terraform.template.tfvars @@ -0,0 +1,4 @@ +# Spectro Cloud credentials +sc_host = "{Enter Spectro Cloud API Host}" #e.g: api.spectrocloud.com (for SaaS) +sc_api_key = "{Enter Spectro Cloud API Key}" +sc_project_name = "{Enter Spectro Cloud Project Name}" #e.g: Default \ No newline at end of file diff --git a/examples/resources/spectrocloud_user/variables.tf b/examples/resources/spectrocloud_user/variables.tf new file mode 100644 index 00000000..428b7837 --- /dev/null +++ b/examples/resources/spectrocloud_user/variables.tf @@ -0,0 +1,43 @@ +variable "sc_host" { + description = "Spectro Cloud Endpoint" + default = "api.spectrocloud.com" +} + +variable "sc_api_key" { + description = "Spectro Cloud API key" +} + +variable "sc_project_name" { + description = "Spectro Cloud Project (e.g: Default)" + default = "Default" +} + +variable "ssh_key_value" { + description = "ssh key value" + default = "ssh-rsa ...... == test@test.com" +} + +variable "tenant_role_var" { + type = list(string) + default = ["Tenant Admin", "Tenant User Admin"] +} + +variable "app_role_var" { + type = list(string) + default = ["App Deployment Admin", "App Deployment Editor"] +} + +variable "workspace_role_var" { + type = list(string) + default = ["Workspace Admin", "Workspace Operator"] +} + +variable "resource_role_var" { + type = list(string) + default = ["Resource Cluster Admin", "Resource Cluster Profile Admin"] +} + +variable "resource_role_editor_var" { + type = list(string) + default = ["Resource Cluster Editor", "Resource Cluster Profile Editor"] +} \ No newline at end of file diff --git a/spectrocloud/data_source_team.go b/spectrocloud/data_source_team.go new file mode 100644 index 00000000..a62c408d --- /dev/null +++ b/spectrocloud/data_source_team.go @@ -0,0 +1,66 @@ +package spectrocloud + +import ( + "context" + "github.com/spectrocloud/palette-sdk-go/api/models" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func dataSourceTeam() *schema.Resource { + return &schema.Resource{ + ReadContext: dataSourceTeamRead, + + Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeString, + Computed: true, + Optional: true, + ConflictsWith: []string{"name"}, + Description: "The unique ID of the team. If provided, `name` cannot be used.", + }, + "name": { + Type: schema.TypeString, + Optional: true, + Description: "The name of the team. If provided, `id` cannot be used.", + }, + "role_ids": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Description: "The roles id's assigned to the team.", + }, + }, + } +} + +func dataSourceTeamRead(_ context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + c := getV1ClientWithResourceContext(m, "") + var diags diag.Diagnostics + var team *models.V1Team + var err error + if v, ok := d.GetOk("name"); ok { + team, err = c.GetTeamWithName(v.(string)) + if err != nil { + return diag.FromErr(err) + } + } else { + if val, okay := d.GetOk("id"); okay && val != "" { + team, err = c.GetTeam(val.(string)) + if err != nil { + return diag.FromErr(err) + } + } + } + if team != nil { + d.SetId(team.Metadata.UID) + if err := d.Set("name", team.Metadata.Name); err != nil { + return diag.FromErr(err) + } + if err := d.Set("role_ids", team.Spec.Roles); err != nil { + return diag.FromErr(err) + } + } + return diags +} diff --git a/spectrocloud/provider.go b/spectrocloud/provider.go index 4925ab7d..029d7638 100644 --- a/spectrocloud/provider.go +++ b/spectrocloud/provider.go @@ -140,9 +140,12 @@ func New(_ string) func() *schema.Provider { "spectrocloud_workspace": resourceWorkspace(), "spectrocloud_alert": resourceAlert(), "spectrocloud_ssh_key": resourceSSHKey(), + "spectrocloud_user": resourceUser(), "spectrocloud_role": resourceRole(), }, DataSourcesMap: map[string]*schema.Resource{ + "spectrocloud_team": dataSourceTeam(), + "spectrocloud_user": dataSourceUser(), "spectrocloud_project": dataSourceProject(), diff --git a/spectrocloud/resource_user.go b/spectrocloud/resource_user.go new file mode 100644 index 00000000..7813d742 --- /dev/null +++ b/spectrocloud/resource_user.go @@ -0,0 +1,721 @@ +package spectrocloud + +import ( + "bytes" + "context" + "fmt" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + "github.com/spectrocloud/palette-sdk-go/client" + "regexp" + "sort" + "time" + + "github.com/spectrocloud/palette-sdk-go/api/models" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func resourceUser() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceUserCreate, + ReadContext: resourceUserRead, + UpdateContext: resourceUserUpdate, + DeleteContext: resourceUserDelete, + Importer: &schema.ResourceImporter{ + StateContext: resourceUserImport, + }, + + Description: "Create and manage projects in Palette.", + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(10 * time.Minute), + Update: schema.DefaultTimeout(10 * time.Minute), + Delete: schema.DefaultTimeout(10 * time.Minute), + }, + + SchemaVersion: 2, + Schema: map[string]*schema.Schema{ + "first_name": { + Type: schema.TypeString, + Required: true, + Description: "The first name of the user.", + }, + "last_name": { + Type: schema.TypeString, + Required: true, + Description: "The last name of the user.", + }, + "email": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringMatch( + regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`), + "must be a valid email address", + ), + Description: "The email of the user.", + }, + "team_ids": { + Type: schema.TypeList, + Optional: true, + ForceNew: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Description: "The team id's assigned to the user.", + }, + "project_role": { + Type: schema.TypeSet, + Set: resourceUserProjectRoleMappingHash, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "project_id": { + Type: schema.TypeString, + Required: true, + Description: "Project id to be associated with the user.", + }, + "role_ids": { + Type: schema.TypeSet, + Required: true, + Set: schema.HashString, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Description: "List of project role ids to be associated with the user. ", + }, + }, + }, + Description: "List of project roles to be associated with the user. ", + }, + "tenant_role": { + Type: schema.TypeSet, + Optional: true, + Set: schema.HashString, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Description: "List of tenant role ids to be associated with the user. ", + }, + "workspace_role": { + Type: schema.TypeSet, + Set: resourceUserWorkspaceRoleMappingHash, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "project_id": { + Type: schema.TypeString, + Required: true, + Description: "Project id to be associated with the user.", + }, + "workspace": { + Type: schema.TypeSet, + Set: resourceUserWorkspaceRoleMappingHashInternal, + Required: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeString, + Required: true, + Description: "Workspace id to be associated with the user.", + }, + "role_ids": { + Type: schema.TypeSet, + Set: schema.HashString, + Required: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Description: "List of workspace role ids to be associated with the user.", + }, + }, + }, + Description: "List of workspace roles to be associated with the user. ", + }, + }, + }, + Description: "List of workspace roles to be associated with the user. ", + }, + "resource_role": { + Type: schema.TypeSet, + Set: resourceUserResourceRoleMappingHash, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "project_ids": { + Type: schema.TypeSet, + Set: schema.HashString, + Required: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Description: "Project id's to be associated with the user.", + }, + "filter_ids": { + Type: schema.TypeSet, + Set: schema.HashString, + Required: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Description: "List of filter ids.", + }, + "role_ids": { + Type: schema.TypeSet, + Set: schema.HashString, + Required: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Description: "List of resource role ids to be associated with the user.", + }, + }, + }, + }, + }, + } +} + +func resourceUserCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + + c := getV1ClientWithResourceContext(m, "tenant") + var diags diag.Diagnostics + user := toUser(d) + uid, err := c.CreateUser(user) + if err != nil { + return diag.FromErr(err) + } + d.SetId(uid) + //creating roles + if pRoles, ok := d.GetOk("project_role"); ok && pRoles != nil { + projectRole := toUserProjectRoleMapping(d) + err := c.AssociateUserProjectRole(uid, projectRole) + if err != nil { + _ = c.DeleteUser(uid) + return diag.FromErr(err) + } + } + + if rRoles, ok := d.GetOk("tenant_role"); ok && rRoles != nil { + tenantRole := toUserTenantRoleMapping(d) + err := c.AssociateUserTenantRole(uid, tenantRole) + if err != nil { + _ = c.DeleteUser(uid) + return diag.FromErr(err) + } + } + + if wRoles, ok := d.GetOk("workspace_role"); ok && wRoles != nil { + workspaceRole := toUserWorkspaceRoleMapping(d) + err := c.AssociateUserWorkspaceRole(uid, workspaceRole) + if err != nil { + _ = c.DeleteUser(uid) + return diag.FromErr(err) + } + } + + if rRoles, ok := d.GetOk("resource_role"); ok && rRoles != nil { + resourceRoles := toUserResourceRoleMapping(d) + for _, role := range resourceRoles { + err := c.CreateUserResourceRole(uid, role) + if err != nil { + _ = c.DeleteUser(uid) + return diag.FromErr(err) + } + } + } + + return diags +} + +func resourceUserRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + + c := getV1ClientWithResourceContext(m, "tenant") + var diags diag.Diagnostics + + email := d.Get("email").(string) + user, err := c.GetUserSummaryByEmail(email) + if err != nil { + return diag.FromErr(err) + } else if user == nil { + // Deleted - Terraform will recreate it + d.SetId("") + return diags + } + err = flattenUser(user, d, c) + if err != nil { + return diag.FromErr(err) + } + return diags +} + +func resourceUserUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + + c := getV1ClientWithResourceContext(m, "tenant") + uid := d.Id() + var diags diag.Diagnostics + + if d.HasChanges("project_role") { + ops, _ := d.GetChange("project_role") + if len(ops.(*schema.Set).List()) > 0 { + _ = deleteProjectResourceRoles(c, ops, uid) + } + projectRole := toUserProjectRoleMapping(d) + if projectRole != nil { + err := c.AssociateUserProjectRole(uid, projectRole) + if err != nil { + return diag.FromErr(err) + } + } + } + if d.HasChanges("tenant_role") { + tenantRole := toUserTenantRoleMapping(d) + err := c.AssociateUserTenantRole(uid, tenantRole) + if err != nil { + return diag.FromErr(err) + } + } + if d.HasChanges("workspace_role") { + ows, _ := d.GetChange("workspace_role") + if len(ows.(*schema.Set).List()) > 0 { + _ = deleteWorkspaceResourceRoles(c, ows, uid) + } + workspaceRole := toUserWorkspaceRoleMapping(d) + if len(workspaceRole.Workspaces) > 0 { + err := c.AssociateUserWorkspaceRole(uid, workspaceRole) + if err != nil { + return diag.FromErr(err) + } + } + + } + if d.HasChanges("resource_role") { + err := deleteUserResourceRoles(c, uid) + if err != nil { + return diag.FromErr(err) + } + resourceRoles := toUserResourceRoleMapping(d) + for _, role := range resourceRoles { + err := c.CreateUserResourceRole(uid, role) + if err != nil { + return diag.FromErr(err) + } + } + } + + return diags +} + +func resourceUserDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + + c := getV1ClientWithResourceContext(m, "tenant") + var diags diag.Diagnostics + + err := c.DeleteUser(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + return diags +} + +func toUserResourceRoleMapping(d *schema.ResourceData) []*models.V1ResourceRolesUpdateEntity { + if resourceRoles, ok := d.GetOk("resource_role"); ok && resourceRoles != nil { + resourceRoleEntities := make([]*models.V1ResourceRolesUpdateEntity, 0) + for _, re := range d.Get("resource_role").(*schema.Set).List() { + resourceEntity := &models.V1ResourceRolesUpdateEntity{ + FilterRefs: setToStringArray(re.(map[string]interface{})["filter_ids"]), + ProjectUids: setToStringArray(re.(map[string]interface{})["project_ids"]), + Roles: setToStringArray(re.(map[string]interface{})["role_ids"]), + } + resourceRoleEntities = append(resourceRoleEntities, resourceEntity) + } + return resourceRoleEntities + } + return nil +} + +func toUserProjectRoleMapping(d *schema.ResourceData) *models.V1ProjectRolesPatch { + if projectRoles, ok := d.GetOk("project_role"); ok && projectRoles != nil { + //var role *models.V1ProjectRolesPatch + var projects []*models.V1ProjectRolesPatchProjectsItems0 + for _, r := range projectRoles.(*schema.Set).List() { + projects = append(projects, &models.V1ProjectRolesPatchProjectsItems0{ + ProjectUID: r.(map[string]interface{})["project_id"].(string), + Roles: setToStringArray(r.(map[string]interface{})["role_ids"]), + }) + } + return &models.V1ProjectRolesPatch{ + Projects: projects, + } + } + + return nil +} + +func toUserTenantRoleMapping(d *schema.ResourceData) *models.V1UserRoleUIDs { + roles := make([]string, 0) + if d.Get("tenant_role") != nil { + for _, role := range d.Get("tenant_role").(*schema.Set).List() { + roles = append(roles, role.(string)) + } + } + + return &models.V1UserRoleUIDs{ + Roles: roles, + } +} + +func toUserWorkspaceRoleMapping(d *schema.ResourceData) *models.V1WorkspacesRolesPatch { + workspaces := make([]*models.V1WorkspaceRolesPatch, 0) + workspaceRoleMappings := d.Get("workspace_role").(*schema.Set).List() + + for _, mapping := range workspaceRoleMappings { + data := mapping.(map[string]interface{}) + + for _, workspace := range data["workspace"].(*schema.Set).List() { + workspaceData := workspace.(map[string]interface{}) + roles := make([]string, 0) + if workspaceData["role_ids"] != nil { + for _, role := range workspaceData["role_ids"].(*schema.Set).List() { + roles = append(roles, role.(string)) + } + } + workspaces = append(workspaces, &models.V1WorkspaceRolesPatch{ + UID: workspaceData["id"].(string), + Roles: roles, + }) + } + + } + + return &models.V1WorkspacesRolesPatch{ + Workspaces: workspaces, + } +} + +func setToStringArray(ids interface{}) []string { + idList := make([]string, 0) + for _, id := range ids.(*schema.Set).List() { + idList = append(idList, id.(string)) + } + return idList +} + +func deleteWorkspaceResourceRoles(c *client.V1Client, oldWs interface{}, userUID string) error { + oldWorkspaces := oldWs.(*schema.Set).List() + for _, p := range oldWorkspaces { + + inWS := make([]*models.V1WorkspaceRolesPatch, 0) + for _, ws := range p.(map[string]interface{})["workspace"].(*schema.Set).List() { + inWS = append(inWS, &models.V1WorkspaceRolesPatch{ + Roles: []string{}, + UID: ws.(map[string]interface{})["id"].(string), + }) + } + deleteWS := &models.V1WorkspacesRolesPatch{ + Workspaces: inWS, + } + _ = c.AssociateUserWorkspaceRole(userUID, deleteWS) + } + return nil +} + +func deleteProjectResourceRoles(c *client.V1Client, oldPs interface{}, userUID string) error { + oldProjectRoles := oldPs.(*schema.Set).List() + + for _, p := range oldProjectRoles { + deletePR := &models.V1ProjectRolesPatch{ + Projects: []*models.V1ProjectRolesPatchProjectsItems0{ + { + ProjectUID: p.(map[string]interface{})["project_id"].(string), + Roles: []string{}, + }, + }, + } + _ = c.AssociateUserProjectRole(userUID, deletePR) + } + return nil +} + +func deleteUserResourceRoles(c *client.V1Client, userUID string) error { + resourceRoles, _ := c.GetUserResourceRoles(userUID) + for _, re := range resourceRoles { + err := c.DeleteUserResourceRoles(userUID, re.UID) + if err != nil { + return err + } + } + return nil +} + +func flattenUser(user *models.V1UserSummary, d *schema.ResourceData, c *client.V1Client) error { + if user != nil { + if err := d.Set("first_name", user.Spec.FirstName); err != nil { + return err + } + if err := d.Set("last_name", user.Spec.LastName); err != nil { + return err + } + if err := d.Set("email", user.Spec.EmailID); err != nil { + return err + } + + if user.Spec.Teams != nil { + var teamIds []string + for _, team := range user.Spec.Teams { + teamIds = append(teamIds, team.UID) + } + if err := d.Set("team_ids", teamIds); err != nil { + return err + } + } + if err := flattenUserProjectRoleMapping(d, c); err != nil { + return err + } + if err := flattenUserTenantRoleMapping(d, c); err != nil { + return err + } + if err := flattenUserWorkspaceRoleMapping(d, c); err != nil { + return err + } + if err := flattenUserResourceRoleMapping(d, c); err != nil { + return err + } + } + + return nil +} + +func flattenUserResourceRoleMapping(d *schema.ResourceData, c *client.V1Client) error { + userUID := d.Id() + resourceRoles, err := c.GetUserResourceRoles(userUID) + if err != nil { + return err + } + rRoles := make([]interface{}, 0) + for _, rr := range resourceRoles { + rRoles = append(rRoles, map[string]interface{}{ + "project_ids": convertSummaryToIDS(rr.ProjectUids), + "filter_ids": convertSummaryToIDS(rr.FilterRefs), + "role_ids": convertSummaryToIDS(rr.Roles), + }) + } + if err := d.Set("resource_role", rRoles); err != nil { + return err + } + return nil +} + +func flattenUserWorkspaceRoleMapping(d *schema.ResourceData, c *client.V1Client) error { + userUID := d.Id() + workspaceRoles, err := c.GetUserWorkspaceRole(userUID) + if err != nil { + return err + } + wRoles := make([]interface{}, 0) + for _, w := range workspaceRoles.Projects { + wsRoles := make([]interface{}, 0) + for _, wr := range w.Workspaces { + wsIDS := make([]string, 0) + for _, ri := range wr.Roles { + wsIDS = append(wsIDS, ri.UID) + } + wsRoles = append(wsRoles, map[string]interface{}{ + "id": wr.UID, + "role_ids": wsIDS, + }) + } + wRoles = append(wRoles, map[interface{}]interface{}{ + "project_id": w.UID, + "workspace": wsRoles, + }) + } + if err := d.Set("workspace_role", wRoles); err != nil { + return err + } + return nil +} + +func flattenUserTenantRoleMapping(d *schema.ResourceData, c *client.V1Client) error { + userUID := d.Id() + tenantRoles, err := c.GetUserTenantRole(userUID) + if err != nil { + return err + } + var tRoles []string + for _, t := range tenantRoles.Roles { + tRoles = append(tRoles, t.UID) + } + if err := d.Set("tenant_role", tRoles); err != nil { + return err + } + return nil +} + +func flattenUserProjectRoleMapping(d *schema.ResourceData, c *client.V1Client) error { + userUID := d.Id() + projectRoles, err := c.GetUserProjectRole(userUID) + if err != nil { + return err + } + pRoles := make([]interface{}, 0) + for _, p := range projectRoles.Projects { + if len(p.Roles) > 0 { + roles := make([]string, 0) + for _, r := range p.Roles { + roles = append(roles, r.UID) + } + pRoles = append(pRoles, map[string]interface{}{ + "project_id": p.UID, + "role_ids": roles, + }) + } + } + if err := d.Set("project_role", pRoles); err != nil { + return err + } + return nil +} + +func toUser(d *schema.ResourceData) *models.V1UserEntity { + fName := d.Get("first_name").(string) + lName := d.Get("last_name").(string) + user := &models.V1UserEntity{ + Metadata: &models.V1ObjectMeta{ + Name: fName + " " + lName, + }, + Spec: &models.V1UserSpecEntity{ + EmailID: d.Get("email").(string), + FirstName: fName, + LastName: lName, + }, + } + if teams, ok := d.GetOk("team_ids"); ok && teams != nil { + user.Spec.Teams = convertToStrings(teams.([]interface{})) + } + return user +} + +func convertToStrings(input []interface{}) []string { + var output []string + for _, v := range input { + if str, ok := v.(string); ok { + output = append(output, str) + } + } + return output +} + +func convertSummaryToIDS(sum []*models.V1UIDSummary) []string { + var out []string + for _, v := range sum { + out = append(out, v.UID) + } + return out +} + +func resourceUserResourceRoleMappingHash(i interface{}) int { + var buf bytes.Buffer + m := i.(map[string]interface{}) + + // Sort the roles to ensure order does not affect the hash + pids := make([]string, len(m["project_ids"].(*schema.Set).List())) + for i, pid := range m["project_ids"].(*schema.Set).List() { + pids[i] = pid.(string) + } + sort.Strings(pids) + + fids := make([]string, len(m["filter_ids"].(*schema.Set).List())) + for i, fid := range m["filter_ids"].(*schema.Set).List() { + fids[i] = fid.(string) + } + sort.Strings(fids) + + rids := make([]string, len(m["role_ids"].(*schema.Set).List())) + for i, rid := range m["role_ids"].(*schema.Set).List() { + rids[i] = rid.(string) + } + sort.Strings(rids) + + //buf.WriteString(fmt.Sprintf("%s-", m["project_id"].(string))) + + for _, id := range pids { + buf.WriteString(fmt.Sprintf("%s-", id)) + } + for _, id := range fids { + buf.WriteString(fmt.Sprintf("%s-", id)) + } + for _, id := range rids { + buf.WriteString(fmt.Sprintf("%s-", id)) + } + + return int(hash(buf.String())) +} + +func resourceUserWorkspaceRoleMappingHash(i interface{}) int { + var buf bytes.Buffer + m := i.(map[string]interface{}) + + // Hash project id + if v, ok := m["project_id"].(string); ok { + h := schema.HashString(v) + buf.WriteString(fmt.Sprintf("%d-", h)) + } + + // Hash workspaces + if v, ok := m["workspace"].(*schema.Set); ok { + // Sort workspace hashes to ensure consistent ordering + workspaces := v.List() + hashes := make([]int, len(workspaces)) + for i, workspaceInterface := range workspaces { + workspace := workspaceInterface.(map[string]interface{}) + hashes[i] = resourceUserWorkspaceRoleMappingHashInternal(workspace) + } + sort.Ints(hashes) + + for _, h := range hashes { + buf.WriteString(fmt.Sprintf("%d-", h)) + } + } + + return int(hash(buf.String())) +} + +func resourceUserWorkspaceRoleMappingHashInternal(workspace interface{}) int { + var buf bytes.Buffer + m := workspace.(map[string]interface{}) + // Sort the roles to ensure order does not affect the hash + roles := make([]string, len(m["role_ids"].(*schema.Set).List())) + for i, role := range m["role_ids"].(*schema.Set).List() { + roles[i] = role.(string) + } + sort.Strings(roles) + + buf.WriteString(fmt.Sprintf("%s-", m["id"].(string))) + + for _, role := range roles { + buf.WriteString(fmt.Sprintf("%s-", role)) + } + + return int(hash(buf.String())) +} + +func resourceUserProjectRoleMappingHash(i interface{}) int { + var buf bytes.Buffer + m := i.(map[string]interface{}) + + // Sort the roles to ensure order does not affect the hash + roles := make([]string, len(m["role_ids"].(*schema.Set).List())) + for i, role := range m["role_ids"].(*schema.Set).List() { + roles[i] = role.(string) + } + sort.Strings(roles) + + buf.WriteString(fmt.Sprintf("%s-", m["project_id"].(string))) + + for _, role := range roles { + buf.WriteString(fmt.Sprintf("%s-", role)) + } + + return int(hash(buf.String())) +} diff --git a/spectrocloud/resource_user_import.go b/spectrocloud/resource_user_import.go new file mode 100644 index 00000000..269fd54c --- /dev/null +++ b/spectrocloud/resource_user_import.go @@ -0,0 +1,24 @@ +package spectrocloud + +import ( + "context" + "fmt" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func resourceUserImport(ctx context.Context, d *schema.ResourceData, m interface{}) ([]*schema.ResourceData, error) { + c := getV1ClientWithResourceContext(m, "tenant") + user, err := c.GetUserByID(d.Id()) + if err != nil { + return nil, err + } + err = d.Set("email", user.Spec.EmailID) + if err != nil { + return nil, err + } + diags := resourceUserRead(ctx, d, m) + if diags.HasError() { + return nil, fmt.Errorf("could not read user for import: %v", diags) + } + return []*schema.ResourceData{d}, nil +} diff --git a/spectrocloud/resource_user_test.go b/spectrocloud/resource_user_test.go new file mode 100644 index 00000000..8325771b --- /dev/null +++ b/spectrocloud/resource_user_test.go @@ -0,0 +1,204 @@ +package spectrocloud + +import ( + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/spectrocloud/palette-sdk-go/api/models" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestConvertSummaryToIDS(t *testing.T) { + tests := []struct { + name string + input []*models.V1UIDSummary + expected []string + }{ + { + name: "Multiple UIDs", + input: []*models.V1UIDSummary{ + {UID: "uid1"}, + {UID: "uid2"}, + {UID: "uid3"}, + }, + expected: []string{"uid1", "uid2", "uid3"}, + }, + { + name: "Empty input", + input: []*models.V1UIDSummary{}, + expected: []string(nil), + }, + { + name: "Single UID", + input: []*models.V1UIDSummary{ + {UID: "singleUID"}, + }, + expected: []string{"singleUID"}, + }, + { + name: "Nil input", + input: nil, + expected: []string(nil), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := convertSummaryToIDS(tt.input) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestConvertToStrings(t *testing.T) { + tests := []struct { + name string + input []interface{} + expected []string + }{ + { + name: "All strings", + input: []interface{}{"one", "two", "three"}, + expected: []string{"one", "two", "three"}, + }, + { + name: "Mixed types", + input: []interface{}{"one", 2, "three", 4.0, true}, + expected: []string{"one", "three"}, + }, + { + name: "No strings", + input: []interface{}{1, 2, 3, 4.5, false}, + expected: []string(nil), + }, + { + name: "Empty input", + input: []interface{}{}, + expected: []string(nil), + }, + { + name: "Nil input", + input: nil, + expected: []string(nil), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := convertToStrings(tt.input) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestToUser(t *testing.T) { + resourceData := schema.TestResourceDataRaw(t, map[string]*schema.Schema{ + "first_name": {Type: schema.TypeString, Required: true}, + "last_name": {Type: schema.TypeString, Required: true}, + "email": {Type: schema.TypeString, Required: true}, + "team_ids": {Type: schema.TypeList, Optional: true, Elem: &schema.Schema{Type: schema.TypeString}}, + }, map[string]interface{}{ + "first_name": "John", + "last_name": "Doe", + "email": "johndoe@example.com", + "team_ids": []interface{}{"team1", "team2"}, + }) + + user := toUser(resourceData) + + expectedUser := &models.V1UserEntity{ + Metadata: &models.V1ObjectMeta{ + Name: "John Doe", + }, + Spec: &models.V1UserSpecEntity{ + EmailID: "johndoe@example.com", + FirstName: "John", + LastName: "Doe", + Teams: []string{"team1", "team2"}, + }, + } + + assert.Equal(t, expectedUser, user) +} + +func TestToUserNoTeams(t *testing.T) { + resourceData := schema.TestResourceDataRaw(t, map[string]*schema.Schema{ + "first_name": {Type: schema.TypeString, Required: true}, + "last_name": {Type: schema.TypeString, Required: true}, + "email": {Type: schema.TypeString, Required: true}, + "team_ids": {Type: schema.TypeList, Optional: true, Elem: &schema.Schema{Type: schema.TypeString}}, + }, map[string]interface{}{ + "first_name": "Alice", + "last_name": "Smith", + "email": "alice@example.com", + }) + + user := toUser(resourceData) + + expectedUser := &models.V1UserEntity{ + Metadata: &models.V1ObjectMeta{ + Name: "Alice Smith", + }, + Spec: &models.V1UserSpecEntity{ + EmailID: "alice@example.com", + FirstName: "Alice", + LastName: "Smith", + Teams: nil, + }, + } + + assert.Equal(t, expectedUser, user) +} + +func TestSetToStringArray(t *testing.T) { + // Create a schema.Set with some string values + input := schema.NewSet(schema.HashString, []interface{}{"id1", "id2", "id3"}) + + // Call the function with the set + result := setToStringArray(input) + + // Define the expected output + expected := []string{"id1", "id2", "id3"} + + // Assert that the result matches the expected output + assert.ElementsMatch(t, expected, result) +} + +func TestSetToStringArrayEmptySet(t *testing.T) { + // Create an empty schema.Set + input := schema.NewSet(schema.HashString, []interface{}{}) + + // Call the function with the empty set + result := setToStringArray(input) + + // Define the expected output for an empty set + expected := []string{} + + // Assert that the result matches the expected output + assert.Equal(t, expected, result) +} + +func TestToUserWorkspaceRoleMappingEmpty(t *testing.T) { + d := schema.TestResourceDataRaw(t, map[string]*schema.Schema{ + "workspace_role": { + Type: schema.TypeSet, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "workspace": { + Type: schema.TypeSet, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "id": {Type: schema.TypeString}, + "role_ids": {Type: schema.TypeSet, Elem: &schema.Schema{Type: schema.TypeString}}, + }, + }, + }, + }, + }, + }, + }, map[string]interface{}{"workspace_role": []interface{}{}}) + + result := toUserWorkspaceRoleMapping(d) + expected := &models.V1WorkspacesRolesPatch{Workspaces: []*models.V1WorkspaceRolesPatch{}} + + assert.Equal(t, expected, result) +} diff --git a/templates/resources/user.md.tmpl b/templates/resources/user.md.tmpl new file mode 100644 index 00000000..c399cc49 --- /dev/null +++ b/templates/resources/user.md.tmpl @@ -0,0 +1,92 @@ +--- +page_title: "{{.Name}} {{.Type}} - {{.ProviderName}}" +subcategory: "" +description: |- +{{ .Description | plainmarkdown | trimspace | prefixlines " " }} +--- + +# {{.Name}} ({{.Type}}) + +{{ .Description | plainmarkdown | trimspace | prefixlines " " }} + +You can learn more about managing users in Palette by reviewing the [Users](https://docs.spectrocloud.com/user-management/) guide. + +## Example Usage + +An example of creating a user resource with assigned teams and custom roles in Palette. + +```hcl +resource "spectrocloud_user" "user-test"{ + first_name = "tf" + last_name = "test" + email = "test-tf@spectrocloud.com" + team_ids = [data.spectrocloud_team.team2.id] + project_role { + project_id = data.spectrocloud_project.default.id + role_ids = [for r in data.spectrocloud_role.app_roles : r.id] + } + project_role { + project_id = data.spectrocloud_project.ranjith.id + role_ids = [for r in data.spectrocloud_role.app_roles : r.id] + } + + tenant_role = [for t in data.spectrocloud_role.tenant_roles : t.id] + + workspace_role { + project_id = data.spectrocloud_project.default.id + workspace { + id = data.spectrocloud_workspace.workspace.id + role_ids = [for w in data.spectrocloud_role.workspace_roles : w.id] + } + workspace { + id = data.spectrocloud_workspace.workspace2.id + role_ids = ["66fbea622947f81fc26983e6"] + } + } + + resource_role { + project_ids = [data.spectrocloud_project.default.id, data.spectrocloud_project.ranjith.id] + filter_ids = [data.spectrocloud_filter.filter.id] + role_ids = [for r in data.spectrocloud_role.resource_roles : r.id] + } + + resource_role { + project_ids = [data.spectrocloud_project.ranjith.id] + filter_ids = [data.spectrocloud_filter.filter.id] + role_ids = [for re in data.spectrocloud_role.resource_roles_editor : re.id] + } + +} +``` + +The example below demonstrates how to create an user with only assigned teams. + +```hcl +resource "spectrocloud_user" "user-test"{ + first_name = "tf" + last_name = "test" + email = "test-tf@spectrocloud.com" + team_ids = [data.spectrocloud_team.team2.id] +} + + +``` + +### Importing existing user states + +```hcl +# import existing user example + import { + to = spectrocloud_user.test_user + id = "{userUID}" + } + +# To generate TF configuration. + terraform plan -generate-config-out=test_user.tf + +# To import State file + terraform import spectrocloud_user.test_user {userUID} +``` + + +{{ .SchemaMarkdown | trimspace }} \ No newline at end of file