Skip to content

Commit

Permalink
feat(org_user): deprecating resource, new datasource (#1484)
Browse files Browse the repository at this point in the history
  • Loading branch information
ivan-savciuc authored Dec 27, 2023
1 parent 294745f commit 0dd160c
Show file tree
Hide file tree
Showing 6 changed files with 167 additions and 90 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand Down
10 changes: 6 additions & 4 deletions docs/data-sources/organization_user.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
5 changes: 3 additions & 2 deletions docs/resources/organization_user.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

<a id="nestedblock--timeouts"></a>
### Nested Schema for `timeouts`
Expand Down
97 changes: 29 additions & 68 deletions internal/sdkprovider/service/organization/organization_user.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package organization

import (
"context"
"log"

"github.com/aiven/aiven-go-client/v2"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
Expand Down Expand Up @@ -32,19 +31,26 @@ 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,
Computed: true,
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 {
Expand All @@ -66,100 +72,55 @@ 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())
if err != nil {
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)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,94 @@ 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 {
return &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
}
Original file line number Diff line number Diff line change
@@ -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)
}

0 comments on commit 0dd160c

Please sign in to comment.