Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Workaround API consistency bugs for various resources #659

Merged
merged 17 commits into from
Nov 12, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
f495016
Switch to hashicorp/go-uuid in acceptance package
manicminer Nov 11, 2021
b61f197
Increase retry limit by one
manicminer Nov 11, 2021
349687d
Add WaitForDeletion helper func
manicminer Nov 11, 2021
8c7f2ff
Use WaitForDeletion helper in conditionalaccess package, move expand/…
manicminer Nov 11, 2021
a004a5b
azuread_user: check for consistency on deletion
manicminer Nov 11, 2021
034f460
azuread_invitation: check for consistency on creation by attmpting to…
manicminer Nov 11, 2021
f65be8a
azuread_group_member: check for consistency on deletion
manicminer Nov 11, 2021
520693d
azuread_group: check for consistency on creation by attempting to pat…
manicminer Nov 11, 2021
ce26230
azuread_directory_role_member: check for consistency on deletion
manicminer Nov 11, 2021
292f3c0
Helper funcs for retrieving a KeyCredential/PasswordCredential by its…
manicminer Nov 11, 2021
f1af244
azuread_application_certificate: check for consistency on deletion
manicminer Nov 11, 2021
ec6fd14
azuread_application_password: check for consistency on deletion
manicminer Nov 11, 2021
30aafee
azuread_application: check for consistency on creation by attempting …
manicminer Nov 11, 2021
d038d2e
azuread_service_principal_certificate: check for consistency on deletion
manicminer Nov 11, 2021
226facc
azuread_service_principal_password: check for consistency on deletion
manicminer Nov 11, 2021
a9abfd8
azuread_service_principal: check for consistency on creation by attem…
manicminer Nov 11, 2021
d0cbaf6
Fix azuread_service_principal test - https identifier_uris must use a…
manicminer Nov 12, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ require (
github.com/aws/aws-sdk-go v1.38.43 // indirect
github.com/fatih/color v1.11.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/google/uuid v1.1.2
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320
github.com/hashicorp/go-hclog v0.16.1 // indirect
Expand Down
1 change: 0 additions & 1 deletion go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,6 @@ github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLe
github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210506205249-923b5ab0fc1a/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM=
Expand Down
11 changes: 8 additions & 3 deletions internal/acceptance/data.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import (
"strconv"
"testing"

"github.com/google/uuid"
"github.com/hashicorp/go-uuid"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest"

"github.com/hashicorp/terraform-provider-azuread/internal/tf"
Expand Down Expand Up @@ -39,7 +39,11 @@ type TestData struct {
}

func (t *TestData) UUID() string {
return uuid.New().String()
uuid, err := uuid.GenerateUUID()
if err != nil {
panic(err)
}
return uuid
}

// BuildTestData generates some test data for the given resource
Expand All @@ -49,14 +53,15 @@ func BuildTestData(t *testing.T, resourceType string, resourceLabel string) Test
testData := TestData{
RandomInteger: tf.AccRandTimeInt(),
RandomString: acctest.RandString(5),
RandomID: uuid.New().String(),
RandomPassword: fmt.Sprintf("%s%s", "p@$$Wd", acctest.RandString(6)),
ResourceName: fmt.Sprintf("%s.%s", resourceType, resourceLabel),

ResourceType: resourceType,
resourceLabel: resourceLabel,
}

testData.RandomID = testData.UUID()

return testData
}

Expand Down
2 changes: 1 addition & 1 deletion internal/common/client_options.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ func (o ClientOptions) ConfigureClient(c *msgraph.Client) {
*c.ResponseMiddlewares = append(*c.ResponseMiddlewares, o.responseLogger)

// Default retry limit, can be overridden from within a resource
c.RetryableClient.RetryMax = 8
c.RetryableClient.RetryMax = 9
}

func (o ClientOptions) requestLogger(req *http.Request) (*http.Request, error) {
Expand Down
43 changes: 43 additions & 0 deletions internal/helpers/consistency.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package helpers

import (
"context"
"errors"
"fmt"
"time"

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
)

type existsFunc func(ctx context.Context) (*bool, error)

func WaitForDeletion(ctx context.Context, f existsFunc) error {
deadline, ok := ctx.Deadline()
if !ok {
return errors.New("context has no deadline")
}

timeout := time.Until(deadline)
_, err := (&resource.StateChangeConf{
Pending: []string{"Waiting"},
Target: []string{"Deleted"},
Timeout: timeout,
MinTimeout: 5 * time.Second,
ContinuousTargetOccurence: 5,
Refresh: func() (interface{}, string, error) {
exists, err := f(ctx)
if err != nil {
return nil, "Error", fmt.Errorf("retrieving resource: %+v", err)
}
if exists == nil {
return nil, "Error", fmt.Errorf("retrieving resource: exists was nil")
}
if *exists {
return "stub", "Waiting", nil
}
return "stub", "Deleted", nil
},
}).WaitForStateContext(ctx)

return err
}
24 changes: 24 additions & 0 deletions internal/helpers/credentials.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,30 @@ func (e CredentialError) Error() string {
return e.str
}

func GetKeyCredential(keyCredentials *[]msgraph.KeyCredential, id string) (credential *msgraph.KeyCredential) {
if keyCredentials != nil {
for _, cred := range *keyCredentials {
if cred.KeyId != nil && strings.EqualFold(*cred.KeyId, id) {
credential = &cred
break
}
}
}
return
}

func GetPasswordCredential(passwordCredentials *[]msgraph.PasswordCredential, id string) (credential *msgraph.PasswordCredential) {
if passwordCredentials != nil {
for _, cred := range *passwordCredentials {
if cred.KeyId != nil && strings.EqualFold(*cred.KeyId, id) {
credential = &cred
break
}
}
}
return
}

func KeyCredentialForResource(d *schema.ResourceData) (*msgraph.KeyCredential, error) {
keyType := d.Get("type").(string)
value := d.Get("value").(string)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"github.com/hashicorp/terraform-provider-azuread/internal/helpers"
"github.com/hashicorp/terraform-provider-azuread/internal/services/applications/parse"
"github.com/hashicorp/terraform-provider-azuread/internal/tf"
"github.com/hashicorp/terraform-provider-azuread/internal/utils"
"github.com/hashicorp/terraform-provider-azuread/internal/validate"
)

Expand Down Expand Up @@ -228,16 +229,7 @@ func applicationCertificateResourceRead(ctx context.Context, d *schema.ResourceD
return tf.ErrorDiagPathF(err, "application_object_id", "Retrieving Application with object ID %q", id.ObjectId)
}

var credential *msgraph.KeyCredential
if app.KeyCredentials != nil {
for _, cred := range *app.KeyCredentials {
if cred.KeyId != nil && strings.EqualFold(*cred.KeyId, id.KeyId) {
credential = &cred
break
}
}
}

credential := helpers.GetKeyCredential(app.KeyCredentials, id.KeyId)
if credential == nil {
log.Printf("[DEBUG] Certificate credential %q (ID %q) was not found - removing from state!", id.KeyId, id.ObjectId)
d.SetId("")
Expand Down Expand Up @@ -301,5 +293,24 @@ func applicationCertificateResourceDelete(ctx context.Context, d *schema.Resourc
return tf.ErrorDiagF(err, "Removing certificate credential %q from application with object ID %q", id.KeyId, id.ObjectId)
}

// Wait for application certificate to be deleted
if err := helpers.WaitForDeletion(ctx, func(ctx context.Context) (*bool, error) {
client.BaseClient.DisableRetries = true

app, _, err := client.Get(ctx, id.ObjectId, odata.Query{})
if err != nil {
return nil, err
}

credential := helpers.GetKeyCredential(app.KeyCredentials, id.KeyId)
if credential == nil {
return utils.Bool(false), nil
}

return utils.Bool(true), nil
}); err != nil {
return tf.ErrorDiagF(err, "Waiting for deletion of certificate credential %q from application with object ID %q", id.KeyId, id.ObjectId)
}

return nil
}
32 changes: 21 additions & 11 deletions internal/services/applications/application_password_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@ import (
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
"github.com/manicminer/hamilton/msgraph"
"github.com/manicminer/hamilton/odata"

"github.com/hashicorp/terraform-provider-azuread/internal/clients"
"github.com/hashicorp/terraform-provider-azuread/internal/helpers"
"github.com/hashicorp/terraform-provider-azuread/internal/services/applications/migrations"
"github.com/hashicorp/terraform-provider-azuread/internal/services/applications/parse"
"github.com/hashicorp/terraform-provider-azuread/internal/tf"
"github.com/hashicorp/terraform-provider-azuread/internal/utils"
"github.com/hashicorp/terraform-provider-azuread/internal/validate"
)

Expand Down Expand Up @@ -219,16 +219,7 @@ func applicationPasswordResourceRead(ctx context.Context, d *schema.ResourceData
return tf.ErrorDiagPathF(err, "application_object_id", "Retrieving application with object ID %q", id.ObjectId)
}

var credential *msgraph.PasswordCredential
if app.PasswordCredentials != nil {
for _, cred := range *app.PasswordCredentials {
if cred.KeyId != nil && strings.EqualFold(*cred.KeyId, id.KeyId) {
credential = &cred
break
}
}
}

credential := helpers.GetPasswordCredential(app.PasswordCredentials, id.KeyId)
if credential == nil {
log.Printf("[DEBUG] Password credential %q (ID %q) was not found - removing from state!", id.KeyId, id.ObjectId)
d.SetId("")
Expand Down Expand Up @@ -279,5 +270,24 @@ func applicationPasswordResourceDelete(ctx context.Context, d *schema.ResourceDa
return tf.ErrorDiagF(err, "Removing password credential %q from application with object ID %q", id.KeyId, id.ObjectId)
}

// Wait for application password to be deleted
if err := helpers.WaitForDeletion(ctx, func(ctx context.Context) (*bool, error) {
client.BaseClient.DisableRetries = true

app, _, err := client.Get(ctx, id.ObjectId, odata.Query{})
if err != nil {
return nil, err
}

credential := helpers.GetPasswordCredential(app.PasswordCredentials, id.KeyId)
if credential == nil {
return utils.Bool(false), nil
}

return utils.Bool(true), nil
}); err != nil {
return tf.ErrorDiagF(err, "Waiting for deletion of password credential %q from application with object ID %q", id.KeyId, id.ObjectId)
}

return nil
}
45 changes: 36 additions & 9 deletions internal/services/applications/application_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -940,11 +940,18 @@ func applicationResourceCreate(ctx context.Context, d *schema.ResourceData, meta
return applicationResourceUpdate(ctx, d, meta)
}

// Set a temporary display name as we'll attempt to patch the application with the correct name after creating it
uuid, err := uuid.GenerateUUID()
if err != nil {
return tf.ErrorDiagF(err, "Failed to generate a UUID")
}
tempDisplayName := fmt.Sprintf("TERRAFORM_UPDATE_%s", uuid)

// Create a new application
properties := msgraph.Application{
Api: expandApplicationApi(d.Get("api").([]interface{})),
AppRoles: expandApplicationAppRoles(d.Get("app_role").(*schema.Set).List()),
DisplayName: utils.String(displayName),
DisplayName: utils.String(tempDisplayName),
GroupMembershipClaims: expandApplicationGroupMembershipClaims(d.Get("group_membership_claims").(*schema.Set).List()),
IdentifierUris: tf.ExpandStringSlicePtr(d.Get("identifier_uris").(*schema.Set).List()),
Info: &msgraph.InformationalUrl{
Expand Down Expand Up @@ -1032,14 +1039,19 @@ func applicationResourceCreate(ctx context.Context, d *schema.ResourceData, meta

d.SetId(*app.ID)

// Wait until the application is updatable (the SDK handles retries for us)
_, err = client.Update(ctx, msgraph.Application{
// Attempt to patch the newly created group with the correct name, 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 := client.Update(ctx, msgraph.Application{
DirectoryObject: msgraph.DirectoryObject{
ID: app.ID,
},
DisplayName: utils.String(displayName),
})
if err != nil {
return tf.ErrorDiagF(err, "Timed out whilst waiting for new application to be replicated in Azure AD")
if status == http.StatusNotFound {
return tf.ErrorDiagF(err, "Timed out whilst waiting for new application to be replicated in Azure AD")
}
return tf.ErrorDiagF(err, "Failed to patch application after creating")
}

if len(ownersExtra) > 0 {
Expand Down Expand Up @@ -1276,19 +1288,34 @@ func applicationResourceRead(ctx context.Context, d *schema.ResourceData, meta i

func applicationResourceDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client := meta.(*clients.Client).Applications.ApplicationsClient
appId := d.Id()

_, status, err := client.Get(ctx, d.Id(), odata.Query{})
_, status, err := client.Get(ctx, appId, odata.Query{})
if err != nil {
if status == http.StatusNotFound {
return tf.ErrorDiagPathF(fmt.Errorf("Application was not found"), "id", "Retrieving Application with object ID %q", d.Id())
return tf.ErrorDiagPathF(fmt.Errorf("Application was not found"), "id", "Retrieving Application with object ID %q", appId)
}

return tf.ErrorDiagPathF(err, "id", "Retrieving application with object ID %q", d.Id())
return tf.ErrorDiagPathF(err, "id", "Retrieving application with object ID %q", appId)
}

status, err = client.Delete(ctx, d.Id())
status, err = client.Delete(ctx, appId)
if err != nil {
return tf.ErrorDiagPathF(err, "id", "Deleting application with object ID %q, got status %d", d.Id(), status)
return tf.ErrorDiagPathF(err, "id", "Deleting application with object ID %q, got status %d", appId, status)
}

// Wait for application object to be deleted
if err := helpers.WaitForDeletion(ctx, func(ctx context.Context) (*bool, error) {
client.BaseClient.DisableRetries = true
if _, status, err := client.Get(ctx, appId, odata.Query{}); err != nil {
if status == http.StatusNotFound {
return utils.Bool(false), nil
}
return nil, err
}
return utils.Bool(true), nil
}); err != nil {
return tf.ErrorDiagF(err, "Waiting for deletion of application with object ID %q", appId)
}

return nil
Expand Down
Loading