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

azurerm_app_configuration - retry checkNameAvailability to make sure delete is finished, add stateWait to confirm key/feature is created #21750

Merged
merged 12 commits into from
Jun 1, 2023
3 changes: 3 additions & 0 deletions internal/services/appconfiguration/app_configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ func appConfigurationGetKeyRefreshFunc(ctx context.Context, client *appconfigura
if utils.ResponseWasForbidden(autorest.Response{Response: v.Response}) {
return "Forbidden", "Forbidden", nil
}
if utils.ResponseWasNotFound(autorest.Response{Response: v.Response}) {
teowa marked this conversation as resolved.
Show resolved Hide resolved
return "NotFound", "NotFound", nil
}
}
return res, "Error", nil
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -212,17 +212,18 @@ func (k FeatureResource) Create() sdk.ResourceFunc {

// from https://learn.microsoft.com/en-us/azure/azure-app-configuration/concept-enable-rbac#azure-built-in-roles-for-azure-app-configuration
// allow some time for role permission to be done propagated
metadata.Logger.Infof("[DEBUG] Waiting for App Configuration Key %q read permission to be done propagated", featureKey)
metadata.Logger.Infof("[DEBUG] Waiting for App Configuration Feature %q read permission to be done propagated", featureKey)
teowa marked this conversation as resolved.
Show resolved Hide resolved
stateConf := &pluginsdk.StateChangeConf{
Pending: []string{"Forbidden"},
Target: []string{"Error", "Exists"},
Refresh: appConfigurationGetKeyRefreshFunc(ctx, client, featureKey, model.Label),
PollInterval: 20 * time.Second,
Timeout: time.Until(deadline),
Pending: []string{"Forbidden"},
Target: []string{"Error", "Exists", "NotFound"},
Refresh: appConfigurationGetKeyRefreshFunc(ctx, client, featureKey, model.Label),
PollInterval: 10 * time.Second,
ContinuousTargetOccurence: 3,
Timeout: time.Until(deadline),
}

if _, err = stateConf.WaitForStateContext(ctx); err != nil {
return fmt.Errorf("waiting for App Configuration Key %q read permission to be propagated: %+v", featureKey, err)
return fmt.Errorf("waiting for App Configuration Feature %q read permission to be propagated: %+v", featureKey, err)
}

kv, err := client.GetKeyValue(ctx, featureKey, model.Label, "", "", "", []appconfiguration.KeyValueFields{})
Expand All @@ -243,6 +244,20 @@ func (k FeatureResource) Create() sdk.ResourceFunc {
return fmt.Errorf("while creating feature: %+v", err)
}

metadata.Logger.Infof("[DEBUG] Waiting for App Configuration Feature %q to be done provisioned", model.Key)
teowa marked this conversation as resolved.
Show resolved Hide resolved
stateConf = &pluginsdk.StateChangeConf{
Pending: []string{"NotFound"},
Target: []string{"Exists"},
Refresh: appConfigurationGetKeyRefreshFunc(ctx, client, featureKey, model.Label),
PollInterval: 10 * time.Second,
ContinuousTargetOccurence: 2,
Timeout: time.Until(deadline),
}

if _, err = stateConf.WaitForStateContext(ctx); err != nil {
return fmt.Errorf("waiting for App Configuration Feature %q to be provisioned: %+v", featureKey, err)
}

metadata.SetID(nestedItemId)
return nil
},
Expand All @@ -258,8 +273,9 @@ func (k FeatureResource) Read() sdk.ResourceFunc {
return fmt.Errorf("while parsing resource ID: %+v", err)
}

resourceClient := metadata.Client.Resource
configurationStoreIdRaw, err := metadata.Client.AppConfiguration.ConfigurationStoreIDFromEndpoint(ctx, resourceClient, nestedItemId.ConfigurationStoreEndpoint)
configurationStoresClient := metadata.Client.AppConfiguration.ConfigurationStoresClient
subscriptionId := metadata.Client.Account.SubscriptionId
configurationStoreIdRaw, err := metadata.Client.AppConfiguration.ConfigurationStoreIDFromEndpoint(ctx, configurationStoresClient, nestedItemId.ConfigurationStoreEndpoint, subscriptionId)
if err != nil {
return fmt.Errorf("while retrieving the Resource ID of Configuration Store at Endpoint: %q: %s", nestedItemId.ConfigurationStoreEndpoint, err)
}
Expand Down Expand Up @@ -294,8 +310,6 @@ func (k FeatureResource) Read() sdk.ResourceFunc {
if utils.ResponseWasNotFound(autorest.Response{Response: v.Response}) {
return metadata.MarkAsGone(nestedItemId)
}
} else {
return fmt.Errorf("while checking for key %q existence: %+v", *nestedItemId, err)
}
return fmt.Errorf("while checking for key %q existence: %+v", *nestedItemId, err)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,11 +153,12 @@ func (k KeyResource) Create() sdk.ResourceFunc {
// allow some time for role permission to be done propagated
metadata.Logger.Infof("[DEBUG] Waiting for App Configuration Key %q read permission to be done propagated", model.Key)
stateConf := &pluginsdk.StateChangeConf{
Pending: []string{"Forbidden"},
Target: []string{"Error", "Exists"},
Refresh: appConfigurationGetKeyRefreshFunc(ctx, client, model.Key, model.Label),
PollInterval: 20 * time.Second,
Timeout: time.Until(deadline),
Pending: []string{"Forbidden"},
Target: []string{"Error", "Exists", "NotFound"},
Refresh: appConfigurationGetKeyRefreshFunc(ctx, client, model.Key, model.Label),
PollInterval: 10 * time.Second,
ContinuousTargetOccurence: 3,
Timeout: time.Until(deadline),
}

if _, err = stateConf.WaitForStateContext(ctx); err != nil {
Expand Down Expand Up @@ -207,6 +208,20 @@ func (k KeyResource) Create() sdk.ResourceFunc {
}
}

metadata.Logger.Infof("[DEBUG] Waiting for App Configuration Key %q to be done provisioned", model.Key)
teowa marked this conversation as resolved.
Show resolved Hide resolved
stateConf = &pluginsdk.StateChangeConf{
Pending: []string{"NotFound"},
Target: []string{"Exists"},
Refresh: appConfigurationGetKeyRefreshFunc(ctx, client, model.Key, model.Label),
PollInterval: 10 * time.Second,
ContinuousTargetOccurence: 2,
Timeout: time.Until(deadline),
}

if _, err = stateConf.WaitForStateContext(ctx); err != nil {
return fmt.Errorf("waiting for App Configuration Key %q to be provisioned: %+v", model.Key, err)
}

metadata.SetID(nestedItemId)
return nil
},
Expand All @@ -222,8 +237,9 @@ func (k KeyResource) Read() sdk.ResourceFunc {
return fmt.Errorf("while parsing resource ID: %+v", err)
}

resourceClient := metadata.Client.Resource
configurationStoreIdRaw, err := metadata.Client.AppConfiguration.ConfigurationStoreIDFromEndpoint(ctx, resourceClient, nestedItemId.ConfigurationStoreEndpoint)
configurationStoresClient := metadata.Client.AppConfiguration.ConfigurationStoresClient
subscriptionId := metadata.Client.Account.SubscriptionId
configurationStoreIdRaw, err := metadata.Client.AppConfiguration.ConfigurationStoreIDFromEndpoint(ctx, configurationStoresClient, nestedItemId.ConfigurationStoreEndpoint, subscriptionId)
if err != nil {
return fmt.Errorf("while retrieving the Resource ID of Configuration Store at Endpoint: %q: %s", nestedItemId.ConfigurationStoreEndpoint, err)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@ import (

"github.com/hashicorp/go-azure-helpers/lang/pointer"
"github.com/hashicorp/go-azure-helpers/lang/response"
"github.com/hashicorp/go-azure-helpers/resourcemanager/commonids"
"github.com/hashicorp/go-azure-helpers/resourcemanager/commonschema"
"github.com/hashicorp/go-azure-helpers/resourcemanager/identity"
"github.com/hashicorp/go-azure-helpers/resourcemanager/location"
"github.com/hashicorp/go-azure-helpers/resourcemanager/tags"
"github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/configurationstores"
"github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/deletedconfigurationstores"
"github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/operations"
"github.com/hashicorp/go-azure-sdk/sdk/client"
"github.com/hashicorp/go-azure-sdk/sdk/client/pollers"
"github.com/hashicorp/terraform-provider-azurerm/helpers/azure"
Expand Down Expand Up @@ -231,6 +233,7 @@ func resourceAppConfiguration() *pluginsdk.Resource {
func resourceAppConfigurationCreate(d *pluginsdk.ResourceData, meta interface{}) error {
client := meta.(*clients.Client).AppConfiguration.ConfigurationStoresClient
deletedConfigurationStoresClient := meta.(*clients.Client).AppConfiguration.DeletedConfigurationStoresClient

subscriptionId := meta.(*clients.Client).Account.SubscriptionId
ctx, cancel := timeouts.ForCreate(meta.(*clients.Client).StopContext, d)
defer cancel()
Expand Down Expand Up @@ -300,12 +303,22 @@ func resourceAppConfigurationCreate(d *pluginsdk.ResourceData, meta interface{})
return fmt.Errorf("expanding `identity`: %+v", err)
}
parameters.Identity = identity
// TODO: retry checkNameAvailability before creation when SDK is ready, see https://github.com/Azure/AppConfiguration/issues/677

if err := client.CreateThenPoll(ctx, resourceId, parameters); err != nil {
return fmt.Errorf("creating %s: %+v", resourceId, err)
}

d.SetId(resourceId.ID())

resp, err := client.Get(ctx, resourceId)
if err != nil {
return fmt.Errorf("retrieving %s: %+v", resourceId, err)
}
if resp.Model == nil || resp.Model.Properties == nil || resp.Model.Properties.Endpoint == nil {
return fmt.Errorf("retrieving %s: `model.properties.Endpoint` was nil", resourceId)
}
meta.(*clients.Client).AppConfiguration.AddToCache(resourceId, *resp.Model.Properties.Endpoint)

return resourceAppConfigurationRead(d, meta)
}

Expand Down Expand Up @@ -571,10 +584,16 @@ func resourceAppConfigurationDelete(d *pluginsdk.ResourceData, meta interface{})
return fmt.Errorf("polling after purging for %s: %+v", *id, err)
}

// TODO: retry checkNameAvailability after deletion when SDK is ready, see https://github.com/Azure/AppConfiguration/issues/677
// retry checkNameAvailability until the name is released by purged app configuration, see https://github.com/Azure/AppConfiguration/issues/677
operationsClient := meta.(*clients.Client).AppConfiguration.OperationsClient
if err = resourceConfigurationStoreWaitForNameAvailable(ctx, operationsClient, *id); err != nil {
return err
}
log.Printf("[DEBUG] Purged AppConfiguration %q.", id.ConfigurationStoreName)
}

meta.(*clients.Client).AppConfiguration.RemoveFromCache(*id)

return nil
}

Expand Down Expand Up @@ -732,3 +751,57 @@ func parsePublicNetworkAccess(input string) *configurationstores.PublicNetworkAc
out := configurationstores.PublicNetworkAccess(input)
return &out
}

func resourceConfigurationStoreWaitForNameAvailable(ctx context.Context, client *operations.OperationsClient, configurationStoreId configurationstores.ConfigurationStoreId) error {
deadline, ok := ctx.Deadline()
if !ok {
return fmt.Errorf("internal error: context had no deadline")
}
state := &pluginsdk.StateChangeConf{
MinTimeout: 10 * time.Second,
ContinuousTargetOccurence: 2,
Pending: []string{"Unavailable"},
Target: []string{"Available"},
Refresh: resourceConfigurationStoreNameAvailabilityRefreshFunc(ctx, client, configurationStoreId),
Timeout: time.Until(deadline),
}

_, err := state.WaitForStateContext(ctx)
if err != nil {
return fmt.Errorf("waiting for the Configuration Store %s Name Available: %+v", configurationStoreId, err)
}

return nil

}

func resourceConfigurationStoreNameAvailabilityRefreshFunc(ctx context.Context, client *operations.OperationsClient, configurationStoreId configurationstores.ConfigurationStoreId) pluginsdk.StateRefreshFunc {
return func() (interface{}, string, error) {
log.Printf("[DEBUG] Checking to see if Configuration Store %s is name available ..", configurationStoreId)

subscriptionId := commonids.NewSubscriptionID(configurationStoreId.SubscriptionId)

parameters := operations.CheckNameAvailabilityParameters{
Name: configurationStoreId.ConfigurationStoreName,
Type: operations.ConfigurationResourceTypeMicrosoftPointAppConfigurationConfigurationStores,
}

resp, err := client.CheckNameAvailability(ctx, subscriptionId, parameters)
if err != nil {
return resp, "Error", fmt.Errorf("retrieving Deployment: %+v", err)
}

if resp.Model == nil {
return resp, "Error", fmt.Errorf("unexpected null model of %s", configurationStoreId)
}

if resp.Model.NameAvailable == nil {
return resp, "Error", fmt.Errorf("unexpected null NameAvailable property of %s", configurationStoreId)
}

if !*resp.Model.NameAvailable {
return resp, "Unavailable", nil
}
return resp, "Available", nil
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -260,8 +260,6 @@ func TestAccAppConfiguration_softDeleteRecoveryDisabled(t *testing.T) {
})
}

// This test may fail due to service API behaviour
// TODO: retry checkNameAvailability to fix this test when SDK is ready, see https://github.com/Azure/AppConfiguration/issues/677
func TestAccAppConfiguration_softDeletePurgeThenRecreate(t *testing.T) {
data := acceptance.BuildTestData(t, "azurerm_app_configuration", "test")
r := AppConfigurationResource{}
Expand Down
9 changes: 9 additions & 0 deletions internal/services/appconfiguration/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/hashicorp/go-azure-helpers/lang/response"
"github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/configurationstores"
"github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/deletedconfigurationstores"
"github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/operations"
authWrapper "github.com/hashicorp/go-azure-sdk/sdk/auth/autorest"
"github.com/hashicorp/go-azure-sdk/sdk/environments"
"github.com/hashicorp/terraform-provider-azurerm/internal/common"
Expand All @@ -18,6 +19,7 @@ import (
type Client struct {
ConfigurationStoresClient *configurationstores.ConfigurationStoresClient
DeletedConfigurationStoresClient *deletedconfigurationstores.DeletedConfigurationStoresClient
OperationsClient *operations.OperationsClient
authorizerFunc common.ApiAuthorizerFunc
configureClientFunc func(c *autorest.Client, authorizer autorest.Authorizer)
}
Expand Down Expand Up @@ -129,9 +131,16 @@ func NewClient(o *common.ClientOptions) (*Client, error) {
}
o.Configure(deletedConfigurationStores.Client, o.Authorizers.ResourceManager)

operationsClient, err := operations.NewOperationsClientWithBaseURI(o.Environment.ResourceManager)
if err != nil {
return nil, fmt.Errorf("building Operations client: %+v", err)
}
o.Configure(operationsClient.Client, o.Authorizers.ResourceManager)

return &Client{
ConfigurationStoresClient: configurationStores,
DeletedConfigurationStoresClient: deletedConfigurationStores,
OperationsClient: operationsClient,
authorizerFunc: o.Authorizers.AuthorizerFunc,
configureClientFunc: o.ConfigureClient,
}, nil
Expand Down
55 changes: 18 additions & 37 deletions internal/services/appconfiguration/client/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ import (
"strings"
"sync"

"github.com/hashicorp/go-azure-helpers/lang/pointer"
"github.com/hashicorp/go-azure-helpers/lang/response"
"github.com/hashicorp/go-azure-helpers/resourcemanager/commonids"
"github.com/hashicorp/go-azure-sdk/resource-manager/appconfiguration/2023-03-01/configurationstores"
resourcesClient "github.com/hashicorp/terraform-provider-azurerm/internal/services/resource/client"
"github.com/hashicorp/terraform-provider-azurerm/utils"
)

var (
Expand All @@ -34,7 +34,7 @@ func (c Client) AddToCache(configurationStoreId configurationstores.Configuratio
keysmith.Unlock()
}

func (c Client) ConfigurationStoreIDFromEndpoint(ctx context.Context, resourcesClient *resourcesClient.Client, configurationStoreEndpoint string) (*string, error) {
func (c Client) ConfigurationStoreIDFromEndpoint(ctx context.Context, configurationStoresClient *configurationstores.ConfigurationStoresClient, configurationStoreEndpoint string, subscriptionId string) (*string, error) {
configurationStoreName, err := c.parseNameFromEndpoint(configurationStoreEndpoint)
if err != nil {
return nil, err
Expand All @@ -53,42 +53,23 @@ func (c Client) ConfigurationStoreIDFromEndpoint(ctx context.Context, resourcesC
return &v.configurationStoreId, nil
}

filter := fmt.Sprintf("resourceType eq 'Microsoft.AppConfiguration/configurationStores' and name eq '%s'", *configurationStoreName)
result, err := resourcesClient.ResourcesClient.List(ctx, filter, "", utils.Int32(5))
subscriptionIdStruct := commonids.NewSubscriptionID(subscriptionId)
predicate := configurationstores.ConfigurationStoreOperationPredicate{
Name: configurationStoreName,
}
result, err := configurationStoresClient.ListCompleteMatchingPredicate(ctx, subscriptionIdStruct, predicate)
if err != nil {
return nil, fmt.Errorf("listing resources matching %q: %+v", filter, err)
}

for result.NotDone() {
for _, v := range result.Values() {
if v.ID == nil {
continue
}

id, err := configurationstores.ParseConfigurationStoreIDInsensitively(*v.ID)
if err != nil {
return nil, fmt.Errorf("parsing %q: %+v", *v.ID, err)
}
if !strings.EqualFold(id.ConfigurationStoreName, *configurationStoreName) {
continue
}

resp, err := c.ConfigurationStoresClient.Get(ctx, *id)
if err != nil {
return nil, fmt.Errorf("retrieving %s: %+v", *id, err)
}
if resp.Model == nil || resp.Model.Properties == nil || resp.Model.Properties.Endpoint == nil {
return nil, fmt.Errorf("retrieving %s: `model.properties.Endpoint` was nil", *id)
}

c.AddToCache(*id, *resp.Model.Properties.Endpoint)

return utils.String(id.ID()), nil
}
return nil, fmt.Errorf("listing Configuration Stores: %+v", err)
}

if err := result.NextWithContext(ctx); err != nil {
return nil, fmt.Errorf("iterating over results: %+v", err)
if len(result.Items) != 0 {
configurationStoreId, err := configurationstores.ParseConfigurationStoreID(*result.Items[0].Id)
if err != nil {
return nil, fmt.Errorf("parsing Configuration Store ID: %+v", err)
}
c.AddToCache(*configurationStoreId, configurationStoreEndpoint)

return pointer.To(configurationStoreId.ID()), nil
}

// we haven't found it, but Data Sources and Resources need to handle this error separately
Expand Down Expand Up @@ -154,7 +135,7 @@ func (c Client) Exists(ctx context.Context, configurationStoreId configurationst
return true, nil
}

func (c Client) Purge(configurationStoreId configurationstores.ConfigurationStoreId) {
func (c Client) RemoveFromCache(configurationStoreId configurationstores.ConfigurationStoreId) {
cacheKey := c.cacheKeyForConfigurationStore(configurationStoreId.ConfigurationStoreName)
keysmith.Lock()
if lock[cacheKey] == nil {
Expand Down
Loading