From a9fae4710eb5a95f0f0d8ef4660ba6f442b15482 Mon Sep 17 00:00:00 2001 From: Tom Bamford Date: Mon, 23 Sep 2024 17:07:17 +0100 Subject: [PATCH] SDK Migration: migrate `invitations` to go-azure-sdk --- .../services/invitations/client/client.go | 29 ++- internal/services/invitations/constants.go | 8 + .../invitations/invitation_resource.go | 199 +++++++++--------- .../invitations/invitation_resource_test.go | 24 +-- internal/services/invitations/invitations.go | 29 ++- internal/services/invitations/registration.go | 2 +- 6 files changed, 158 insertions(+), 133 deletions(-) create mode 100644 internal/services/invitations/constants.go diff --git a/internal/services/invitations/client/client.go b/internal/services/invitations/client/client.go index 0185567c4..f44ae5764 100644 --- a/internal/services/invitations/client/client.go +++ b/internal/services/invitations/client/client.go @@ -4,24 +4,31 @@ package client import ( + "github.com/hashicorp/go-azure-sdk/microsoft-graph/invitations/stable/invitation" + "github.com/hashicorp/go-azure-sdk/microsoft-graph/users/stable/user" "github.com/hashicorp/terraform-provider-azuread/internal/common" - "github.com/manicminer/hamilton/msgraph" ) type Client struct { - InvitationsClient *msgraph.InvitationsClient - UsersClient *msgraph.UsersClient + InvitationClient *invitation.InvitationClient + UserClient *user.UserClient } -func NewClient(o *common.ClientOptions) *Client { - invitationsClient := msgraph.NewInvitationsClient() - o.ConfigureClient(&invitationsClient.BaseClient) +func NewClient(o *common.ClientOptions) (*Client, error) { + invitationClient, err := invitation.NewInvitationClientWithBaseURI(o.Environment.MicrosoftGraph) + if err != nil { + return nil, err + } + o.Configure(invitationClient.Client) - usersClient := msgraph.NewUsersClient() - o.ConfigureClient(&usersClient.BaseClient) + userClient, err := user.NewUserClientWithBaseURI(o.Environment.MicrosoftGraph) + if err != nil { + return nil, err + } + o.Configure(userClient.Client) return &Client{ - InvitationsClient: invitationsClient, - UsersClient: usersClient, - } + InvitationClient: invitationClient, + UserClient: userClient, + }, nil } diff --git a/internal/services/invitations/constants.go b/internal/services/invitations/constants.go new file mode 100644 index 000000000..f594d44e5 --- /dev/null +++ b/internal/services/invitations/constants.go @@ -0,0 +1,8 @@ +package invitations + +const ( + InvitedUserTypeGuest = "Guest" + InvitedUserTypeMember = "Member" +) + +var possibleValuesForInvitedUserType = []string{InvitedUserTypeGuest, InvitedUserTypeMember} diff --git a/internal/services/invitations/invitation_resource.go b/internal/services/invitations/invitation_resource.go index 901ecb7ee..45db412be 100644 --- a/internal/services/invitations/invitation_resource.go +++ b/internal/services/invitations/invitation_resource.go @@ -7,18 +7,23 @@ import ( "context" "errors" "fmt" + "github.com/hashicorp/go-azure-sdk/sdk/odata" "log" "net/http" "time" "github.com/hashicorp/go-azure-helpers/lang/pointer" - "github.com/hashicorp/go-azure-sdk/sdk/odata" + "github.com/hashicorp/go-azure-helpers/lang/response" + "github.com/hashicorp/go-azure-sdk/microsoft-graph/common-types/stable" + "github.com/hashicorp/go-azure-sdk/microsoft-graph/invitations/stable/invitation" + "github.com/hashicorp/go-azure-sdk/microsoft-graph/users/stable/user" + "github.com/hashicorp/go-azure-sdk/sdk/nullable" + "github.com/hashicorp/go-uuid" "github.com/hashicorp/terraform-provider-azuread/internal/clients" - "github.com/hashicorp/terraform-provider-azuread/internal/helpers" - "github.com/hashicorp/terraform-provider-azuread/internal/tf" - "github.com/hashicorp/terraform-provider-azuread/internal/tf/pluginsdk" - "github.com/hashicorp/terraform-provider-azuread/internal/tf/validation" - "github.com/manicminer/hamilton/msgraph" + "github.com/hashicorp/terraform-provider-azuread/internal/helpers/consistency" + "github.com/hashicorp/terraform-provider-azuread/internal/helpers/tf" + "github.com/hashicorp/terraform-provider-azuread/internal/helpers/tf/pluginsdk" + "github.com/hashicorp/terraform-provider-azuread/internal/helpers/tf/validation" ) func invitationResource() *pluginsdk.Resource { @@ -43,19 +48,19 @@ func invitationResource() *pluginsdk.Resource { }, "user_email_address": { - Description: "The email address of the user being invited", - Type: pluginsdk.TypeString, - Required: true, - ForceNew: true, - ValidateDiagFunc: validation.StringIsEmailAddress, + Description: "The email address of the user being invited", + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringIsEmailAddress, }, "user_display_name": { - Description: "The display name of the user being invited", - Type: pluginsdk.TypeString, - Optional: true, - ForceNew: true, - ValidateDiagFunc: validation.ValidateDiag(validation.StringIsNotEmpty), + Description: "The display name of the user being invited", + Type: pluginsdk.TypeString, + Optional: true, + ForceNew: true, + ValidateFunc: validation.StringIsNotEmpty, }, "message": { @@ -72,40 +77,37 @@ func invitationResource() *pluginsdk.Resource { Optional: true, MaxItems: 1, Elem: &pluginsdk.Schema{ - Type: pluginsdk.TypeString, - ValidateDiagFunc: validation.StringIsEmailAddress, + Type: pluginsdk.TypeString, + ValidateFunc: validation.StringIsNotEmpty, }, }, "body": { - Description: "Customized message body you want to send if you don't want to send the default message", - Type: pluginsdk.TypeString, - Optional: true, - ConflictsWith: []string{"message.0.language"}, - ValidateDiagFunc: validation.ValidateDiag(validation.StringIsNotEmpty), + Description: "Customized message body you want to send if you don't want to send the default message", + Type: pluginsdk.TypeString, + Optional: true, + ConflictsWith: []string{"message.0.language"}, + ValidateFunc: validation.StringIsNotEmpty, }, "language": { - Description: "The language you want to send the default message in", - Type: pluginsdk.TypeString, - Optional: true, - ConflictsWith: []string{"message.0.body"}, - ValidateDiagFunc: validation.ISO639Language, + Description: "The language you want to send the default message in", + Type: pluginsdk.TypeString, + Optional: true, + ConflictsWith: []string{"message.0.body"}, + ValidateFunc: validation.ISO639Language, }, }, }, }, "user_type": { - Description: "The user type of the user being invited", - Type: pluginsdk.TypeString, - Optional: true, - ForceNew: true, - Default: "Guest", - ValidateFunc: validation.StringInSlice([]string{ - msgraph.InvitedUserTypeGuest, - msgraph.InvitedUserTypeMember, - }, false), + Description: "The user type of the user being invited", + Type: pluginsdk.TypeString, + Optional: true, + ForceNew: true, + Default: "Guest", + ValidateFunc: validation.StringInSlice(possibleValuesForInvitedUserType, false), }, "redeem_url": { @@ -124,127 +126,130 @@ func invitationResource() *pluginsdk.Resource { } func invitationResourceCreate(ctx context.Context, d *pluginsdk.ResourceData, meta interface{}) pluginsdk.Diagnostics { - client := meta.(*clients.Client).Invitations.InvitationsClient - usersClient := meta.(*clients.Client).Invitations.UsersClient + client := meta.(*clients.Client).Invitations.InvitationClient + userClient := meta.(*clients.Client).Invitations.UserClient - properties := msgraph.Invitation{ - InvitedUserEmailAddress: pointer.To(d.Get("user_email_address").(string)), - InviteRedirectURL: pointer.To(d.Get("redirect_url").(string)), - InvitedUserType: pointer.To(d.Get("user_type").(string)), + properties := stable.Invitation{ + InvitedUserEmailAddress: d.Get("user_email_address").(string), + InviteRedirectUrl: d.Get("redirect_url").(string), + InvitedUserType: nullable.Value(d.Get("user_type").(string)), } if v, ok := d.GetOk("user_display_name"); ok { - properties.InvitedUserDisplayName = pointer.To(v.(string)) + properties.InvitedUserDisplayName = nullable.Value(v.(string)) } if v, ok := d.GetOk("message"); ok { - properties.SendInvitationMessage = pointer.To(true) + properties.SendInvitationMessage = nullable.Value(true) properties.InvitedUserMessageInfo = expandInvitedUserMessageInfo(v.([]interface{})) } - invitation, _, err := client.Create(ctx, properties) + resp, err := client.CreateInvitation(ctx, properties, invitation.DefaultCreateInvitationOperationOptions()) if err != nil { - return tf.ErrorDiagF(err, "Could not create invitation") + return tf.ErrorDiagF(err, "Creating invitation") } - if invitation.ID == nil || *invitation.ID == "" { + invite := resp.Model + if invite == nil { + return tf.ErrorDiagF(errors.New("model was nil"), "Creating invitation") + } + + if invite.Id == nil || *invite.Id == "" { return tf.ErrorDiagF(errors.New("Bad API response"), "Object ID returned for invitation is nil/empty") } - d.SetId(*invitation.ID) - if invitation.InvitedUser == nil || invitation.InvitedUser.ID() == nil || *invitation.InvitedUser.ID() == "" { + d.SetId(*invite.Id) + + if invite.InvitedUser == nil || invite.InvitedUser.Id == nil || *invite.InvitedUser.Id == "" { return tf.ErrorDiagF(errors.New("Bad API response"), "Invited user object ID returned for invitation is nil/empty") } - d.Set("user_id", invitation.InvitedUser.ID()) - if invitation.InviteRedeemURL == nil || *invitation.InviteRedeemURL == "" { + userId := stable.NewUserID(*invite.InvitedUser.Id) + d.Set("user_id", userId.UserId) + + if invite.InviteRedeemUrl.GetOrZero() == "" { return tf.ErrorDiagF(errors.New("Bad API response"), "Redeem URL returned for invitation is nil/empty") } - d.Set("redeem_url", invitation.InviteRedeemURL) + d.Set("redeem_url", invite.InviteRedeemUrl.GetOrZero()) // Attempt to patch the newly created guest user, which will tell us whether it exists yet // The SDK handles retries for us here in the event of 404, 429 or 5xx, then returns after giving up - status, err := usersClient.Update(ctx, msgraph.User{ - DirectoryObject: msgraph.DirectoryObject{ - Id: invitation.InvitedUser.ID(), + uid, err := uuid.GenerateUUID() + if err != nil { + return tf.ErrorDiagF(err, "Failed to generate a UUID") + } + tempCompanyName := fmt.Sprintf("TERRAFORM_UPDATE_%s", uid) + + userResp, err := userClient.UpdateUser(ctx, userId, stable.User{ + CompanyName: nullable.NoZero(tempCompanyName), + }, user.UpdateUserOperationOptions{ + RetryFunc: func(resp *http.Response, o *odata.OData) (bool, error) { + return response.WasNotFound(resp) || response.WasStatusCode(resp, 500) || response.WasStatusCode(resp, 503), nil }, - CompanyName: tf.NullableString("TERRAFORM_UPDATE"), }) if err != nil { - if status == http.StatusNotFound { + if response.WasNotFound(userResp.HttpResponse) { return tf.ErrorDiagF(err, "Timed out whilst waiting for new guest user to be replicated in Azure AD") } - return tf.ErrorDiagF(err, "Failed to patch guest user after creating invitation") + return tf.ErrorDiagF(err, "Failed to patch guest user (1) after creating invitation") } - status, err = usersClient.Update(ctx, msgraph.User{ - DirectoryObject: msgraph.DirectoryObject{ - Id: invitation.InvitedUser.ID(), - }, - CompanyName: tf.NullableString(""), - }) + + userResp, err = userClient.UpdateUser(ctx, userId, stable.User{ + CompanyName: nullable.NoZero(""), + }, user.DefaultUpdateUserOperationOptions()) if err != nil { - if status == http.StatusNotFound { + if response.WasNotFound(userResp.HttpResponse) { return tf.ErrorDiagF(err, "Timed out whilst waiting for new guest user to be replicated in Azure AD") } - return tf.ErrorDiagF(err, "Failed to patch guest user after creating invitation") + return tf.ErrorDiagF(err, "Failed to patch guest user (2) after creating invitation") } return invitationResourceRead(ctx, d, meta) } func invitationResourceRead(ctx context.Context, d *pluginsdk.ResourceData, meta interface{}) pluginsdk.Diagnostics { - client := meta.(*clients.Client).Invitations.UsersClient + client := meta.(*clients.Client).Invitations.UserClient + userId := stable.NewUserID(d.Get("user_id").(string)) - userID := d.Get("user_id").(string) - - user, status, err := client.Get(ctx, userID, odata.Query{}) + resp, err := client.GetUser(ctx, userId, user.DefaultGetUserOperationOptions()) if err != nil { - if status == http.StatusNotFound { - log.Printf("[DEBUG] Invited user with Object ID %q was not found - removing from state!", userID) + if response.WasNotFound(resp.HttpResponse) { + log.Printf("[DEBUG] Invited %s was not found - removing from state!", userId) d.Set("user_id", "") return nil } - return tf.ErrorDiagF(err, "Retrieving invited user with object ID: %q", userID) + return tf.ErrorDiagF(err, "Retrieving invited %s", userId) + } + + if resp.Model == nil { + return tf.ErrorDiagF(errors.New("model was nil"), "Retrieving invited %s", userId) } - tf.Set(d, "user_id", user.ID()) - tf.Set(d, "user_email_address", user.Mail) + tf.Set(d, "user_id", userId.UserId) + tf.Set(d, "user_email_address", resp.Model.Mail.GetOrZero()) return nil } func invitationResourceDelete(ctx context.Context, d *pluginsdk.ResourceData, meta interface{}) pluginsdk.Diagnostics { - client := meta.(*clients.Client).Invitations.UsersClient - - userID := d.Get("user_id").(string) - - _, status, err := client.Get(ctx, userID, odata.Query{}) - if err != nil { - if status == http.StatusNotFound { - return tf.ErrorDiagPathF(fmt.Errorf("User was not found"), "id", "Retrieving invited user with object ID %q", userID) - } - - return tf.ErrorDiagPathF(err, "id", "Retrieving invited user with object ID %q", userID) - } + client := meta.(*clients.Client).Invitations.UserClient + userId := stable.NewUserID(d.Get("user_id").(string)) - status, err = client.Delete(ctx, userID) - if err != nil { - return tf.ErrorDiagPathF(err, "id", "Deleting invited user with object ID %q, got status %d with error: %+v", userID, status, err) + if _, err := client.DeleteUser(ctx, userId, user.DefaultDeleteUserOperationOptions()); err != nil { + return tf.ErrorDiagPathF(err, "id", "Deleting invited %s", userId) } // Wait for user object to be deleted, this seems much slower for invited users - if err := helpers.WaitForDeletion(ctx, func(ctx context.Context) (*bool, error) { - defer func() { client.BaseClient.DisableRetries = false }() - client.BaseClient.DisableRetries = true - if _, status, err := client.Get(ctx, userID, odata.Query{}); err != nil { - if status == http.StatusNotFound { + if err := consistency.WaitForDeletion(ctx, func(ctx context.Context) (*bool, error) { + if resp, err := client.GetUser(ctx, userId, user.DefaultGetUserOperationOptions()); err != nil { + if response.WasNotFound(resp.HttpResponse) { return pointer.To(false), nil } return nil, err } return pointer.To(true), nil }); err != nil { - return tf.ErrorDiagF(err, "Waiting for deletion of invited user with object ID %q", userID) + return tf.ErrorDiagF(err, "Waiting for deletion of invited %s", userId) } return nil diff --git a/internal/services/invitations/invitation_resource_test.go b/internal/services/invitations/invitation_resource_test.go index 3636abe06..e8f91295a 100644 --- a/internal/services/invitations/invitation_resource_test.go +++ b/internal/services/invitations/invitation_resource_test.go @@ -6,11 +6,12 @@ package invitations_test import ( "context" "fmt" - "net/http" "testing" "github.com/hashicorp/go-azure-helpers/lang/pointer" - "github.com/hashicorp/go-azure-sdk/sdk/odata" + "github.com/hashicorp/go-azure-helpers/lang/response" + "github.com/hashicorp/go-azure-sdk/microsoft-graph/common-types/stable" + "github.com/hashicorp/go-azure-sdk/microsoft-graph/users/stable/user" "github.com/hashicorp/terraform-plugin-testing/terraform" "github.com/hashicorp/terraform-provider-azuread/internal/acceptance" "github.com/hashicorp/terraform-provider-azuread/internal/acceptance/check" @@ -19,7 +20,7 @@ import ( type InvitationResource struct{} -func TestAccInvitation_basic(t *testing.T) { +func TestAccInvitation_guest(t *testing.T) { data := acceptance.BuildTestData(t, "azuread_invitation", "test") r := InvitationResource{} @@ -140,21 +141,18 @@ func TestAccInvitation_withGroupMembership(t *testing.T) { } func (r InvitationResource) Exists(ctx context.Context, clients *clients.Client, state *terraform.InstanceState) (*bool, error) { - client := clients.Invitations.UsersClient - client.BaseClient.DisableRetries = true - defer func() { client.BaseClient.DisableRetries = false }() + client := clients.Invitations.UserClient + userId := stable.NewUserID(state.Attributes["user_id"]) - userID := state.Attributes["user_id"] - - user, status, err := client.Get(ctx, userID, odata.Query{}) + resp, err := client.GetUser(ctx, userId, user.DefaultGetUserOperationOptions()) if err != nil { - if status == http.StatusNotFound { - return nil, fmt.Errorf("Invited user with object ID %q does not exist", userID) + if response.WasNotFound(resp.HttpResponse) { + return pointer.To(false), nil } - return nil, fmt.Errorf("failed to retrieve invited user with object ID %q: %+v", userID, err) + return nil, fmt.Errorf("failed to retrieve invited %s: %+v", userId, err) } - return pointer.To(user.ID() != nil && *user.ID() == userID), nil + return pointer.To(true), nil } func (InvitationResource) basic(data acceptance.TestData) string { diff --git a/internal/services/invitations/invitations.go b/internal/services/invitations/invitations.go index 7870f247e..9f38f38f0 100644 --- a/internal/services/invitations/invitations.go +++ b/internal/services/invitations/invitations.go @@ -3,35 +3,42 @@ package invitations -import "github.com/manicminer/hamilton/msgraph" +import ( + "github.com/hashicorp/go-azure-sdk/microsoft-graph/common-types/stable" + "github.com/hashicorp/go-azure-sdk/sdk/nullable" +) -func expandInvitedUserMessageInfo(in []interface{}) *msgraph.InvitedUserMessageInfo { +func expandInvitedUserMessageInfo(in []interface{}) *stable.InvitedUserMessageInfo { if len(in) == 0 || in[0] == nil { return nil } - result := msgraph.InvitedUserMessageInfo{} + result := stable.InvitedUserMessageInfo{} config := in[0].(map[string]interface{}) additionalRecipients := config["additional_recipients"].([]interface{}) messageBody := config["body"].(string) messageLanguage := config["language"].(string) - result.CCRecipients = expandRecipients(additionalRecipients) - result.CustomizedMessageBody = &messageBody - result.MessageLanguage = &messageLanguage + result.CcRecipients = expandRecipients(additionalRecipients) + result.CustomizedMessageBody = nullable.NoZero(messageBody) + result.MessageLanguage = nullable.Value(messageLanguage) return &result } -func expandRecipients(in []interface{}) *[]msgraph.Recipient { - recipients := make([]msgraph.Recipient, 0, len(in)) +func expandRecipients(in []interface{}) *[]stable.Recipient { + if len(in) == 0 { + return nil + } + + recipients := make([]stable.Recipient, 0, len(in)) for _, recipientRaw := range in { recipient := recipientRaw.(string) - newRecipient := msgraph.Recipient{ - EmailAddress: &msgraph.EmailAddress{ - Address: &recipient, + newRecipient := stable.BaseRecipientImpl{ + EmailAddress: &stable.EmailAddress{ + Address: nullable.Value(recipient), }, } diff --git a/internal/services/invitations/registration.go b/internal/services/invitations/registration.go index 0f4986e8b..4fa1bc19d 100644 --- a/internal/services/invitations/registration.go +++ b/internal/services/invitations/registration.go @@ -3,7 +3,7 @@ package invitations -import "github.com/hashicorp/terraform-provider-azuread/internal/tf/pluginsdk" +import "github.com/hashicorp/terraform-provider-azuread/internal/helpers/tf/pluginsdk" type Registration struct{}