From 98c3b16f556329fcbe065e897e5a9046f7390e42 Mon Sep 17 00:00:00 2001 From: Peter McAtominey Date: Mon, 2 Jan 2017 12:09:26 +0000 Subject: [PATCH] provider/azurerm: avoid registering providers already registered, add option to disable The provider now consults the Providers API to detect which providers are already registered and uses this call to test the credentials A new option `skip_provider_registration` / `ARM_SKIP_PROVIDER_REGISTRATION` is now available to opt out of provider registration entirely --- builtin/providers/azurerm/config.go | 9 -- builtin/providers/azurerm/provider.go | 100 +++++++++++------- .../providers/azurerm/index.html.markdown | 6 ++ 3 files changed, 68 insertions(+), 47 deletions(-) diff --git a/builtin/providers/azurerm/config.go b/builtin/providers/azurerm/config.go index d72363f6d2..4b6e35be65 100644 --- a/builtin/providers/azurerm/config.go +++ b/builtin/providers/azurerm/config.go @@ -144,15 +144,6 @@ func (c *Config) getArmClient() (*ArmClient, error) { if err != nil { return nil, fmt.Errorf("Error creating Riviera client: %s", err) } - - // validate that the credentials are correct using Riviera. Note that this must be - // done _before_ using the Microsoft SDK, because Riviera handles errors. Using a - // namespace registration instead of a simple OAuth token refresh guarantees that - // service delegation is correct. This has the effect of registering Microsoft.Compute - // which is neccessary anyway. - if err := registerProviderWithSubscription("Microsoft.Compute", rivieraClient); err != nil { - return nil, err - } client.rivieraClient = rivieraClient oauthConfig, err := azure.PublicCloud.OAuthConfigForTenant(c.TenantID) diff --git a/builtin/providers/azurerm/provider.go b/builtin/providers/azurerm/provider.go index d1fa7d6822..e626688cdb 100644 --- a/builtin/providers/azurerm/provider.go +++ b/builtin/providers/azurerm/provider.go @@ -7,6 +7,9 @@ import ( "sync" + "log" + + "github.com/Azure/azure-sdk-for-go/arm/resources/resources" "github.com/hashicorp/go-multierror" "github.com/hashicorp/terraform/helper/mutexkv" "github.com/hashicorp/terraform/helper/resource" @@ -43,6 +46,12 @@ func Provider() terraform.ResourceProvider { Required: true, DefaultFunc: schema.EnvDefaultFunc("ARM_TENANT_ID", ""), }, + + "skip_provider_registration": { + Type: schema.TypeBool, + Optional: true, + DefaultFunc: schema.EnvDefaultFunc("ARM_SKIP_PROVIDER_REGISTRATION", false), + }, }, DataSourcesMap: map[string]*schema.Resource{ @@ -121,10 +130,11 @@ func Provider() terraform.ResourceProvider { type Config struct { ManagementURL string - SubscriptionID string - ClientID string - ClientSecret string - TenantID string + SubscriptionID string + ClientID string + ClientSecret string + TenantID string + SkipProviderRegistration bool validateCredentialsOnce sync.Once } @@ -151,10 +161,11 @@ func (c *Config) validate() error { func providerConfigure(p *schema.Provider) schema.ConfigureFunc { return func(d *schema.ResourceData) (interface{}, error) { config := &Config{ - SubscriptionID: d.Get("subscription_id").(string), - ClientID: d.Get("client_id").(string), - ClientSecret: d.Get("client_secret").(string), - TenantID: d.Get("tenant_id").(string), + SubscriptionID: d.Get("subscription_id").(string), + ClientID: d.Get("client_id").(string), + ClientSecret: d.Get("client_secret").(string), + TenantID: d.Get("tenant_id").(string), + SkipProviderRegistration: d.Get("skip_provider_registration").(bool), } if err := config.validate(); err != nil { @@ -168,30 +179,30 @@ func providerConfigure(p *schema.Provider) schema.ConfigureFunc { client.StopContext = p.StopContext() - err = registerAzureResourceProvidersWithSubscription(client.rivieraClient) + // 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.providers.List(nil, "") if err != nil { - return nil, err + return nil, fmt.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) + } + + if !config.SkipProviderRegistration { + err = registerAzureResourceProvidersWithSubscription(*providerList.Value, client.providers) + if err != nil { + return nil, err + } } return client, nil } } -func registerProviderWithSubscription(providerName string, client *riviera.Client) error { - request := client.NewRequest() - request.Command = riviera.RegisterResourceProvider{ - Namespace: providerName, - } - - response, err := request.Execute() +func registerProviderWithSubscription(providerName string, client resources.ProvidersClient) error { + _, err := client.Register(providerName) if err != nil { - return fmt.Errorf("Cannot request provider registration for Azure Resource Manager: %s.", err) - } - - if !response.IsSuccessful() { - return fmt.Errorf("Credentials for accessing the Azure Resource Manager API are likely " + - "to be incorrect, or\n the service principal does not have permission to use " + - "the Azure Service Management\n API.") + return fmt.Errorf("Cannot register provider %s with Azure Resource Manager: %s.", providerName, err) } return nil @@ -203,28 +214,41 @@ var providerRegistrationOnce sync.Once // all Azure resource providers which the Terraform provider may require (regardless of // whether they are actually used by the configuration or not). It was confirmed by Microsoft // that this is the approach their own internal tools also take. -func registerAzureResourceProvidersWithSubscription(client *riviera.Client) error { +func registerAzureResourceProvidersWithSubscription(providerList []resources.Provider, client resources.ProvidersClient) error { var err error providerRegistrationOnce.Do(func() { - // We register Microsoft.Compute during client initialization - providers := []string{ - "Microsoft.Cache", - "Microsoft.Network", - "Microsoft.Cdn", - "Microsoft.Storage", - "Microsoft.Sql", - "Microsoft.Search", - "Microsoft.Resources", - "Microsoft.ServiceBus", - "Microsoft.KeyVault", - "Microsoft.EventHub", + providers := map[string]struct{}{ + "Microsoft.Compute": struct{}{}, + "Microsoft.Cache": struct{}{}, + "Microsoft.Network": struct{}{}, + "Microsoft.Cdn": struct{}{}, + "Microsoft.Storage": struct{}{}, + "Microsoft.Sql": struct{}{}, + "Microsoft.Search": struct{}{}, + "Microsoft.Resources": struct{}{}, + "Microsoft.ServiceBus": struct{}{}, + "Microsoft.KeyVault": struct{}{}, + "Microsoft.EventHub": struct{}{}, + } + + // filter out any providers already registered + for _, p := range providerList { + if _, ok := providers[*p.Namespace]; !ok { + continue + } + + if strings.ToLower(*p.RegistrationState) == "registered" { + log.Printf("[DEBUG] Skipping provider registration for namespace %s\n", *p.Namespace) + delete(providers, *p.Namespace) + } } var wg sync.WaitGroup wg.Add(len(providers)) - for _, providerName := range providers { + for providerName := range providers { go func(p string) { defer wg.Done() + log.Printf("[DEBUG] Registering provider with namespace %s\n", p) if innerErr := registerProviderWithSubscription(p, client); err != nil { err = innerErr } diff --git a/website/source/docs/providers/azurerm/index.html.markdown b/website/source/docs/providers/azurerm/index.html.markdown index 610a2fd343..f6d9bc2afd 100644 --- a/website/source/docs/providers/azurerm/index.html.markdown +++ b/website/source/docs/providers/azurerm/index.html.markdown @@ -76,6 +76,12 @@ The following arguments are supported: * `tenant_id` - (Optional) The tenant ID to use. It can also be sourced from the `ARM_TENANT_ID` environment variable. +* `skip_provider_registration` - (Optional) Prevents the provier from registering + the ARM provider namespaces, this can be used if you don't wish to give the Active + Directory Application permission to register resource providers. It can also be + sourced from the `ARM_SKIP_PROVIDER_REGISTRATION` environment variable, defaults + to `false`. + ## Creating Credentials Azure requires that an application is added to Azure Active Directory to generate the `client_id`, `client_secret`, and `tenant_id` needed by Terraform (`subscription_id` can be recovered from your Azure account details).