diff --git a/azurerm/config.go b/azurerm/config.go index 2340498ffdc7..67439f464b6d 100644 --- a/azurerm/config.go +++ b/azurerm/config.go @@ -4,8 +4,6 @@ import ( "context" "fmt" "log" - "net/http" - "net/http/httputil" "os" "strings" "sync" @@ -66,10 +64,11 @@ import ( mainStorage "github.com/Azure/azure-sdk-for-go/storage" "github.com/Azure/go-autorest/autorest" "github.com/Azure/go-autorest/autorest/adal" - "github.com/Azure/go-autorest/autorest/azure" + az "github.com/Azure/go-autorest/autorest/azure" uuid "github.com/hashicorp/go-uuid" "github.com/hashicorp/terraform/httpclient" "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/authentication" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/azure" "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" "github.com/terraform-providers/terraform-provider-azurerm/version" ) @@ -81,7 +80,7 @@ type ArmClient struct { tenantId string subscriptionId string usingServicePrincipal bool - environment azure.Environment + environment az.Environment skipProviderRegistration bool StopContext context.Context @@ -332,59 +331,11 @@ func (c *ArmClient) configureClient(client *autorest.Client, auth autorest.Autho setUserAgent(client) client.Authorizer = auth //client.RequestInspector = azure.WithClientID(clientRequestID()) - client.Sender = buildSender() + client.Sender = azure.BuildSender() client.SkipResourceProviderRegistration = c.skipProviderRegistration client.PollingDuration = 60 * time.Minute } -func buildSender() autorest.Sender { - return autorest.DecorateSender(&http.Client{ - Transport: &http.Transport{ - Proxy: http.ProxyFromEnvironment, - }, - }, withRequestLogging()) -} - -func withRequestLogging() autorest.SendDecorator { - return func(s autorest.Sender) autorest.Sender { - return autorest.SenderFunc(func(r *http.Request) (*http.Response, error) { - // strip the authorization header prior to printing - authHeaderName := "Authorization" - auth := r.Header.Get(authHeaderName) - if auth != "" { - r.Header.Del(authHeaderName) - } - - // dump request to wire format - if dump, err := httputil.DumpRequestOut(r, true); err == nil { - log.Printf("[DEBUG] AzureRM Request: \n%s\n", dump) - } else { - // fallback to basic message - log.Printf("[DEBUG] AzureRM Request: %s to %s\n", r.Method, r.URL) - } - - // add the auth header back - if auth != "" { - r.Header.Add(authHeaderName, auth) - } - - resp, err := s.Do(r) - if resp != nil { - // dump response to wire format - if dump, err := httputil.DumpResponse(resp, true); err == nil { - log.Printf("[DEBUG] AzureRM Response for %s: \n%s\n", r.URL, dump) - } else { - // fallback to basic message - log.Printf("[DEBUG] AzureRM Response: %s for %s\n", resp.Status, r.URL) - } - } else { - log.Printf("[DEBUG] Request to %s completed with no response", r.URL) - } - return resp, err - }) - } -} - func setUserAgent(client *autorest.Client) { // TODO: This is the SDK version not the CLI version, once we are on 0.12, should revisit tfUserAgent := httpclient.UserAgentString() @@ -401,63 +352,12 @@ func setUserAgent(client *autorest.Client) { log.Printf("[DEBUG] AzureRM Client User Agent: %s\n", client.UserAgent) } -func getAuthorizationToken(c *authentication.Config, oauthConfig *adal.OAuthConfig, endpoint string) (*autorest.BearerAuthorizer, error) { - useServicePrincipal := c.ClientSecret != "" - - if useServicePrincipal { - spt, err := adal.NewServicePrincipalToken(*oauthConfig, c.ClientID, c.ClientSecret, endpoint) - if err != nil { - return nil, err - } - - auth := autorest.NewBearerAuthorizer(spt) - return auth, nil - } - - if c.UseMsi { - spt, err := adal.NewServicePrincipalTokenFromMSI(c.MsiEndpoint, endpoint) - if err != nil { - return nil, err - } - auth := autorest.NewBearerAuthorizer(spt) - return auth, nil - } - - if c.IsCloudShell { - // load the refreshed tokens from the Azure CLI - err := c.LoadTokensFromAzureCLI() - if err != nil { - return nil, fmt.Errorf("Error loading the refreshed CloudShell tokens: %+v", err) - } - } - - spt, err := adal.NewServicePrincipalTokenFromManualToken(*oauthConfig, c.ClientID, endpoint, *c.AccessToken) - if err != nil { - return nil, err - } - - err = spt.Refresh() - - if err != nil { - return nil, fmt.Errorf("Error refreshing Service Principal Token: %+v", err) - } - - auth := autorest.NewBearerAuthorizer(spt) - return auth, nil -} - // getArmClient is a helper method which returns a fully instantiated // *ArmClient based on the Config's current settings. func getArmClient(c *authentication.Config) (*ArmClient, error) { - // detect cloud from environment - env, envErr := azure.EnvironmentFromName(c.Environment) - if envErr != nil { - // try again with wrapped value to support readable values like german instead of AZUREGERMANCLOUD - wrapped := fmt.Sprintf("AZURE%sCLOUD", c.Environment) - var innerErr error - if env, innerErr = azure.EnvironmentFromName(wrapped); innerErr != nil { - return nil, envErr - } + env, err := authentication.DetermineEnvironment(c.Environment) + if err != nil { + return nil, err } // client declarations: @@ -465,7 +365,7 @@ func getArmClient(c *authentication.Config) (*ArmClient, error) { clientId: c.ClientID, tenantId: c.TenantID, subscriptionId: c.SubscriptionID, - environment: env, + environment: *env, usingServicePrincipal: c.ClientSecret != "", skipProviderRegistration: c.SkipProviderRegistration, } @@ -480,25 +380,24 @@ func getArmClient(c *authentication.Config) (*ArmClient, error) { return nil, fmt.Errorf("Unable to configure OAuthConfig for tenant %s", c.TenantID) } - sender := buildSender() - // Resource Manager endpoints endpoint := env.ResourceManagerEndpoint - auth, err := getAuthorizationToken(c, oauthConfig, endpoint) + auth, err := authentication.GetAuthorizationToken(c, oauthConfig, endpoint) if err != nil { return nil, err } // Graph Endpoints graphEndpoint := env.GraphEndpoint - graphAuth, err := getAuthorizationToken(c, oauthConfig, graphEndpoint) + graphAuth, err := authentication.GetAuthorizationToken(c, oauthConfig, graphEndpoint) if err != nil { return nil, err } // Key Vault Endpoints + sender := azure.BuildSender() keyVaultAuth := autorest.NewBearerAuthorizerCallback(sender, func(tenantID, resource string) (*autorest.BearerAuthorizer, error) { - keyVaultSpt, err := getAuthorizationToken(c, oauthConfig, resource) + keyVaultSpt, err := authentication.GetAuthorizationToken(c, oauthConfig, resource) if err != nil { return nil, err } diff --git a/azurerm/helpers/authentication/authorization_token.go b/azurerm/helpers/authentication/authorization_token.go new file mode 100644 index 000000000000..77927b8f8259 --- /dev/null +++ b/azurerm/helpers/authentication/authorization_token.go @@ -0,0 +1,54 @@ +package authentication + +import ( + "fmt" + + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/adal" +) + +// GetAuthorizationToken returns an authentication token for the current authentication method +func GetAuthorizationToken(c *Config, oauthConfig *adal.OAuthConfig, endpoint string) (*autorest.BearerAuthorizer, error) { + useServicePrincipal := c.ClientSecret != "" + + if useServicePrincipal { + spt, err := adal.NewServicePrincipalToken(*oauthConfig, c.ClientID, c.ClientSecret, endpoint) + if err != nil { + return nil, err + } + + auth := autorest.NewBearerAuthorizer(spt) + return auth, nil + } + + if c.UseMsi { + spt, err := adal.NewServicePrincipalTokenFromMSI(c.MsiEndpoint, endpoint) + if err != nil { + return nil, err + } + auth := autorest.NewBearerAuthorizer(spt) + return auth, nil + } + + if c.IsCloudShell { + // load the refreshed tokens from the Azure CLI + err := c.LoadTokensFromAzureCLI() + if err != nil { + return nil, fmt.Errorf("Error loading the refreshed CloudShell tokens: %+v", err) + } + } + + spt, err := adal.NewServicePrincipalTokenFromManualToken(*oauthConfig, c.ClientID, endpoint, *c.AccessToken) + if err != nil { + return nil, err + } + + err = spt.Refresh() + + if err != nil { + return nil, fmt.Errorf("Error refreshing Service Principal Token: %+v", err) + } + + auth := autorest.NewBearerAuthorizer(spt) + return auth, nil +} diff --git a/azurerm/helpers/authentication/config.go b/azurerm/helpers/authentication/config.go index 3dac57a521fd..70bbb8dec4d8 100644 --- a/azurerm/helpers/authentication/config.go +++ b/azurerm/helpers/authentication/config.go @@ -9,10 +9,12 @@ import ( "github.com/Azure/go-autorest/autorest/azure/cli" ) +// TODO: separate objects for Input/Output + // Config is the configuration structure used to instantiate a // new Azure management client. type Config struct { - ManagementURL string + // TODO: feature toggles for which Authentication Providers are supported // Core ClientID string @@ -32,6 +34,9 @@ type Config struct { MsiEndpoint string } +// LoadTokensFromAzureCLI loads the access tokens and subscription/tenant ID's from the +// Azure CLI metadata if it's not provided +// NOTE: this'll become an internal-only method in the near future func (c *Config) LoadTokensFromAzureCLI() error { profilePath, err := cli.ProfilePath() if err != nil { diff --git a/azurerm/helpers/authentication/environment.go b/azurerm/helpers/authentication/environment.go index b18da1f22568..73065638f212 100644 --- a/azurerm/helpers/authentication/environment.go +++ b/azurerm/helpers/authentication/environment.go @@ -1,6 +1,29 @@ package authentication -import "strings" +import ( + "fmt" + "strings" + + "github.com/Azure/go-autorest/autorest/azure" +) + +// DetermineEnvironment determines what the Environment name is within +// the Azure SDK for Go and then returns the association environment, if it exists. +func DetermineEnvironment(name string) (*azure.Environment, error) { + // detect cloud from environment + env, envErr := azure.EnvironmentFromName(name) + + if envErr != nil { + // try again with wrapped value to support readable values like german instead of AZUREGERMANCLOUD + wrapped := fmt.Sprintf("AZURE%sCLOUD", name) + env, envErr = azure.EnvironmentFromName(wrapped) + if envErr != nil { + return nil, fmt.Errorf("An Azure Environment with name %q was not found: %+v", name, envErr) + } + } + + return &env, nil +} func normalizeEnvironmentName(input string) string { // Environment is stored as `Azure{Environment}Cloud` diff --git a/azurerm/helpers/authentication/validation.go b/azurerm/helpers/authentication/validation.go index 460441065515..bedce68e63e7 100644 --- a/azurerm/helpers/authentication/validation.go +++ b/azurerm/helpers/authentication/validation.go @@ -2,11 +2,56 @@ package authentication import ( "fmt" + "log" + "github.com/Azure/go-autorest/autorest/adal" "github.com/hashicorp/go-multierror" ) -func (c *Config) ValidateBearerAuth() error { +// Validate ensures that the current set of credentials provided +// are valid for the selected authentication type (e.g. Client Secret, Azure CLI, MSI etc.) +func (c *Config) Validate() error { + if c.UseMsi { + log.Printf("[DEBUG] use_msi specified - using MSI Authentication") + if c.MsiEndpoint == "" { + msiEndpoint, err := adal.GetMSIVMEndpoint() + if err != nil { + return fmt.Errorf("Could not retrieve MSI endpoint from VM settings."+ + "Ensure the VM has MSI enabled, or try setting msi_endpoint. Error: %s", err) + } + c.MsiEndpoint = msiEndpoint + } + log.Printf("[DEBUG] Using MSI endpoint %s", c.MsiEndpoint) + if err := c.validateMsi(); err != nil { + return err + } + + return nil + } + + if c.ClientSecret != "" { + log.Printf("[DEBUG] Client Secret specified - using Service Principal for Authentication") + if err := c.validateServicePrincipal(); err != nil { + return err + } + + return nil + } + + // Azure CLI / CloudShell + log.Printf("[DEBUG] No Client Secret specified - loading credentials from Azure CLI") + if err := c.LoadTokensFromAzureCLI(); err != nil { + return err + } + + if err := c.validateAzureCliBearerAuth(); err != nil { + return fmt.Errorf("Please specify either a Service Principal, or log in with the Azure CLI (using `az login`)") + } + + return nil +} + +func (c *Config) validateAzureCliBearerAuth() error { var err *multierror.Error if c.AccessToken == nil { @@ -28,7 +73,7 @@ func (c *Config) ValidateBearerAuth() error { return err.ErrorOrNil() } -func (c *Config) ValidateServicePrincipal() error { +func (c *Config) validateServicePrincipal() error { var err *multierror.Error if c.SubscriptionID == "" { @@ -50,7 +95,7 @@ func (c *Config) ValidateServicePrincipal() error { return err.ErrorOrNil() } -func (c *Config) ValidateMsi() error { +func (c *Config) validateMsi() error { var err *multierror.Error if c.SubscriptionID == "" { diff --git a/azurerm/helpers/authentication/validation_test.go b/azurerm/helpers/authentication/validation_test.go index f69594b4bad1..62a7888cb53a 100644 --- a/azurerm/helpers/authentication/validation_test.go +++ b/azurerm/helpers/authentication/validation_test.go @@ -66,7 +66,7 @@ func TestAzureValidateBearerAuth(t *testing.T) { } for _, v := range cases { - err := v.Config.ValidateBearerAuth() + err := v.Config.validateAzureCliBearerAuth() if v.ExpectError && err == nil { t.Fatalf("Expected an error for %q: didn't get one", v.Description) @@ -153,7 +153,7 @@ func TestAzureValidateServicePrincipal(t *testing.T) { } for _, v := range cases { - err := v.Config.ValidateServicePrincipal() + err := v.Config.validateServicePrincipal() if v.ExpectError && err == nil { t.Fatalf("Expected an error for %q: didn't get one", v.Description) @@ -225,7 +225,7 @@ func TestAzureValidateMsi(t *testing.T) { } for _, v := range cases { - err := v.Config.ValidateMsi() + err := v.Config.validateMsi() if v.ExpectError && err == nil { t.Fatalf("Expected an error for %q: didn't get one", v.Description) diff --git a/azurerm/helpers/azure/sender.go b/azurerm/helpers/azure/sender.go new file mode 100644 index 000000000000..cfc4173c9d17 --- /dev/null +++ b/azurerm/helpers/azure/sender.go @@ -0,0 +1,57 @@ +package azure + +import ( + "log" + "net/http" + "net/http/httputil" + + "github.com/Azure/go-autorest/autorest" +) + +func BuildSender() autorest.Sender { + return autorest.DecorateSender(&http.Client{ + Transport: &http.Transport{ + Proxy: http.ProxyFromEnvironment, + }, + }, withRequestLogging()) +} + +func withRequestLogging() autorest.SendDecorator { + return func(s autorest.Sender) autorest.Sender { + return autorest.SenderFunc(func(r *http.Request) (*http.Response, error) { + // strip the authorization header prior to printing + authHeaderName := "Authorization" + auth := r.Header.Get(authHeaderName) + if auth != "" { + r.Header.Del(authHeaderName) + } + + // dump request to wire format + if dump, err := httputil.DumpRequestOut(r, true); err == nil { + log.Printf("[DEBUG] AzureRM Request: \n%s\n", dump) + } else { + // fallback to basic message + log.Printf("[DEBUG] AzureRM Request: %s to %s\n", r.Method, r.URL) + } + + // add the auth header back + if auth != "" { + r.Header.Add(authHeaderName, auth) + } + + resp, err := s.Do(r) + if resp != nil { + // dump response to wire format + if dump, err := httputil.DumpResponse(resp, true); err == nil { + log.Printf("[DEBUG] AzureRM Response for %s: \n%s\n", r.URL, dump) + } else { + // fallback to basic message + log.Printf("[DEBUG] AzureRM Response: %s for %s\n", resp.Status, r.URL) + } + } else { + log.Printf("[DEBUG] Request to %s completed with no response", r.URL) + } + return resp, err + }) + } +} diff --git a/azurerm/helpers/resourceproviders/registration.go b/azurerm/helpers/resourceproviders/registration.go new file mode 100644 index 000000000000..04a81f6c1710 --- /dev/null +++ b/azurerm/helpers/resourceproviders/registration.go @@ -0,0 +1,58 @@ +package resourceproviders + +import ( + "context" + "fmt" + "log" + "strings" + "sync" + + "github.com/Azure/azure-sdk-for-go/services/resources/mgmt/2017-05-10/resources" +) + +func DetermineResourceProvidersRequiringRegistration(availableResourceProviders []resources.Provider, requiredResourceProviders map[string]struct{}) map[string]struct{} { + providers := requiredResourceProviders + + // filter out any providers already registered + for _, p := range availableResourceProviders { + 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) + } + } + + return providers +} + +func RegisterForSubscription(ctx context.Context, client resources.ProvidersClient, providersToRegister map[string]struct{}) error { + var err error + var wg sync.WaitGroup + wg.Add(len(providersToRegister)) + + for providerName := range providersToRegister { + go func(p string) { + defer wg.Done() + log.Printf("[DEBUG] Registering Resource Provider %q with namespace", p) + if innerErr := registerWithSubscription(ctx, p, client); err != nil { + err = innerErr + } + }(providerName) + } + + wg.Wait() + + return err +} + +func registerWithSubscription(ctx context.Context, providerName string, client resources.ProvidersClient) error { + _, err := client.Register(ctx, providerName) + if err != nil { + return fmt.Errorf("Cannot register provider %s with Azure Resource Manager: %s.", providerName, err) + } + + return nil +} diff --git a/azurerm/provider.go b/azurerm/provider.go index 6a0f0be870e9..7810e36b685f 100644 --- a/azurerm/provider.go +++ b/azurerm/provider.go @@ -1,17 +1,12 @@ package azurerm import ( - "context" "crypto/sha1" "encoding/base64" "encoding/hex" "fmt" - "log" "strings" - "sync" - "github.com/Azure/azure-sdk-for-go/services/resources/mgmt/2017-05-10/resources" - "github.com/Azure/go-autorest/autorest/adal" "github.com/hashicorp/terraform/helper/mutexkv" "github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/terraform" @@ -326,34 +321,9 @@ func providerConfigure(p *schema.Provider) schema.ConfigureFunc { SkipProviderRegistration: d.Get("skip_provider_registration").(bool), } - if config.UseMsi { - log.Printf("[DEBUG] use_msi specified - using MSI Authentication") - if config.MsiEndpoint == "" { - msiEndpoint, err := adal.GetMSIVMEndpoint() - if err != nil { - return nil, fmt.Errorf("Could not retrieve MSI endpoint from VM settings."+ - "Ensure the VM has MSI enabled, or try setting msi_endpoint. Error: %s", err) - } - config.MsiEndpoint = msiEndpoint - } - log.Printf("[DEBUG] Using MSI endpoint %s", config.MsiEndpoint) - if err := config.ValidateMsi(); err != nil { - return nil, err - } - } else if config.ClientSecret != "" { - log.Printf("[DEBUG] Client Secret specified - using Service Principal for Authentication") - if err := config.ValidateServicePrincipal(); err != nil { - return nil, err - } - } else { - log.Printf("[DEBUG] No Client Secret specified - loading credentials from Azure CLI") - if err := config.LoadTokensFromAzureCLI(); err != nil { - return nil, err - } - - if err := config.ValidateBearerAuth(); err != nil { - return nil, fmt.Errorf("Please specify either a Service Principal, or log in with the Azure CLI (using `az login`)") - } + err := config.Validate() + if err != nil { + return nil, fmt.Errorf("Error validating provider: %s", err) } client, err := getArmClient(config) @@ -381,9 +351,12 @@ func providerConfigure(p *schema.Provider) schema.ConfigureFunc { } if !config.SkipProviderRegistration { - err = registerAzureResourceProvidersWithSubscription(ctx, providerList.Values(), client.providersClient) + availableResourceProviders := providerList.Values() + requiredResourceProviders := requiredResourceProviders() + + err := ensureResourceProvidersAreRegistered(ctx, client.providersClient, availableResourceProviders, requiredResourceProviders) if err != nil { - return nil, err + return nil, fmt.Errorf("Error ensuring Resource Providers are registered: %s", err) } } } @@ -392,94 +365,6 @@ func providerConfigure(p *schema.Provider) schema.ConfigureFunc { } } -func registerProviderWithSubscription(ctx context.Context, providerName string, client resources.ProvidersClient) error { - _, err := client.Register(ctx, providerName) - if err != nil { - return fmt.Errorf("Cannot register provider %s with Azure Resource Manager: %s.", providerName, err) - } - - return nil -} - -func determineAzureResourceProvidersToRegister(providerList []resources.Provider) map[string]struct{} { - providers := map[string]struct{}{ - "Microsoft.ApiManagement": {}, - "Microsoft.Authorization": {}, - "Microsoft.Automation": {}, - "Microsoft.Cache": {}, - "Microsoft.Cdn": {}, - "Microsoft.CognitiveServices": {}, - "Microsoft.Compute": {}, - "Microsoft.ContainerInstance": {}, - "Microsoft.ContainerRegistry": {}, - "Microsoft.ContainerService": {}, - "Microsoft.Databricks": {}, - "Microsoft.DataLakeStore": {}, - "Microsoft.DBforMySQL": {}, - "Microsoft.DBforPostgreSQL": {}, - "Microsoft.Devices": {}, - "Microsoft.DevTestLab": {}, - "Microsoft.DocumentDB": {}, - "Microsoft.EventGrid": {}, - "Microsoft.EventHub": {}, - "Microsoft.KeyVault": {}, - "microsoft.insights": {}, - "Microsoft.Logic": {}, - "Microsoft.ManagedIdentity": {}, - "Microsoft.Management": {}, - "Microsoft.Network": {}, - "Microsoft.NotificationHubs": {}, - "Microsoft.OperationalInsights": {}, - "Microsoft.Relay": {}, - "Microsoft.Resources": {}, - "Microsoft.Search": {}, - "Microsoft.ServiceBus": {}, - "Microsoft.ServiceFabric": {}, - "Microsoft.Sql": {}, - "Microsoft.Storage": {}, - } - - // 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) - } - } - - return providers -} - -// registerAzureResourceProvidersWithSubscription uses the providers client to register -// 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(ctx context.Context, providerList []resources.Provider, client resources.ProvidersClient) error { - providers := determineAzureResourceProvidersToRegister(providerList) - - var err error - var wg sync.WaitGroup - wg.Add(len(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(ctx, p, client); err != nil { - err = innerErr - } - }(providerName) - } - - wg.Wait() - - return err -} - // armMutexKV is the instance of MutexKV for ARM resources var armMutexKV = mutexkv.NewMutexKV() diff --git a/azurerm/provider_test.go b/azurerm/provider_test.go index 4c8fc5ff3d8c..4255c4b7baa7 100644 --- a/azurerm/provider_test.go +++ b/azurerm/provider_test.go @@ -6,7 +6,6 @@ import ( "testing" "github.com/Azure/go-autorest/autorest/azure" - "github.com/davecgh/go-spew/spew" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/terraform" @@ -70,19 +69,7 @@ func testArmEnvironmentName() string { func testArmEnvironment() (*azure.Environment, error) { envName := testArmEnvironmentName() - - // detect cloud from environment - env, envErr := azure.EnvironmentFromName(envName) - if envErr != nil { - // try again with wrapped value to support readable values like german instead of AZUREGERMANCLOUD - wrapped := fmt.Sprintf("AZURE%sCLOUD", envName) - var innerErr error - if env, innerErr = azure.EnvironmentFromName(wrapped); innerErr != nil { - return nil, envErr - } - } - - return &env, nil + return authentication.DetermineEnvironment(envName) } func testGetAzureConfig(t *testing.T) *authentication.Config { @@ -104,34 +91,3 @@ func testGetAzureConfig(t *testing.T) *authentication.Config { } return &config } - -func TestAccAzureRMResourceProviderRegistration(t *testing.T) { - config := testGetAzureConfig(t) - if config == nil { - return - } - - armClient, err := getArmClient(config) - if err != nil { - t.Fatalf("Error building ARM Client: %+v", err) - } - - client := armClient.providersClient - ctx := testAccProvider.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) - } - - err = registerAzureResourceProvidersWithSubscription(ctx, providerList.Values(), client) - if err != nil { - t.Fatalf("Error registering Resource Providers: %+v", err) - } - - needingRegistration := determineAzureResourceProvidersToRegister(providerList.Values()) - if len(needingRegistration) > 0 { - t.Fatalf("'%d' Resource Providers are still Pending Registration: %s", len(needingRegistration), spew.Sprint(needingRegistration)) - } -} diff --git a/azurerm/required_resource_providers.go b/azurerm/required_resource_providers.go new file mode 100644 index 000000000000..1853ab76021b --- /dev/null +++ b/azurerm/required_resource_providers.go @@ -0,0 +1,78 @@ +package azurerm + +import ( + "context" + "log" + + "github.com/Azure/azure-sdk-for-go/services/resources/mgmt/2017-05-10/resources" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/resourceproviders" +) + +// requiredResourceProviders returns all of the Resource Providers used by the AzureRM Provider +// whilst all may not be used by every user - the intention is that we determine which should be +// registered such that we can avoid obscure errors where Resource Providers aren't registered. +// new Resource Providers should be added to this list as they're used in the Provider +// (this is the approach used by Microsoft in their tooling) +func requiredResourceProviders() map[string]struct{} { + // NOTE: Resource Providers in this list are case sensitive + return map[string]struct{}{ + "Microsoft.ApiManagement": {}, + "Microsoft.Authorization": {}, + "Microsoft.Automation": {}, + "Microsoft.Cache": {}, + "Microsoft.Cdn": {}, + "Microsoft.CognitiveServices": {}, + "Microsoft.Compute": {}, + "Microsoft.ContainerInstance": {}, + "Microsoft.ContainerRegistry": {}, + "Microsoft.ContainerService": {}, + "Microsoft.Databricks": {}, + "Microsoft.DataLakeAnalytics": {}, + "Microsoft.DataLakeStore": {}, + "Microsoft.DBforMySQL": {}, + "Microsoft.DBforPostgreSQL": {}, + "Microsoft.Devices": {}, + "Microsoft.DevSpaces": {}, + "Microsoft.DevTestLab": {}, + "Microsoft.DocumentDB": {}, + "Microsoft.EventGrid": {}, + "Microsoft.EventHub": {}, + "Microsoft.KeyVault": {}, + "microsoft.insights": {}, + "Microsoft.Logic": {}, + "Microsoft.ManagedIdentity": {}, + "Microsoft.Management": {}, + "Microsoft.Network": {}, + "Microsoft.NotificationHubs": {}, + "Microsoft.OperationalInsights": {}, + "Microsoft.OperationsManagement": {}, + "Microsoft.Relay": {}, + "Microsoft.RecoveryServices": {}, + "Microsoft.Resources": {}, + "Microsoft.Scheduler": {}, + "Microsoft.Search": {}, + "Microsoft.Security": {}, + "Microsoft.ServiceBus": {}, + "Microsoft.ServiceFabric": {}, + "Microsoft.Sql": {}, + "Microsoft.Storage": {}, + "Microsoft.Web": {}, + } +} + +func ensureResourceProvidersAreRegistered(ctx context.Context, client resources.ProvidersClient, availableRPs []resources.Provider, requiredRPs map[string]struct{}) error { + log.Printf("[DEBUG] Determining which Resource Providers require Registration") + providersToRegister := resourceproviders.DetermineResourceProvidersRequiringRegistration(availableRPs, requiredRPs) + + if len(providersToRegister) > 0 { + log.Printf("[DEBUG] Registering %d Resource Providers", len(providersToRegister)) + err := resourceproviders.RegisterForSubscription(ctx, client, providersToRegister) + if err != nil { + return err + } + } else { + log.Printf("[DEBUG] All required Resource Providers are registered") + } + + return nil +} diff --git a/azurerm/required_resource_providers_test.go b/azurerm/required_resource_providers_test.go new file mode 100644 index 000000000000..7830fc56d97c --- /dev/null +++ b/azurerm/required_resource_providers_test.go @@ -0,0 +1,49 @@ +package azurerm + +import ( + "testing" + + "github.com/davecgh/go-spew/spew" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/resourceproviders" +) + +func TestAccAzureRMEnsureRequiredResourceProvidersAreRegistered(t *testing.T) { + config := testGetAzureConfig(t) + if config == nil { + return + } + + armClient, err := getArmClient(config) + if err != nil { + t.Fatalf("Error building ARM Client: %+v", err) + } + + client := armClient.providersClient + ctx := testAccProvider.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 := requiredResourceProviders() + err = ensureResourceProvidersAreRegistered(ctx, client, availableResourceProviders, requiredResourceProviders) + if 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) + } + + 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)) + } +}