From 0dd160c1b7427898a2fc27f7d14641f590dcd9a3 Mon Sep 17 00:00:00 2001 From: Ivan Savciuc Date: Wed, 27 Dec 2023 15:07:08 +0200 Subject: [PATCH] feat(org_user): deprecating resource, new datasource (#1484) --- CHANGELOG.md | 3 +- docs/data-sources/organization_user.md | 10 +- docs/resources/organization_user.md | 5 +- .../service/organization/organization_user.go | 97 ++++++------------- .../organization_user_data_source.go | 77 ++++++++++++++- .../organization_user_data_source_test.go | 65 ++++++++++--- 6 files changed, 167 insertions(+), 90 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 335c6ce78..dbe887ea9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ nav_order: 1 ## [MAJOR.MINOR.PATCH] - YYYY-MM-DD +- Add support for the `aiven_organization_user_group_member` resource, allowing the association of groups with the users. Please note that this resource is in the beta stage, and to use it, you would need to set the environment variable PROVIDER_AIVEN_ENABLE_BETA to a non-zero value. + ## [4.10.0] - 2023-12-27 - Deprecating `project_user`, `account_team` and `account_team_member` resources @@ -25,7 +27,6 @@ nav_order: 1 - Add `external_postgresql` and `external_google_cloud_bigquery` service integration endpoints - Do not return error on `aiven_account_team_member` deletion if the member does not exist - Deprecating `aiven_organization_user` resource and update data source logic that will be used instead of the corresponding resource -- Add support for the `aiven_organization_user_group_member` resource, allowing the association of groups with the users. Please note that this resource is in the beta stage, and to use it, you would need to set the environment variable PROVIDER_AIVEN_ENABLE_BETA to a non-zero value. - Retry kafka topic creation error `Cluster only has N broker(s), cannot set replication factor to M` - Fix Kafka Topic migration issues from V3 to V4. - Fix V3 to V4 migration issue related to cloud_name diff. diff --git a/docs/data-sources/organization_user.md b/docs/data-sources/organization_user.md index 40857e324..03af82393 100644 --- a/docs/data-sources/organization_user.md +++ b/docs/data-sources/organization_user.md @@ -17,12 +17,14 @@ The Organization User data source provides information about the existing Aiven ### Required -- `organization_id` (String) The unique organization ID. This property cannot be changed, doing so forces recreation of the resource. -- `user_email` (String) This is a user email address that first will be invited, and after accepting an invitation, they become a member of the organization. Should be lowercase. This property cannot be changed, doing so forces recreation of the resource. +- `organization_id` (String) The unique organization ID. + +### Optional + +- `user_email` (String) This is a user email address +- `user_id` (String) The unique organization user ID ### Read-Only -- `accepted` (Boolean) This is a boolean flag that determines whether an invitation was accepted or not by the user. `false` value means that the invitation was sent to the user but not yet accepted. `true` means that the user accepted the invitation and now a member of an organization. - `create_time` (String) Time of creation - `id` (String) The ID of this resource. -- `invited_by` (String) The email address of the user who sent an invitation to the user. diff --git a/docs/resources/organization_user.md b/docs/resources/organization_user.md index 12d550d54..779770f99 100644 --- a/docs/resources/organization_user.md +++ b/docs/resources/organization_user.md @@ -37,10 +37,11 @@ eliminate the member from the organization if one has accepted an invitation pre ### Read-Only -- `accepted` (Boolean) This is a boolean flag that determines whether an invitation was accepted or not by the user. `false` value means that the invitation was sent to the user but not yet accepted. `true` means that the user accepted the invitation and now a member of an organization. +- `accepted` (Boolean, Deprecated) This is a boolean flag that determines whether an invitation was accepted or not by the user. `false` value means that the invitation was sent to the user but not yet accepted. `true` means that the user accepted the invitation and now a member of an organization. - `create_time` (String) Time of creation - `id` (String) The ID of this resource. -- `invited_by` (String) The email address of the user who sent an invitation to the user. +- `invited_by` (String, Deprecated) The email address of the user who sent an invitation to the user. +- `user_id` (String) The unique organization user ID ### Nested Schema for `timeouts` diff --git a/internal/sdkprovider/service/organization/organization_user.go b/internal/sdkprovider/service/organization/organization_user.go index 1b9c2f095..cb998237c 100644 --- a/internal/sdkprovider/service/organization/organization_user.go +++ b/internal/sdkprovider/service/organization/organization_user.go @@ -2,7 +2,6 @@ package organization import ( "context" - "log" "github.com/aiven/aiven-go-client/v2" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" @@ -32,6 +31,7 @@ var aivenOrganizationUserSchema = map[string]*schema.Schema{ Type: schema.TypeString, Computed: true, Description: "The email address of the user who sent an invitation to the user.", + Deprecated: "This field is deprecated and will be removed in the next major release. ", }, "accepted": { Type: schema.TypeBool, @@ -39,12 +39,18 @@ var aivenOrganizationUserSchema = map[string]*schema.Schema{ Description: "This is a boolean flag that determines whether an invitation was accepted or not by the user. " + "`false` value means that the invitation was sent to the user but not yet accepted. `true` means that" + " the user accepted the invitation and now a member of an organization.", + Deprecated: "This field is deprecated and will be removed in the next major release. ", }, "create_time": { Type: schema.TypeString, Computed: true, Description: "Time of creation", }, + "user_id": { + Type: schema.TypeString, + Computed: true, + Description: "The unique organization user ID", + }, } func ResourceOrganizationUser() *schema.Resource { @@ -66,28 +72,27 @@ eliminate the member from the organization if one has accepted an invitation pre }, Timeouts: schemautil.DefaultResourceTimeouts(), Schema: aivenOrganizationUserSchema, + DeprecationMessage: ` +This resource is deprecated, please use aiven_organization_user datasource instead. +Invitation of organization users is not supported anymore via Terraform. Therefore +creation of this resource is not supported anymore. We reccoemnd to use WebUI to create +and organization user invitation. And upon receiving an invitation, a user can accept it +using WebUI. Once accepted, the user will become a member of the organization and will +be able to access it via Terraform. + `, } } -func resourceOrganizationUserCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - client := m.(*aiven.Client) - organizationID := d.Get("organization_id").(string) - userEmail := d.Get("user_email").(string) - - err := client.OrganizationUserInvitations.Invite(ctx, organizationID, aiven.OrganizationUserInvitationAddRequest{ - UserEmail: userEmail, - }) - if err != nil { - return diag.FromErr(err) - } - - d.SetId(schemautil.BuildResourceID(organizationID, userEmail)) - - return resourceOrganizationUserRead(ctx, d, m) +// resourceOrganizationUserCreate create is not supported anymore +func resourceOrganizationUserCreate(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { + return diag.Errorf("creation of organization user is not supported anymore via Terraform. " + + "Please use WebUI to create an organization user invitation. And upon receiving an invitation, " + + "a user can accept it using WebUI. Once accepted, the user will become a member of the organization " + + "and will be able to access it via Terraform using datasource `aiven_organization_user`") } +// resourceOrganizationUserRead reads the properties of an Aiven Organization User and provides them to Terraform func resourceOrganizationUserRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - var found bool client := m.(*aiven.Client) organizationID, userEmail, err := schemautil.SplitResourceID2(d.Id()) @@ -95,71 +100,27 @@ func resourceOrganizationUserRead(ctx context.Context, d *schema.ResourceData, m return diag.FromErr(err) } - r, err := client.OrganizationUserInvitations.List(ctx, organizationID) + rm, err := client.OrganizationUser.List(ctx, organizationID) if err != nil { return diag.FromErr(err) } - for _, invite := range r.Invitations { - if invite.UserEmail == userEmail { - found = true + for _, user := range rm.Users { + userInfo := user.UserInfo + if userInfo.UserEmail == userEmail { if err := d.Set("organization_id", organizationID); err != nil { return diag.FromErr(err) } - if err := d.Set("user_email", invite.UserEmail); err != nil { + if err := d.Set("user_email", userInfo.UserEmail); err != nil { return diag.FromErr(err) } - if err := d.Set("invited_by", invite.InvitedBy); err != nil { + if err := d.Set("create_time", user.JoinTime.String()); err != nil { return diag.FromErr(err) } - if err := d.Set("create_time", invite.CreateTime.String()); err != nil { + if err := d.Set("user_id", user.UserID); err != nil { return diag.FromErr(err) } - - // if a user is in the invitations list, it means invitation was sent but not yet accepted - if err := d.Set("accepted", false); err != nil { - return diag.FromErr(err) - } - } - } - - if !found { - rm, err := client.OrganizationUser.List(ctx, organizationID) - if err != nil { - return diag.FromErr(err) - } - - for _, user := range rm.Users { - userInfo := user.UserInfo - - if userInfo.UserEmail == userEmail { - found = true - - if err := d.Set("organization_id", organizationID); err != nil { - return diag.FromErr(err) - } - if err := d.Set("user_email", userInfo.UserEmail); err != nil { - return diag.FromErr(err) - } - if err := d.Set("create_time", user.JoinTime.String()); err != nil { - return diag.FromErr(err) - } - - // when a user accepts an invitation, it will appear in the member's list - // and disappear from invitations list - if err := d.Set("accepted", true); err != nil { - return diag.FromErr(err) - } - } - } - } - - if !found { - log.Printf("[WARNING] cannot find user invitation for %s", d.Id()) - if !d.Get("accepted").(bool) { - log.Printf("[DEBUG] resending organization user invitation ") - return resourceOrganizationUserCreate(ctx, d, m) } } diff --git a/internal/sdkprovider/service/organization/organization_user_data_source.go b/internal/sdkprovider/service/organization/organization_user_data_source.go index 85c341ab8..9a0c5a239 100644 --- a/internal/sdkprovider/service/organization/organization_user_data_source.go +++ b/internal/sdkprovider/service/organization/organization_user_data_source.go @@ -6,7 +6,9 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/aiven/aiven-go-client/v2" "github.com/aiven/terraform-provider-aiven/internal/schemautil" + "github.com/aiven/terraform-provider-aiven/internal/schemautil/userconfig" ) func DatasourceOrganizationUser() *schema.Resource { @@ -14,17 +16,84 @@ func DatasourceOrganizationUser() *schema.Resource { ReadContext: datasourceOrganizationUserRead, Description: "The Organization User data source provides information about the existing Aiven" + " Organization User.", - Schema: schemautil.ResourceSchemaAsDatasourceSchema( - aivenOrganizationUserSchema, "organization_id", "user_email", - ), + Schema: map[string]*schema.Schema{ + "organization_id": { + Type: schema.TypeString, + Required: true, + Description: userconfig.Desc("The unique organization ID").Build(), + }, + "user_id": { + Type: schema.TypeString, + Optional: true, + Description: "The unique organization user ID", + }, + "user_email": { + Type: schema.TypeString, + Optional: true, + Description: "This is a user email address", + }, + "create_time": { + Type: schema.TypeString, + Computed: true, + Description: "Time of creation", + }, + }, } } +// datasourceOrganizationUserRead reads the specified Organization User data source. func datasourceOrganizationUserRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { organizationID := d.Get("organization_id").(string) userEmail := d.Get("user_email").(string) + userID := d.Get("user_id").(string) + + if userEmail == "" && userID == "" { + return diag.Errorf("either user_email or user_id must be specified") + } + + client := m.(*aiven.Client) + rm, err := client.OrganizationUser.List(ctx, organizationID) + if err != nil { + return diag.Errorf("cannot get organization [%s] user list: %s", organizationID, err) + } + + var found int + + var user *aiven.OrganizationMemberInfo + for _, u := range rm.Users { + if userEmail != "" && u.UserInfo.UserEmail == userEmail { + user = &u + found++ + } + + if userID != "" && u.UserID == userID { + user = &u + found++ + } + } + + if found == 0 { + return diag.Errorf("organization user %s not found in organization %s", userEmail, organizationID) + } + + if found > 1 { + return diag.Errorf("multiple organization users %s found in organization %s", userEmail, organizationID) + } + + if err := d.Set("organization_id", organizationID); err != nil { + return diag.FromErr(err) + } + if err := d.Set("user_email", user.UserInfo.UserEmail); err != nil { + return diag.FromErr(err) + } + if err := d.Set("create_time", user.JoinTime.String()); err != nil { + return diag.FromErr(err) + } + if err := d.Set("user_id", user.UserID); err != nil { + return diag.FromErr(err) + } d.SetId(schemautil.BuildResourceID(organizationID, userEmail)) - return resourceOrganizationUserRead(ctx, d, m) + return nil } diff --git a/internal/sdkprovider/service/organization/organization_user_data_source_test.go b/internal/sdkprovider/service/organization/organization_user_data_source_test.go index a88470f17..b60e57251 100644 --- a/internal/sdkprovider/service/organization/organization_user_data_source_test.go +++ b/internal/sdkprovider/service/organization/organization_user_data_source_test.go @@ -1,34 +1,77 @@ package organization_test import ( + "fmt" + "os" "testing" - "github.com/hashicorp/terraform-plugin-testing/helper/acctest" "github.com/hashicorp/terraform-plugin-testing/helper/resource" acc "github.com/aiven/terraform-provider-aiven/internal/acctest" ) -func TestAccAivenOrganizationUserDataSource_basic(t *testing.T) { +func TestAccAivenOrganizationUserDataSource_using_email(t *testing.T) { + orgID := os.Getenv("AIVEN_ORG_ID") + email := os.Getenv("AIVEN_ORG_USER_EMAIL") + + if orgID == "" || email == "" { + t.Skip("Skipping test due to missing AIVEN_ORG_ID or AIVEN_ORG_USER_EMAIL environment variable") + } + + datasourceName := "data.aiven_organization_user.member" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acc.TestAccPreCheck(t) }, + ProtoV6ProviderFactories: acc.TestProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: testAccOrganizationUserDataResourceByEmail(orgID, email), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet(datasourceName, "user_email"), + resource.TestCheckResourceAttrSet(datasourceName, "create_time"), + ), + }, + }, + }) +} + +func TestAccAivenOrganizationUserDataSource_using_userid(t *testing.T) { + orgID := os.Getenv("AIVEN_ORG_ID") + userID := os.Getenv("AIVEN_ORG_USER_ID") + + if orgID == "" || userID == "" { + t.Skip("Skipping test due to missing AIVEN_ORG_ID or AIVEN_ORG_USER_ID environment variable") + } + datasourceName := "data.aiven_organization_user.member" - resourceName := "aiven_organization_user.foo" - rName := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acc.TestAccPreCheck(t) }, ProtoV6ProviderFactories: acc.TestProtoV6ProviderFactories, Steps: []resource.TestStep{ { - Config: testAccOrganizationUserResource(rName), + Config: testAccOrganizationUserDataResourceByUserID(orgID, userID), Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttrPair( - datasourceName, "organization_id", resourceName, "organization_id", - ), - resource.TestCheckResourceAttrPair(datasourceName, "user_email", resourceName, "user_email"), - resource.TestCheckResourceAttrPair(datasourceName, "create_time", resourceName, "create_time"), - resource.TestCheckResourceAttrPair(datasourceName, "accepted", resourceName, "accepted"), + resource.TestCheckResourceAttrSet(datasourceName, "user_id"), + resource.TestCheckResourceAttrSet(datasourceName, "create_time"), ), }, }, }) } + +func testAccOrganizationUserDataResourceByUserID(orgID, userID string) string { + return fmt.Sprintf(` +data "aiven_organization_user" "member" { + organization_id = "%s" + user_id = "%s" +}`, orgID, userID) +} + +func testAccOrganizationUserDataResourceByEmail(orgID, email string) string { + return fmt.Sprintf(` +data "aiven_organization_user" "member" { + organization_id = "%s" + user_email = "%s" +}`, orgID, email) +}