From 6ef35c934bc703244cbec08a021470400cc9b30d Mon Sep 17 00:00:00 2001 From: Michal Wasilewski Date: Tue, 10 Oct 2023 11:31:59 +0200 Subject: [PATCH 01/13] nit: order of resources Signed-off-by: Michal Wasilewski --- spacelift/provider.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spacelift/provider.go b/spacelift/provider.go index e8353422..e8e707a4 100644 --- a/spacelift/provider.go +++ b/spacelift/provider.go @@ -109,6 +109,7 @@ func Provider(commit, version string) plugin.ProviderFunc { "spacelift_drift_detection": resourceDriftDetection(), "spacelift_environment_variable": resourceEnvironmentVariable(), "spacelift_gcp_service_account": resourceGCPServiceAccount(), + "spacelift_idp_group_mapping": resourceIdpGroupMapping(), "spacelift_module": resourceModule(), "spacelift_mounted_file": resourceMountedFile(), "spacelift_policy_attachment": resourcePolicyAttachment(), @@ -125,7 +126,6 @@ func Provider(commit, version string) plugin.ProviderFunc { "spacelift_stack_aws_role": resourceStackAWSRole(), // deprecated "spacelift_stack_gcp_service_account": resourceStackGCPServiceAccount(), // deprecated "spacelift_terraform_provider": resourceTerraformProvider(), - "spacelift_idp_group_mapping": resourceIdpGroupMapping(), "spacelift_vcs_agent_pool": resourceVCSAgentPool(), "spacelift_webhook": resourceWebhook(), "spacelift_named_webhook": resourceNamedWebhook(), From ed2a21e8328296936bbf2dc65b11ee618a02f1eb Mon Sep 17 00:00:00 2001 From: Michal Wasilewski Date: Tue, 10 Oct 2023 13:00:55 +0200 Subject: [PATCH 02/13] interim commit Signed-off-by: Michal Wasilewski --- spacelift/internal/structs/user.go | 7 ++ spacelift/internal/structs/user_input.go | 8 ++ spacelift/provider.go | 1 + spacelift/resource_user.go | 134 +++++++++++++++++++++++ 4 files changed, 150 insertions(+) create mode 100644 spacelift/internal/structs/user.go create mode 100644 spacelift/internal/structs/user_input.go create mode 100644 spacelift/resource_user.go diff --git a/spacelift/internal/structs/user.go b/spacelift/internal/structs/user.go new file mode 100644 index 00000000..211c91f9 --- /dev/null +++ b/spacelift/internal/structs/user.go @@ -0,0 +1,7 @@ +package structs + +type User struct { + ID string `graphql:"id"` + Username string `graphql:"username"` + AccessRules []SpaceAccessRule `graphql:"accessRules"` +} diff --git a/spacelift/internal/structs/user_input.go b/spacelift/internal/structs/user_input.go new file mode 100644 index 00000000..42ba2c19 --- /dev/null +++ b/spacelift/internal/structs/user_input.go @@ -0,0 +1,8 @@ +package structs + +import "github.com/shurcooL/graphql" + +type ManagedUserUpdateInput struct { + ID graphql.ID `json:"id"` + AccessRules []SpaceAccessRuleInput `json:"accessRules"` +} diff --git a/spacelift/provider.go b/spacelift/provider.go index e8e707a4..ebedbf06 100644 --- a/spacelift/provider.go +++ b/spacelift/provider.go @@ -126,6 +126,7 @@ func Provider(commit, version string) plugin.ProviderFunc { "spacelift_stack_aws_role": resourceStackAWSRole(), // deprecated "spacelift_stack_gcp_service_account": resourceStackGCPServiceAccount(), // deprecated "spacelift_terraform_provider": resourceTerraformProvider(), + "spacelift_user": resourceUser(), "spacelift_vcs_agent_pool": resourceVCSAgentPool(), "spacelift_webhook": resourceWebhook(), "spacelift_named_webhook": resourceNamedWebhook(), diff --git a/spacelift/resource_user.go b/spacelift/resource_user.go new file mode 100644 index 00000000..54ba6d91 --- /dev/null +++ b/spacelift/resource_user.go @@ -0,0 +1,134 @@ +package spacelift + +import ( + "context" + + "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" + + "github.com/spacelift-io/terraform-provider-spacelift/spacelift/internal" + "github.com/spacelift-io/terraform-provider-spacelift/spacelift/internal/structs" + "github.com/spacelift-io/terraform-provider-spacelift/spacelift/internal/validations" +) + +func resourceUser() *schema.Resource { + return &schema.Resource{ + Description: "" + + "`spacelift_user` represents a Spacelift user. ", + CreateContext: resourceUserCreate, + ReadContext: resourceUserRead, + UpdateContext: resourceUserUpdate, + DeleteContext: resourceUserDelete, + + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: map[string]*schema.Schema{ + "username": { + Type: schema.TypeString, + Description: "Username of the user", + Required: true, + }, + "policy": { + Type: schema.TypeList, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "space_id": { + Type: schema.TypeString, + Description: "ID (slug) of the space the user group has access to", + Required: true, + ValidateDiagFunc: validations.DisallowEmptyString, + }, + "role": { + Type: schema.TypeString, + Description: "Type of access to the space. Possible values are: " + + "READ, WRITE, ADMIN", + Required: true, + ValidateFunc: validation.StringInSlice(validAccessLevels, false), + }, + }, + }, + }, + }, + } +} + +func resourceUserCreate(ctx context.Context, d *schema.ResourceData, i interface{}) diag.Diagnostics { + // send a create query to the API + var mutation struct { + User *structs.User `graphql:` + } + +} + +func resourceUserRead(ctx context.Context, d *schema.ResourceData, i interface{}) diag.Diagnostics { + // send a read query to the API + var query struct { + User *structs.User `graphql:"managedUser(id: $id)"` + } + variables := map[string]interface{}{"id": toID(d.Id())} + if err := i.(*internal.Client).Query(ctx, "ManagedUserRead", &query, variables); err != nil { + return diag.Errorf("could not query for user: %v", err) + } + + // if the user was not found on the Spacelift side, delete it from TF state + if query.User == nil { + d.SetId("") + return nil + } + + // if found, update the TF state + d.Set("username", query.User.Username) + var accessList []interface{} + for _, a := range query.User.AccessRules { + accessList = append(accessList, map[string]interface{}{ + "space_id": a.Space, + "role": a.SpaceAccessLevel, + }) + } + d.Set("policy", accessList) + + return nil +} + +func resourceUserUpdate(ctx context.Context, d *schema.ResourceData, i interface{}) diag.Diagnostics { + var ret diag.Diagnostics + + // send an update query to the API + var mutation struct { + User *structs.User `graphql:"managedUserUpdate(input: $input)"` + } + variables := map[string]interface{}{ + "input": structs.ManagedUserUpdateInput{ + ID: toID(d.Id()), + AccessRules: getAccessRules(d), + }, + } + if err := i.(*internal.Client).Mutate(ctx, "ManagedUserUpdate", &mutation, variables); err != nil { + ret = append(ret, diag.Errorf("could not update user %s: %v", d.Id(), internal.FromSpaceliftError(err))...) + } + + // send a read query to the API + ret = append(ret, resourceUserRead(ctx, d, i)...) + + return ret + +} + +func resourceUserDelete(ctx context.Context, d *schema.ResourceData, i interface{}) diag.Diagnostics { + // send a delete query to the API + var mutation struct { + User *structs.User `graphql:"managedUserDelete(id: $id)"` + } + variables := map[string]interface{}{"id": toID(d.Id())} + if err := i.(*internal.Client).Mutate(ctx, "ManagedUserDelete", &mutation, variables); err != nil { + return diag.Errorf("could not delete user %s: %v", d.Id(), internal.FromSpaceliftError(err)) + } + + // if the user was deleted, remove it from the TF state as well + d.SetId("") + + return nil +} From 381fca9287ed032d9ab97ec95a2e7f21d80a2ec6 Mon Sep 17 00:00:00 2001 From: Michal Wasilewski Date: Tue, 10 Oct 2023 15:08:03 +0200 Subject: [PATCH 03/13] feat: add support for managing user mapping Signed-off-by: Michal Wasilewski --- .../spacelift_user_mapping/resource.tf | 12 ++++ spacelift/internal/structs/user.go | 1 + spacelift/internal/structs/user_input.go | 9 ++- spacelift/provider.go | 2 +- ...ource_user.go => resource_user_mapping.go} | 60 +++++++++++------ spacelift/resource_user_mapping_test.go | 67 +++++++++++++++++++ 6 files changed, 129 insertions(+), 22 deletions(-) create mode 100644 examples/resources/spacelift_user_mapping/resource.tf rename spacelift/{resource_user.go => resource_user_mapping.go} (60%) create mode 100644 spacelift/resource_user_mapping_test.go diff --git a/examples/resources/spacelift_user_mapping/resource.tf b/examples/resources/spacelift_user_mapping/resource.tf new file mode 100644 index 00000000..8adca364 --- /dev/null +++ b/examples/resources/spacelift_user_mapping/resource.tf @@ -0,0 +1,12 @@ +resource "spacelift_user_mapping" "test" { + email = "johnk@eample.com" + username = "johnk" + policy { + space_id = "root" + role = "ADMIN" + } + policy { + space_id = "legacy" + role = "READ" + } +} diff --git a/spacelift/internal/structs/user.go b/spacelift/internal/structs/user.go index 211c91f9..7532d2b1 100644 --- a/spacelift/internal/structs/user.go +++ b/spacelift/internal/structs/user.go @@ -2,6 +2,7 @@ package structs type User struct { ID string `graphql:"id"` + Email string `graphql:"email"` Username string `graphql:"username"` AccessRules []SpaceAccessRule `graphql:"accessRules"` } diff --git a/spacelift/internal/structs/user_input.go b/spacelift/internal/structs/user_input.go index 42ba2c19..0371dcac 100644 --- a/spacelift/internal/structs/user_input.go +++ b/spacelift/internal/structs/user_input.go @@ -2,7 +2,12 @@ package structs import "github.com/shurcooL/graphql" -type ManagedUserUpdateInput struct { - ID graphql.ID `json:"id"` +type UserInviteInput struct { + Email graphql.String `json:"email"` + Username graphql.String `json:"username"` + AccessRules []SpaceAccessRuleInput `json:"accessRules"` +} + +type UserUpdateInput struct { AccessRules []SpaceAccessRuleInput `json:"accessRules"` } diff --git a/spacelift/provider.go b/spacelift/provider.go index ebedbf06..e24e38e2 100644 --- a/spacelift/provider.go +++ b/spacelift/provider.go @@ -126,7 +126,7 @@ func Provider(commit, version string) plugin.ProviderFunc { "spacelift_stack_aws_role": resourceStackAWSRole(), // deprecated "spacelift_stack_gcp_service_account": resourceStackGCPServiceAccount(), // deprecated "spacelift_terraform_provider": resourceTerraformProvider(), - "spacelift_user": resourceUser(), + "spacelift_user_mapping": resourceUserMapping(), "spacelift_vcs_agent_pool": resourceVCSAgentPool(), "spacelift_webhook": resourceWebhook(), "spacelift_named_webhook": resourceNamedWebhook(), diff --git a/spacelift/resource_user.go b/spacelift/resource_user_mapping.go similarity index 60% rename from spacelift/resource_user.go rename to spacelift/resource_user_mapping.go index 54ba6d91..d3058af1 100644 --- a/spacelift/resource_user.go +++ b/spacelift/resource_user_mapping.go @@ -12,27 +12,35 @@ import ( "github.com/spacelift-io/terraform-provider-spacelift/spacelift/internal/validations" ) -func resourceUser() *schema.Resource { +func resourceUserMapping() *schema.Resource { return &schema.Resource{ Description: "" + - "`spacelift_user` represents a Spacelift user. ", - CreateContext: resourceUserCreate, - ReadContext: resourceUserRead, - UpdateContext: resourceUserUpdate, - DeleteContext: resourceUserDelete, + "`spacelift_user_mapping` represents a mapping between a Spacelift user " + + "(managed using an Identity Provider) and a Policy. A Policy defines " + + "what access rights the user has to a given Space.", + CreateContext: resourceUserMappingCreate, + ReadContext: resourceUserMappingRead, + UpdateContext: resourceUserMappingUpdate, + DeleteContext: resourceUserMappingDelete, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, Schema: map[string]*schema.Schema{ + "email": { + Type: schema.TypeString, + Description: "Email of the user. Used for sending an invitation.", + Required: true, + }, "username": { Type: schema.TypeString, Description: "Username of the user", Required: true, }, "policy": { - Type: schema.TypeList, + Type: schema.TypeList, + Optional: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "space_id": { @@ -55,15 +63,30 @@ func resourceUser() *schema.Resource { } } -func resourceUserCreate(ctx context.Context, d *schema.ResourceData, i interface{}) diag.Diagnostics { - // send a create query to the API +func resourceUserMappingCreate(ctx context.Context, d *schema.ResourceData, i interface{}) diag.Diagnostics { + // send an Invite (create) mutation to the API var mutation struct { - User *structs.User `graphql:` + User *structs.User `graphql:"managedUserInvite(input: $iinput)"` } + variables := map[string]interface{}{ + "input": structs.UserInviteInput{ + Email: toString(d.Get("email")), + Username: toString(d.Get("username")), + AccessRules: getAccessRules(d), + }, + } + if err := i.(*internal.Client).Mutate(ctx, "ManagedUserInvite", &mutation, variables); err != nil { + return diag.Errorf("could not create user %s: %v", toString(d.Get("username")), internal.FromSpaceliftError(err)) + } + + // set the ID in TF state + d.SetId(mutation.User.ID) + // fetch state from remote and write to TF state + return resourceUserMappingRead(ctx, d, i) } -func resourceUserRead(ctx context.Context, d *schema.ResourceData, i interface{}) diag.Diagnostics { +func resourceUserMappingRead(ctx context.Context, d *schema.ResourceData, i interface{}) diag.Diagnostics { // send a read query to the API var query struct { User *structs.User `graphql:"managedUser(id: $id)"` @@ -73,13 +96,14 @@ func resourceUserRead(ctx context.Context, d *schema.ResourceData, i interface{} return diag.Errorf("could not query for user: %v", err) } - // if the user was not found on the Spacelift side, delete it from TF state + // if the mapping is not found on the remote side, delete it from the TF state if query.User == nil { d.SetId("") return nil } // if found, update the TF state + d.Set("email", query.User.Email) d.Set("username", query.User.Username) var accessList []interface{} for _, a := range query.User.AccessRules { @@ -93,7 +117,7 @@ func resourceUserRead(ctx context.Context, d *schema.ResourceData, i interface{} return nil } -func resourceUserUpdate(ctx context.Context, d *schema.ResourceData, i interface{}) diag.Diagnostics { +func resourceUserMappingUpdate(ctx context.Context, d *schema.ResourceData, i interface{}) diag.Diagnostics { var ret diag.Diagnostics // send an update query to the API @@ -101,8 +125,7 @@ func resourceUserUpdate(ctx context.Context, d *schema.ResourceData, i interface User *structs.User `graphql:"managedUserUpdate(input: $input)"` } variables := map[string]interface{}{ - "input": structs.ManagedUserUpdateInput{ - ID: toID(d.Id()), + "input": structs.UserUpdateInput{ AccessRules: getAccessRules(d), }, } @@ -110,14 +133,13 @@ func resourceUserUpdate(ctx context.Context, d *schema.ResourceData, i interface ret = append(ret, diag.Errorf("could not update user %s: %v", d.Id(), internal.FromSpaceliftError(err))...) } - // send a read query to the API - ret = append(ret, resourceUserRead(ctx, d, i)...) + // fetch from remote and write to TF state + ret = append(ret, resourceUserMappingCreate(ctx, d, i)...) return ret - } -func resourceUserDelete(ctx context.Context, d *schema.ResourceData, i interface{}) diag.Diagnostics { +func resourceUserMappingDelete(ctx context.Context, d *schema.ResourceData, i interface{}) diag.Diagnostics { // send a delete query to the API var mutation struct { User *structs.User `graphql:"managedUserDelete(id: $id)"` diff --git a/spacelift/resource_user_mapping_test.go b/spacelift/resource_user_mapping_test.go new file mode 100644 index 00000000..43a588a1 --- /dev/null +++ b/spacelift/resource_user_mapping_test.go @@ -0,0 +1,67 @@ +package spacelift + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + + . "github.com/spacelift-io/terraform-provider-spacelift/spacelift/internal/testhelpers" +) + +var userWithOneAccess = ` +resource "spacelift_user_mapping" "test" { + email = "%s" + username = "%s" + policy { + space_id = "root" + role = "ADMIN" + } +} +` + +func TestUserResource(t *testing.T) { + const resourceName = "spacelift_user_mapping.test" + + t.Run("creates and updates a user mapping without an error", func(t *testing.T) { + randomEmail := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + randomUsername := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + + newEmail := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + newUsername := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + + testSteps(t, []resource.TestStep{ + { + Config: fmt.Sprintf(userWithOneAccess, randomEmail, randomUsername), + Check: Resource( + resourceName, + Attribute("email", Equals(randomEmail)), + Attribute("username", Equals(randomUsername)), + SetContains("policy", "root"), + SetContains("policy", "ADMIN"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: fmt.Sprintf(userWithOneAccess, newEmail, newUsername), + Check: Resource( + resourceName, + Attribute("email", Equals(newEmail)), + Attribute("username", Equals(newUsername)), + SetContains("policy", "root"), + SetContains("policy", "ADMIN"), + ), + }, + }) + }) + + t.Run("can remove one access", func(t *testing.T) { + + }) + +} From d9081f884995e67dd59db8287c1aab01e384e449 Mon Sep 17 00:00:00 2001 From: Michal Wasilewski Date: Tue, 10 Oct 2023 15:14:08 +0200 Subject: [PATCH 04/13] typos Signed-off-by: Michal Wasilewski --- examples/resources/spacelift_user_mapping/resource.tf | 2 +- spacelift/resource_user_mapping.go | 4 ++-- spacelift/resource_user_mapping_test.go | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/resources/spacelift_user_mapping/resource.tf b/examples/resources/spacelift_user_mapping/resource.tf index 8adca364..fbf30572 100644 --- a/examples/resources/spacelift_user_mapping/resource.tf +++ b/examples/resources/spacelift_user_mapping/resource.tf @@ -1,5 +1,5 @@ resource "spacelift_user_mapping" "test" { - email = "johnk@eample.com" + email = "johnk@example.com" username = "johnk" policy { space_id = "root" diff --git a/spacelift/resource_user_mapping.go b/spacelift/resource_user_mapping.go index d3058af1..3e08fa6b 100644 --- a/spacelift/resource_user_mapping.go +++ b/spacelift/resource_user_mapping.go @@ -45,7 +45,7 @@ func resourceUserMapping() *schema.Resource { Schema: map[string]*schema.Schema{ "space_id": { Type: schema.TypeString, - Description: "ID (slug) of the space the user group has access to", + Description: "ID (slug) of the space the user has access to", Required: true, ValidateDiagFunc: validations.DisallowEmptyString, }, @@ -66,7 +66,7 @@ func resourceUserMapping() *schema.Resource { func resourceUserMappingCreate(ctx context.Context, d *schema.ResourceData, i interface{}) diag.Diagnostics { // send an Invite (create) mutation to the API var mutation struct { - User *structs.User `graphql:"managedUserInvite(input: $iinput)"` + User *structs.User `graphql:"managedUserInvite(input: $input)"` } variables := map[string]interface{}{ "input": structs.UserInviteInput{ diff --git a/spacelift/resource_user_mapping_test.go b/spacelift/resource_user_mapping_test.go index 43a588a1..bee98868 100644 --- a/spacelift/resource_user_mapping_test.go +++ b/spacelift/resource_user_mapping_test.go @@ -61,7 +61,7 @@ func TestUserResource(t *testing.T) { }) t.Run("can remove one access", func(t *testing.T) { - + }) } From 858ab2ccadd19510a1bd0884f759271b22c549ed Mon Sep 17 00:00:00 2001 From: Michal Wasilewski Date: Fri, 13 Oct 2023 14:25:43 +0200 Subject: [PATCH 05/13] fix: match the new types in api Signed-off-by: Michal Wasilewski --- spacelift/internal/structs/user_input.go | 4 ++-- spacelift/resource_user_mapping.go | 21 ++++++++++++--------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/spacelift/internal/structs/user_input.go b/spacelift/internal/structs/user_input.go index 0371dcac..9b1d9c44 100644 --- a/spacelift/internal/structs/user_input.go +++ b/spacelift/internal/structs/user_input.go @@ -2,12 +2,12 @@ package structs import "github.com/shurcooL/graphql" -type UserInviteInput struct { +type ManagedUserInviteInput struct { Email graphql.String `json:"email"` Username graphql.String `json:"username"` AccessRules []SpaceAccessRuleInput `json:"accessRules"` } -type UserUpdateInput struct { +type ManagedUserUpdateInput struct { AccessRules []SpaceAccessRuleInput `json:"accessRules"` } diff --git a/spacelift/resource_user_mapping.go b/spacelift/resource_user_mapping.go index 3e08fa6b..b75742e4 100644 --- a/spacelift/resource_user_mapping.go +++ b/spacelift/resource_user_mapping.go @@ -2,6 +2,7 @@ package spacelift import ( "context" + "fmt" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -31,7 +32,7 @@ func resourceUserMapping() *schema.Resource { "email": { Type: schema.TypeString, Description: "Email of the user. Used for sending an invitation.", - Required: true, + Optional: true, }, "username": { Type: schema.TypeString, @@ -40,7 +41,8 @@ func resourceUserMapping() *schema.Resource { }, "policy": { Type: schema.TypeList, - Optional: true, + MinItems: 1, + Required: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "space_id": { @@ -69,14 +71,14 @@ func resourceUserMappingCreate(ctx context.Context, d *schema.ResourceData, i in User *structs.User `graphql:"managedUserInvite(input: $input)"` } variables := map[string]interface{}{ - "input": structs.UserInviteInput{ + "input": structs.ManagedUserInviteInput{ Email: toString(d.Get("email")), Username: toString(d.Get("username")), AccessRules: getAccessRules(d), }, } if err := i.(*internal.Client).Mutate(ctx, "ManagedUserInvite", &mutation, variables); err != nil { - return diag.Errorf("could not create user %s: %v", toString(d.Get("username")), internal.FromSpaceliftError(err)) + return diag.Errorf("could not create user mapping %s: %v", toString(d.Get("username")), internal.FromSpaceliftError(err)) } // set the ID in TF state @@ -88,12 +90,13 @@ func resourceUserMappingCreate(ctx context.Context, d *schema.ResourceData, i in func resourceUserMappingRead(ctx context.Context, d *schema.ResourceData, i interface{}) diag.Diagnostics { // send a read query to the API + fmt.Println("reading") var query struct { User *structs.User `graphql:"managedUser(id: $id)"` } variables := map[string]interface{}{"id": toID(d.Id())} - if err := i.(*internal.Client).Query(ctx, "ManagedUserRead", &query, variables); err != nil { - return diag.Errorf("could not query for user: %v", err) + if err := i.(*internal.Client).Query(ctx, "ManagedUser", &query, variables); err != nil { + return diag.Errorf("could not query for user mapping: %v", err) } // if the mapping is not found on the remote side, delete it from the TF state @@ -125,12 +128,12 @@ func resourceUserMappingUpdate(ctx context.Context, d *schema.ResourceData, i in User *structs.User `graphql:"managedUserUpdate(input: $input)"` } variables := map[string]interface{}{ - "input": structs.UserUpdateInput{ + "input": structs.ManagedUserUpdateInput{ AccessRules: getAccessRules(d), }, } if err := i.(*internal.Client).Mutate(ctx, "ManagedUserUpdate", &mutation, variables); err != nil { - ret = append(ret, diag.Errorf("could not update user %s: %v", d.Id(), internal.FromSpaceliftError(err))...) + ret = append(ret, diag.Errorf("could not update user mapping %s: %v", d.Id(), internal.FromSpaceliftError(err))...) } // fetch from remote and write to TF state @@ -146,7 +149,7 @@ func resourceUserMappingDelete(ctx context.Context, d *schema.ResourceData, i in } variables := map[string]interface{}{"id": toID(d.Id())} if err := i.(*internal.Client).Mutate(ctx, "ManagedUserDelete", &mutation, variables); err != nil { - return diag.Errorf("could not delete user %s: %v", d.Id(), internal.FromSpaceliftError(err)) + return diag.Errorf("could not delete user mapping %s: %v", d.Id(), internal.FromSpaceliftError(err)) } // if the user was deleted, remove it from the TF state as well From a23e48c1b6b826e9e33288716938c9adbb4419ea Mon Sep 17 00:00:00 2001 From: Michal Wasilewski Date: Fri, 13 Oct 2023 15:43:02 +0200 Subject: [PATCH 06/13] fix: assume consistent naming in the api Signed-off-by: Michal Wasilewski --- spacelift/internal/structs/user.go | 8 ++++---- spacelift/internal/structs/user_input.go | 7 ++++--- spacelift/resource_user_mapping.go | 13 ++++++------- spacelift/resource_user_mapping_test.go | 14 +++++++------- 4 files changed, 21 insertions(+), 21 deletions(-) diff --git a/spacelift/internal/structs/user.go b/spacelift/internal/structs/user.go index 7532d2b1..6754bcf3 100644 --- a/spacelift/internal/structs/user.go +++ b/spacelift/internal/structs/user.go @@ -1,8 +1,8 @@ package structs type User struct { - ID string `graphql:"id"` - Email string `graphql:"email"` - Username string `graphql:"username"` - AccessRules []SpaceAccessRule `graphql:"accessRules"` + ID string `graphql:"id"` + InvitationEmail string `graphql:"invitationEmail"` + Username string `graphql:"username"` + AccessRules []SpaceAccessRule `graphql:"accessRules"` } diff --git a/spacelift/internal/structs/user_input.go b/spacelift/internal/structs/user_input.go index 9b1d9c44..2edfceed 100644 --- a/spacelift/internal/structs/user_input.go +++ b/spacelift/internal/structs/user_input.go @@ -3,11 +3,12 @@ package structs import "github.com/shurcooL/graphql" type ManagedUserInviteInput struct { - Email graphql.String `json:"email"` - Username graphql.String `json:"username"` - AccessRules []SpaceAccessRuleInput `json:"accessRules"` + InvitationEmail graphql.String `json:"invitationEmail"` + Username graphql.String `json:"username"` + AccessRules []SpaceAccessRuleInput `json:"accessRules"` } type ManagedUserUpdateInput struct { + ID graphql.ID `json:"id"` AccessRules []SpaceAccessRuleInput `json:"accessRules"` } diff --git a/spacelift/resource_user_mapping.go b/spacelift/resource_user_mapping.go index b75742e4..eae07d93 100644 --- a/spacelift/resource_user_mapping.go +++ b/spacelift/resource_user_mapping.go @@ -2,7 +2,6 @@ package spacelift import ( "context" - "fmt" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -29,7 +28,7 @@ func resourceUserMapping() *schema.Resource { }, Schema: map[string]*schema.Schema{ - "email": { + "invitation_email": { Type: schema.TypeString, Description: "Email of the user. Used for sending an invitation.", Optional: true, @@ -72,9 +71,9 @@ func resourceUserMappingCreate(ctx context.Context, d *schema.ResourceData, i in } variables := map[string]interface{}{ "input": structs.ManagedUserInviteInput{ - Email: toString(d.Get("email")), - Username: toString(d.Get("username")), - AccessRules: getAccessRules(d), + InvitationEmail: toString(d.Get("invitation_email")), + Username: toString(d.Get("username")), + AccessRules: getAccessRules(d), }, } if err := i.(*internal.Client).Mutate(ctx, "ManagedUserInvite", &mutation, variables); err != nil { @@ -90,7 +89,6 @@ func resourceUserMappingCreate(ctx context.Context, d *schema.ResourceData, i in func resourceUserMappingRead(ctx context.Context, d *schema.ResourceData, i interface{}) diag.Diagnostics { // send a read query to the API - fmt.Println("reading") var query struct { User *structs.User `graphql:"managedUser(id: $id)"` } @@ -106,7 +104,7 @@ func resourceUserMappingRead(ctx context.Context, d *schema.ResourceData, i inte } // if found, update the TF state - d.Set("email", query.User.Email) + d.Set("invitation_email", query.User.InvitationEmail) d.Set("username", query.User.Username) var accessList []interface{} for _, a := range query.User.AccessRules { @@ -129,6 +127,7 @@ func resourceUserMappingUpdate(ctx context.Context, d *schema.ResourceData, i in } variables := map[string]interface{}{ "input": structs.ManagedUserUpdateInput{ + ID: toID(d.Id()), AccessRules: getAccessRules(d), }, } diff --git a/spacelift/resource_user_mapping_test.go b/spacelift/resource_user_mapping_test.go index bee98868..afca5c2a 100644 --- a/spacelift/resource_user_mapping_test.go +++ b/spacelift/resource_user_mapping_test.go @@ -12,7 +12,7 @@ import ( var userWithOneAccess = ` resource "spacelift_user_mapping" "test" { - email = "%s" + invitation_email = "%s" username = "%s" policy { space_id = "root" @@ -25,18 +25,18 @@ func TestUserResource(t *testing.T) { const resourceName = "spacelift_user_mapping.test" t.Run("creates and updates a user mapping without an error", func(t *testing.T) { - randomEmail := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) randomUsername := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + exampleEmail := fmt.Sprintf("%s@example.com", randomUsername) - newEmail := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) newUsername := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + exampleEmailNew := fmt.Sprintf("%s@example.com", newUsername) testSteps(t, []resource.TestStep{ { - Config: fmt.Sprintf(userWithOneAccess, randomEmail, randomUsername), + Config: fmt.Sprintf(userWithOneAccess, exampleEmail, randomUsername), Check: Resource( resourceName, - Attribute("email", Equals(randomEmail)), + Attribute("invitation_email", Equals(exampleEmail)), Attribute("username", Equals(randomUsername)), SetContains("policy", "root"), SetContains("policy", "ADMIN"), @@ -48,10 +48,10 @@ func TestUserResource(t *testing.T) { ImportStateVerify: true, }, { - Config: fmt.Sprintf(userWithOneAccess, newEmail, newUsername), + Config: fmt.Sprintf(userWithOneAccess, exampleEmailNew, newUsername), Check: Resource( resourceName, - Attribute("email", Equals(newEmail)), + Attribute("invitation_email", Equals(exampleEmailNew)), Attribute("username", Equals(newUsername)), SetContains("policy", "root"), SetContains("policy", "ADMIN"), From 5a21048114eec62189972006406338efe3019e85 Mon Sep 17 00:00:00 2001 From: Michal Wasilewski Date: Fri, 13 Oct 2023 15:47:38 +0200 Subject: [PATCH 07/13] docs: update docs Signed-off-by: Michal Wasilewski --- docs/resources/user_mapping.md | 52 +++++++++++++++++++ .../spacelift_user_mapping/resource.tf | 8 +-- 2 files changed, 56 insertions(+), 4 deletions(-) create mode 100644 docs/resources/user_mapping.md diff --git a/docs/resources/user_mapping.md b/docs/resources/user_mapping.md new file mode 100644 index 00000000..1c445f69 --- /dev/null +++ b/docs/resources/user_mapping.md @@ -0,0 +1,52 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "spacelift_user_mapping Resource - terraform-provider-spacelift" +subcategory: "" +description: |- + spacelift_user_mapping represents a mapping between a Spacelift user (managed using an Identity Provider) and a Policy. A Policy defines what access rights the user has to a given Space. +--- + +# spacelift_user_mapping (Resource) + +`spacelift_user_mapping` represents a mapping between a Spacelift user (managed using an Identity Provider) and a Policy. A Policy defines what access rights the user has to a given Space. + +## Example Usage + +```terraform +resource "spacelift_user_mapping" "test" { + invitation_email = "johnk@example.com" + username = "johnk" + policy { + space_id = "root" + role = "ADMIN" + } + policy { + space_id = "legacy" + role = "READ" + } +} +``` + + +## Schema + +### Required + +- `policy` (Block List, Min: 1) (see [below for nested schema](#nestedblock--policy)) +- `username` (String) Username of the user + +### Optional + +- `invitation_email` (String) Email of the user. Used for sending an invitation. + +### Read-Only + +- `id` (String) The ID of this resource. + + +### Nested Schema for `policy` + +Required: + +- `role` (String) Type of access to the space. Possible values are: READ, WRITE, ADMIN +- `space_id` (String) ID (slug) of the space the user has access to diff --git a/examples/resources/spacelift_user_mapping/resource.tf b/examples/resources/spacelift_user_mapping/resource.tf index fbf30572..84c7323a 100644 --- a/examples/resources/spacelift_user_mapping/resource.tf +++ b/examples/resources/spacelift_user_mapping/resource.tf @@ -1,12 +1,12 @@ resource "spacelift_user_mapping" "test" { - email = "johnk@example.com" - username = "johnk" + invitation_email = "johnk@example.com" + username = "johnk" policy { space_id = "root" - role = "ADMIN" + role = "ADMIN" } policy { space_id = "legacy" - role = "READ" + role = "READ" } } From 475b9000e1da325b6b36a2040f1993550bda3eb0 Mon Sep 17 00:00:00 2001 From: Michal Wasilewski Date: Tue, 17 Oct 2023 15:48:55 +0200 Subject: [PATCH 08/13] fix: email field is required Signed-off-by: Michal Wasilewski --- spacelift/resource_user_mapping.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spacelift/resource_user_mapping.go b/spacelift/resource_user_mapping.go index eae07d93..abc3bebc 100644 --- a/spacelift/resource_user_mapping.go +++ b/spacelift/resource_user_mapping.go @@ -31,7 +31,7 @@ func resourceUserMapping() *schema.Resource { "invitation_email": { Type: schema.TypeString, Description: "Email of the user. Used for sending an invitation.", - Optional: true, + Required: true, }, "username": { Type: schema.TypeString, From edb77d29e9ea582589c5f50adc8551bb6fd1637f Mon Sep 17 00:00:00 2001 From: Michal Wasilewski Date: Tue, 17 Oct 2023 15:51:32 +0200 Subject: [PATCH 09/13] docs: regenerate docs Signed-off-by: Michal Wasilewski --- docs/resources/user_mapping.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/docs/resources/user_mapping.md b/docs/resources/user_mapping.md index 1c445f69..29351772 100644 --- a/docs/resources/user_mapping.md +++ b/docs/resources/user_mapping.md @@ -32,13 +32,10 @@ resource "spacelift_user_mapping" "test" { ### Required +- `invitation_email` (String) Email of the user. Used for sending an invitation. - `policy` (Block List, Min: 1) (see [below for nested schema](#nestedblock--policy)) - `username` (String) Username of the user -### Optional - -- `invitation_email` (String) Email of the user. Used for sending an invitation. - ### Read-Only - `id` (String) The ID of this resource. From 85e1183ba72f2ec6fe32bed183af872e006db3b1 Mon Sep 17 00:00:00 2001 From: Michal Wasilewski Date: Thu, 19 Oct 2023 11:28:02 +0200 Subject: [PATCH 10/13] fix: use naming consistent with the web UI Signed-off-by: Michal Wasilewski --- docs/resources/user.md | 34 +++++++++++++++++ docs/resources/user_mapping.md | 49 ------------------------- spacelift/provider.go | 2 +- spacelift/resource_user_mapping.go | 24 ++++++------ spacelift/resource_user_mapping_test.go | 6 +-- 5 files changed, 50 insertions(+), 65 deletions(-) create mode 100644 docs/resources/user.md delete mode 100644 docs/resources/user_mapping.md diff --git a/docs/resources/user.md b/docs/resources/user.md new file mode 100644 index 00000000..0db7c8bb --- /dev/null +++ b/docs/resources/user.md @@ -0,0 +1,34 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "spacelift_user Resource - terraform-provider-spacelift" +subcategory: "" +description: |- + spacelift_user represents a mapping between a Spacelift user (managed using an Identity Provider) and a Policy. A Policy defines what access rights the user has to a given Space. +--- + +# spacelift_user (Resource) + +`spacelift_user` represents a mapping between a Spacelift user (managed using an Identity Provider) and a Policy. A Policy defines what access rights the user has to a given Space. + + + + +## Schema + +### Required + +- `invitation_email` (String) Email of the user. Used for sending an invitation. +- `policy` (Block List, Min: 1) (see [below for nested schema](#nestedblock--policy)) +- `username` (String) Username of the user + +### Read-Only + +- `id` (String) The ID of this resource. + + +### Nested Schema for `policy` + +Required: + +- `role` (String) Type of access to the space. Possible values are: READ, WRITE, ADMIN +- `space_id` (String) ID (slug) of the space the user has access to diff --git a/docs/resources/user_mapping.md b/docs/resources/user_mapping.md deleted file mode 100644 index 29351772..00000000 --- a/docs/resources/user_mapping.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -# generated by https://github.com/hashicorp/terraform-plugin-docs -page_title: "spacelift_user_mapping Resource - terraform-provider-spacelift" -subcategory: "" -description: |- - spacelift_user_mapping represents a mapping between a Spacelift user (managed using an Identity Provider) and a Policy. A Policy defines what access rights the user has to a given Space. ---- - -# spacelift_user_mapping (Resource) - -`spacelift_user_mapping` represents a mapping between a Spacelift user (managed using an Identity Provider) and a Policy. A Policy defines what access rights the user has to a given Space. - -## Example Usage - -```terraform -resource "spacelift_user_mapping" "test" { - invitation_email = "johnk@example.com" - username = "johnk" - policy { - space_id = "root" - role = "ADMIN" - } - policy { - space_id = "legacy" - role = "READ" - } -} -``` - - -## Schema - -### Required - -- `invitation_email` (String) Email of the user. Used for sending an invitation. -- `policy` (Block List, Min: 1) (see [below for nested schema](#nestedblock--policy)) -- `username` (String) Username of the user - -### Read-Only - -- `id` (String) The ID of this resource. - - -### Nested Schema for `policy` - -Required: - -- `role` (String) Type of access to the space. Possible values are: READ, WRITE, ADMIN -- `space_id` (String) ID (slug) of the space the user has access to diff --git a/spacelift/provider.go b/spacelift/provider.go index e24e38e2..ebedbf06 100644 --- a/spacelift/provider.go +++ b/spacelift/provider.go @@ -126,7 +126,7 @@ func Provider(commit, version string) plugin.ProviderFunc { "spacelift_stack_aws_role": resourceStackAWSRole(), // deprecated "spacelift_stack_gcp_service_account": resourceStackGCPServiceAccount(), // deprecated "spacelift_terraform_provider": resourceTerraformProvider(), - "spacelift_user_mapping": resourceUserMapping(), + "spacelift_user": resourceUser(), "spacelift_vcs_agent_pool": resourceVCSAgentPool(), "spacelift_webhook": resourceWebhook(), "spacelift_named_webhook": resourceNamedWebhook(), diff --git a/spacelift/resource_user_mapping.go b/spacelift/resource_user_mapping.go index abc3bebc..cb5979c6 100644 --- a/spacelift/resource_user_mapping.go +++ b/spacelift/resource_user_mapping.go @@ -12,16 +12,16 @@ import ( "github.com/spacelift-io/terraform-provider-spacelift/spacelift/internal/validations" ) -func resourceUserMapping() *schema.Resource { +func resourceUser() *schema.Resource { return &schema.Resource{ Description: "" + - "`spacelift_user_mapping` represents a mapping between a Spacelift user " + + "`spacelift_user` represents a mapping between a Spacelift user " + "(managed using an Identity Provider) and a Policy. A Policy defines " + "what access rights the user has to a given Space.", - CreateContext: resourceUserMappingCreate, - ReadContext: resourceUserMappingRead, - UpdateContext: resourceUserMappingUpdate, - DeleteContext: resourceUserMappingDelete, + CreateContext: resourceUserCreate, + ReadContext: resourceUserRead, + UpdateContext: resourceUserUpdate, + DeleteContext: resourceUserDelete, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, @@ -64,7 +64,7 @@ func resourceUserMapping() *schema.Resource { } } -func resourceUserMappingCreate(ctx context.Context, d *schema.ResourceData, i interface{}) diag.Diagnostics { +func resourceUserCreate(ctx context.Context, d *schema.ResourceData, i interface{}) diag.Diagnostics { // send an Invite (create) mutation to the API var mutation struct { User *structs.User `graphql:"managedUserInvite(input: $input)"` @@ -84,10 +84,10 @@ func resourceUserMappingCreate(ctx context.Context, d *schema.ResourceData, i in d.SetId(mutation.User.ID) // fetch state from remote and write to TF state - return resourceUserMappingRead(ctx, d, i) + return resourceUserRead(ctx, d, i) } -func resourceUserMappingRead(ctx context.Context, d *schema.ResourceData, i interface{}) diag.Diagnostics { +func resourceUserRead(ctx context.Context, d *schema.ResourceData, i interface{}) diag.Diagnostics { // send a read query to the API var query struct { User *structs.User `graphql:"managedUser(id: $id)"` @@ -118,7 +118,7 @@ func resourceUserMappingRead(ctx context.Context, d *schema.ResourceData, i inte return nil } -func resourceUserMappingUpdate(ctx context.Context, d *schema.ResourceData, i interface{}) diag.Diagnostics { +func resourceUserUpdate(ctx context.Context, d *schema.ResourceData, i interface{}) diag.Diagnostics { var ret diag.Diagnostics // send an update query to the API @@ -136,12 +136,12 @@ func resourceUserMappingUpdate(ctx context.Context, d *schema.ResourceData, i in } // fetch from remote and write to TF state - ret = append(ret, resourceUserMappingCreate(ctx, d, i)...) + ret = append(ret, resourceUserCreate(ctx, d, i)...) return ret } -func resourceUserMappingDelete(ctx context.Context, d *schema.ResourceData, i interface{}) diag.Diagnostics { +func resourceUserDelete(ctx context.Context, d *schema.ResourceData, i interface{}) diag.Diagnostics { // send a delete query to the API var mutation struct { User *structs.User `graphql:"managedUserDelete(id: $id)"` diff --git a/spacelift/resource_user_mapping_test.go b/spacelift/resource_user_mapping_test.go index afca5c2a..8cde22c1 100644 --- a/spacelift/resource_user_mapping_test.go +++ b/spacelift/resource_user_mapping_test.go @@ -11,7 +11,7 @@ import ( ) var userWithOneAccess = ` -resource "spacelift_user_mapping" "test" { +resource "spacelift_user" "test" { invitation_email = "%s" username = "%s" policy { @@ -22,9 +22,9 @@ resource "spacelift_user_mapping" "test" { ` func TestUserResource(t *testing.T) { - const resourceName = "spacelift_user_mapping.test" + const resourceName = "spacelift_user.test" - t.Run("creates and updates a user mapping without an error", func(t *testing.T) { + t.Run("creates and updates a user without an error", func(t *testing.T) { randomUsername := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) exampleEmail := fmt.Sprintf("%s@example.com", randomUsername) From 04c422a215760b8b0d599446f84c9491499990d7 Mon Sep 17 00:00:00 2001 From: Michal Wasilewski Date: Fri, 20 Oct 2023 14:28:59 +0200 Subject: [PATCH 11/13] test: working test for removing access Signed-off-by: Michal Wasilewski --- ...ource_user_mapping.go => resource_user.go} | 2 +- ..._mapping_test.go => resource_user_test.go} | 54 ++++++++++++++----- 2 files changed, 41 insertions(+), 15 deletions(-) rename spacelift/{resource_user_mapping.go => resource_user.go} (98%) rename spacelift/{resource_user_mapping_test.go => resource_user_test.go} (51%) diff --git a/spacelift/resource_user_mapping.go b/spacelift/resource_user.go similarity index 98% rename from spacelift/resource_user_mapping.go rename to spacelift/resource_user.go index cb5979c6..910fbfa6 100644 --- a/spacelift/resource_user_mapping.go +++ b/spacelift/resource_user.go @@ -136,7 +136,7 @@ func resourceUserUpdate(ctx context.Context, d *schema.ResourceData, i interface } // fetch from remote and write to TF state - ret = append(ret, resourceUserCreate(ctx, d, i)...) + ret = append(ret, resourceUserRead(ctx, d, i)...) return ret } diff --git a/spacelift/resource_user_mapping_test.go b/spacelift/resource_user_test.go similarity index 51% rename from spacelift/resource_user_mapping_test.go rename to spacelift/resource_user_test.go index 8cde22c1..f410cfee 100644 --- a/spacelift/resource_user_mapping_test.go +++ b/spacelift/resource_user_test.go @@ -16,7 +16,22 @@ resource "spacelift_user" "test" { username = "%s" policy { space_id = "root" - role = "ADMIN" + role = "ADMIN" + } +} +` + +var userWithTwoAccesses = ` +resource "spacelift_user" "test" { + invitation_email = "%s" + username = "%s" + policy { + space_id = "root" + role = "ADMIN" + } + policy { + space_id = "legacy" + role = "READ" } } ` @@ -24,13 +39,10 @@ resource "spacelift_user" "test" { func TestUserResource(t *testing.T) { const resourceName = "spacelift_user.test" - t.Run("creates and updates a user without an error", func(t *testing.T) { + t.Run("creates a user without an error", func(t *testing.T) { randomUsername := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) exampleEmail := fmt.Sprintf("%s@example.com", randomUsername) - newUsername := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) - exampleEmailNew := fmt.Sprintf("%s@example.com", newUsername) - testSteps(t, []resource.TestStep{ { Config: fmt.Sprintf(userWithOneAccess, exampleEmail, randomUsername), @@ -47,20 +59,34 @@ func TestUserResource(t *testing.T) { ImportState: true, ImportStateVerify: true, }, + }) + }) + + // Note: the api doesn't allow for the username or email to be updated + t.Run("can remove one access", func(t *testing.T) { + randomUsername := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + exampleEmail := fmt.Sprintf("%s@example.com", randomUsername) + + testSteps(t, []resource.TestStep{ + { + Config: fmt.Sprintf(userWithTwoAccesses, exampleEmail, randomUsername), + Check: Resource( + resourceName, + Attribute("invitation_email", Equals(exampleEmail)), + Attribute("username", Equals(randomUsername)), + SetContains("policy", "root", "ADMIN"), + SetContains("policy", "legacy", "READ")), + }, { - Config: fmt.Sprintf(userWithOneAccess, exampleEmailNew, newUsername), + Config: fmt.Sprintf(userWithOneAccess, exampleEmail, randomUsername), Check: Resource( resourceName, - Attribute("invitation_email", Equals(exampleEmailNew)), - Attribute("username", Equals(newUsername)), - SetContains("policy", "root"), - SetContains("policy", "ADMIN"), - ), + Attribute("invitation_email", Equals(exampleEmail)), + Attribute("username", Equals(randomUsername)), + SetContains("policy", "root", "ADMIN"), + SetDoesNotContain("policy", "legacy", "READ")), }, }) - }) - - t.Run("can remove one access", func(t *testing.T) { }) From 3ce19ebd640780417505a2f1f0a2013563e41575 Mon Sep 17 00:00:00 2001 From: Michal Wasilewski Date: Fri, 20 Oct 2023 14:52:52 +0200 Subject: [PATCH 12/13] test: added a couple more tests to test access list edits, email edits, etc Signed-off-by: Michal Wasilewski --- spacelift/resource_user.go | 8 +++++ spacelift/resource_user_test.go | 53 +++++++++++++++++++++++++++++++-- 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/spacelift/resource_user.go b/spacelift/resource_user.go index 910fbfa6..b0afaab9 100644 --- a/spacelift/resource_user.go +++ b/spacelift/resource_user.go @@ -119,6 +119,14 @@ func resourceUserRead(ctx context.Context, d *schema.ResourceData, i interface{} } func resourceUserUpdate(ctx context.Context, d *schema.ResourceData, i interface{}) diag.Diagnostics { + // input validation + if d.HasChange("invitation_email") { + return diag.Errorf("invitation_email cannot be changed") + } + if d.HasChange("username") { + return diag.Errorf("username cannot be changed") + } + var ret diag.Diagnostics // send an update query to the API diff --git a/spacelift/resource_user_test.go b/spacelift/resource_user_test.go index f410cfee..fcac520e 100644 --- a/spacelift/resource_user_test.go +++ b/spacelift/resource_user_test.go @@ -2,6 +2,7 @@ package spacelift import ( "fmt" + "regexp" "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" @@ -62,8 +63,7 @@ func TestUserResource(t *testing.T) { }) }) - // Note: the api doesn't allow for the username or email to be updated - t.Run("can remove one access", func(t *testing.T) { + t.Run("can edit access list", func(t *testing.T) { randomUsername := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) exampleEmail := fmt.Sprintf("%s@example.com", randomUsername) @@ -90,4 +90,53 @@ func TestUserResource(t *testing.T) { }) + t.Run("cannot change email address", func(t *testing.T) { + randomUsername := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + exampleEmail := fmt.Sprintf("%s@example.com", randomUsername) + + randomUsername2 := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + exampleEmail2 := fmt.Sprintf("%s@example.com", randomUsername2) + + testSteps(t, []resource.TestStep{ + { + Config: fmt.Sprintf(userWithOneAccess, exampleEmail, randomUsername), + Check: Resource( + resourceName, + Attribute("invitation_email", Equals(exampleEmail)), + Attribute("username", Equals(randomUsername)), + SetContains("policy", "root", "ADMIN"), + SetDoesNotContain("policy", "legacy"), + ), + }, + { + Config: fmt.Sprintf(userWithOneAccess, exampleEmail2, randomUsername), + ExpectError: regexp.MustCompile(`invitation_email cannot be changed`), + }, + }) + }) + + t.Run("cannot change username", func(t *testing.T) { + randomUsername := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + exampleEmail := fmt.Sprintf("%s@example.com", randomUsername) + + randomUsername2 := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + + testSteps(t, []resource.TestStep{ + { + Config: fmt.Sprintf(userWithOneAccess, exampleEmail, randomUsername), + Check: Resource( + resourceName, + Attribute("invitation_email", Equals(exampleEmail)), + Attribute("username", Equals(randomUsername)), + SetContains("policy", "root", "ADMIN"), + SetDoesNotContain("policy", "legacy"), + ), + }, + { + Config: fmt.Sprintf(userWithOneAccess, exampleEmail, randomUsername2), + ExpectError: regexp.MustCompile(`username cannot be changed`), + }, + }) + }) + } From daada9f21ca73dfdee238fa385bdf14ca441350b Mon Sep 17 00:00:00 2001 From: Michal Wasilewski Date: Fri, 20 Oct 2023 14:56:39 +0200 Subject: [PATCH 13/13] chore: clean up after wrong naming Signed-off-by: Michal Wasilewski --- .../resources/spacelift_user_mapping/resource.tf | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 examples/resources/spacelift_user_mapping/resource.tf diff --git a/examples/resources/spacelift_user_mapping/resource.tf b/examples/resources/spacelift_user_mapping/resource.tf deleted file mode 100644 index 84c7323a..00000000 --- a/examples/resources/spacelift_user_mapping/resource.tf +++ /dev/null @@ -1,12 +0,0 @@ -resource "spacelift_user_mapping" "test" { - invitation_email = "johnk@example.com" - username = "johnk" - policy { - space_id = "root" - role = "ADMIN" - } - policy { - space_id = "legacy" - role = "READ" - } -}