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

provider: adding a cache around the Resource Provider Registration #21695

Merged
merged 9 commits into from
May 9, 2023
39 changes: 17 additions & 22 deletions internal/acceptance/required_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import (
"testing"

"github.com/davecgh/go-spew/spew"
"github.com/hashicorp/go-azure-helpers/resourceproviders"
"github.com/hashicorp/go-azure-helpers/resourcemanager/commonids"
"github.com/hashicorp/terraform-provider-azurerm/internal/clients"
rmResourceProviders "github.com/hashicorp/terraform-provider-azurerm/internal/resourceproviders"
"github.com/hashicorp/terraform-provider-azurerm/internal/resourceproviders"
)

// since this depends on GetAuthConfig which lives in this package
Expand All @@ -35,32 +35,27 @@ func TestAccEnsureRequiredResourceProvidersAreRegistered(t *testing.T) {
t.Fatalf("Error building ARM Client: %+v", err)
}

client := armClient.Resource.ProvidersClient
client := armClient.Resource.ResourceProvidersClient
ctx := armClient.StopContext
providerList, err := client.List(ctx, nil, "")
if err != nil {
t.Fatalf("Unable to list provider registration status, it is possible that this is due to invalid "+
"credentials or the service principal does not have permission to use the Resource Manager API, Azure "+
"error: %s", err)
}

availableResourceProviders := providerList.Values()
requiredResourceProviders := rmResourceProviders.Required()
err = rmResourceProviders.EnsureRegistered(ctx, *client, availableResourceProviders, requiredResourceProviders)
if err != nil {
requiredResourceProviders := resourceproviders.Required()
subscriptionId := commonids.NewSubscriptionID(armClient.Account.SubscriptionId)

if err = resourceproviders.EnsureRegistered(ctx, client, subscriptionId, requiredResourceProviders); err != nil {
t.Fatalf("Error registering Resource Providers: %+v", err)
}

// refresh the list now things have been re-registered
providerList, err = client.List(ctx, nil, "")
if err != nil {
t.Fatalf("Unable to list provider registration status, it is possible that this is due to invalid "+
"credentials or the service principal does not have permission to use the Resource Manager API, Azure "+
"error: %s", err)
// refresh the cache now things have been re-registered
resourceproviders.ClearCache()
if err := resourceproviders.CacheSupportedProviders(ctx, client, subscriptionId); err != nil {
t.Fatalf("re-caching Resource Providers: %+v", err)
}

stillRequiringRegistration := resourceproviders.DetermineResourceProvidersRequiringRegistration(providerList.Values(), requiredResourceProviders)
if len(stillRequiringRegistration) > 0 {
t.Fatalf("'%d' Resource Providers are still Pending Registration: %s", len(stillRequiringRegistration), spew.Sprint(stillRequiringRegistration))
stillRequiringRegistration, err := resourceproviders.DetermineWhichRequiredResourceProvidersRequireRegistration(requiredResourceProviders)
if err != nil {
t.Fatalf("determining which Resource Providers still require Registration: %+v", err)
}
if len(*stillRequiringRegistration) > 0 {
t.Fatalf("'%d' Resource Providers are still Pending Registration: %s", len(*stillRequiringRegistration), spew.Sprint(stillRequiringRegistration))
}
}
7 changes: 6 additions & 1 deletion internal/clients/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"strings"

"github.com/hashicorp/go-azure-helpers/authentication"
"github.com/hashicorp/go-azure-helpers/resourcemanager/commonids"
"github.com/hashicorp/go-azure-helpers/resourcemanager/location"
"github.com/hashicorp/go-azure-sdk/sdk/auth"
authWrapper "github.com/hashicorp/go-azure-sdk/sdk/auth/autorest"
Expand Down Expand Up @@ -157,8 +158,12 @@ func Build(ctx context.Context, builder ClientBuilder) (*Client, error) {
}

if features.EnhancedValidationEnabled() {
subscriptionId := commonids.NewSubscriptionID(client.Account.SubscriptionId)

location.CacheSupportedLocations(ctx, *resourceManagerEndpoint)
resourceproviders.CacheSupportedProviders(ctx, client.Resource.ProvidersClient)
if err := resourceproviders.CacheSupportedProviders(ctx, client.Resource.ResourceProvidersClient, subscriptionId); err != nil {
log.Printf("[DEBUG] error retrieving providers: %s. Enhanced validation will be unavailable", err)
}
}

return &client, nil
Expand Down
17 changes: 6 additions & 11 deletions internal/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import (
"log"
"os"
"strings"
"time"

"github.com/hashicorp/go-azure-helpers/resourcemanager/commonids"
"github.com/hashicorp/go-azure-sdk/sdk/auth"
"github.com/hashicorp/go-azure-sdk/sdk/environments"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
Expand Down Expand Up @@ -455,19 +457,12 @@ func buildClient(ctx context.Context, p *schema.Provider, d *schema.ResourceData
client.StopContext = stopCtx

if !skipProviderRegistration {
// List all the available providers and their registration state to avoid unnecessary
// requests. This also lets us check if the provider credentials are correct.
providerList, err := client.Resource.ProvidersClient.List(ctx, nil, "")
if err != nil {
return nil, diag.Errorf("Unable to list provider registration status, it is possible that this is due to invalid "+
"credentials or the service principal does not have permission to use the Resource Manager API, Azure "+
"error: %s", err)
}

availableResourceProviders := providerList.Values()
subscriptionId := commonids.NewSubscriptionID(client.Account.SubscriptionId)
requiredResourceProviders := resourceproviders.Required()
ctx2, cancel := context.WithTimeout(ctx, 30*time.Minute)
defer cancel()

if err := resourceproviders.EnsureRegistered(ctx, *client.Resource.ProvidersClient, availableResourceProviders, requiredResourceProviders); err != nil {
if err := resourceproviders.EnsureRegistered(ctx2, client.Resource.ResourceProvidersClient, subscriptionId, requiredResourceProviders); err != nil {
return nil, diag.Errorf(resourceProviderRegistrationErrorFmt, err)
}
}
Expand Down
28 changes: 0 additions & 28 deletions internal/resourceproviders/azure.go

This file was deleted.

64 changes: 57 additions & 7 deletions internal/resourceproviders/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,72 @@ package resourceproviders

import (
"context"
"log"
"fmt"
"strings"
"sync"

"github.com/Azure/azure-sdk-for-go/profiles/2017-03-09/resources/mgmt/resources"
"github.com/hashicorp/go-azure-helpers/resourcemanager/commonids"
"github.com/hashicorp/go-azure-sdk/resource-manager/resources/2022-09-01/providers"
)

// cachedResourceProviders can be (validly) nil - as such this shouldn't be relied on
var cachedResourceProviders *[]string
var registeredResourceProviders *map[string]struct{}
var unregisteredResourceProviders *map[string]struct{}
manicminer marked this conversation as resolved.
Show resolved Hide resolved

var cacheLock = &sync.Mutex{}

// CacheSupportedProviders attempts to retrieve the supported Resource Providers from the Resource Manager API
// and caches them, for used in enhanced validation
func CacheSupportedProviders(ctx context.Context, client *resources.ProvidersClient) {
providers, err := availableResourceProviders(ctx, client)
func CacheSupportedProviders(ctx context.Context, client *providers.ProvidersClient, subscriptionId commonids.SubscriptionId) error {
// already populated
if cachedResourceProviders != nil {
return nil
}

if err := populateCache(ctx, client, subscriptionId); err != nil {
return fmt.Errorf("populating cache: %+v", err)
}

return nil
}

func ClearCache() {
cacheLock.Lock()
cachedResourceProviders = nil
registeredResourceProviders = nil
unregisteredResourceProviders = nil
cacheLock.Unlock()
}

func populateCache(ctx context.Context, client *providers.ProvidersClient, subscriptionId commonids.SubscriptionId) error {
cacheLock.Lock()
defer cacheLock.Unlock()

providers, err := client.ListComplete(ctx, subscriptionId, providers.DefaultListOperationOptions())
if err != nil {
log.Printf("[DEBUG] error retrieving providers: %s. Enhanced validation will be unavailable", err)
return
return fmt.Errorf("listing Resource Providers: %+v", err)
}

providerNames := make([]string, 0)
registeredProviders := make(map[string]struct{}, 0)
unregisteredProviders := make(map[string]struct{}, 0)
for _, provider := range providers.Items {
if provider.Namespace == nil {
continue
}

providerNames = append(providerNames, *provider.Namespace)
registered := provider.RegistrationState != nil && strings.EqualFold(*provider.RegistrationState, "registered")
if registered {
registeredProviders[*provider.Namespace] = struct{}{}
} else {
unregisteredProviders[*provider.Namespace] = struct{}{}
}
}

cachedResourceProviders = providers
cachedResourceProviders = &providerNames
registeredResourceProviders = &registeredProviders
unregisteredResourceProviders = &unregisteredProviders
return nil
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package custompollers

import (
"context"
"fmt"
"strings"
"time"

"github.com/hashicorp/go-azure-sdk/resource-manager/resources/2022-09-01/providers"
"github.com/hashicorp/go-azure-sdk/sdk/client/pollers"
)

var _ pollers.PollerType = &resourceProviderRegistrationPoller{}

func NewResourceProviderRegistrationPoller(client *providers.ProvidersClient, id providers.SubscriptionProviderId) *resourceProviderRegistrationPoller {
return &resourceProviderRegistrationPoller{
client: client,
id: id,
}
}

type resourceProviderRegistrationPoller struct {
client *providers.ProvidersClient
id providers.SubscriptionProviderId
}

func (p *resourceProviderRegistrationPoller) Poll(ctx context.Context) (*pollers.PollResult, error) {
resp, err := p.client.Get(ctx, p.id, providers.DefaultGetOperationOptions())
if err != nil {
return nil, fmt.Errorf("retrieving %s: %+v", p.id, err)
}

registrationState := ""
if model := resp.Model; model != nil && model.RegistrationState != nil {
registrationState = *model.RegistrationState
}

if strings.EqualFold(registrationState, "Registered") {
return &pollers.PollResult{
Status: pollers.PollingStatusSucceeded,
PollInterval: 10 * time.Second,
}, nil
}

// Processing
return &pollers.PollResult{
Status: pollers.PollingStatusInProgress,
PollInterval: 10 * time.Second,
}, nil
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package custompollers

import (
"context"
"fmt"
"strings"
"time"

"github.com/hashicorp/go-azure-sdk/resource-manager/resources/2022-09-01/providers"
"github.com/hashicorp/go-azure-sdk/sdk/client/pollers"
)

var _ pollers.PollerType = &resourceProviderUnregistrationPoller{}

func NewResourceProviderUnregistrationPoller(client *providers.ProvidersClient, id providers.SubscriptionProviderId) *resourceProviderUnregistrationPoller {
return &resourceProviderUnregistrationPoller{
client: client,
id: id,
}
}

type resourceProviderUnregistrationPoller struct {
client *providers.ProvidersClient
id providers.SubscriptionProviderId
}

func (p *resourceProviderUnregistrationPoller) Poll(ctx context.Context) (*pollers.PollResult, error) {
resp, err := p.client.Get(ctx, p.id, providers.DefaultGetOperationOptions())
if err != nil {
return nil, fmt.Errorf("retrieving %s: %+v", p.id, err)
}

registrationState := ""
if model := resp.Model; model != nil && model.RegistrationState != nil {
registrationState = *model.RegistrationState
}

if strings.EqualFold(registrationState, "Unregistered") {
return &pollers.PollResult{
Status: pollers.PollingStatusSucceeded,
PollInterval: 10 * time.Second,
}, nil
}

// Processing
return &pollers.PollResult{
Status: pollers.PollingStatusInProgress,
PollInterval: 10 * time.Second,
}, nil
}
Loading