Skip to content

Commit

Permalink
Merge pull request #6927 from terraform-providers/f/enhanced-location…
Browse files Browse the repository at this point in the history
…-validation

feature: optionally enhanced location validation
  • Loading branch information
tombuildsstuff committed May 18, 2020
2 parents e77174e + 4e8b906 commit 9404b60
Show file tree
Hide file tree
Showing 7 changed files with 378 additions and 2 deletions.
8 changes: 8 additions & 0 deletions azurerm/internal/clients/builder.go
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/hashicorp/go-azure-helpers/sender"
"github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/common"
"github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/features"
"github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/location"
)

type ClientBuilder struct {
Expand Down Expand Up @@ -42,6 +43,13 @@ func Build(ctx context.Context, builder ClientBuilder) (*Client, error) {
return nil, err
}

if features.EnhancedValidationEnabled() {
// e.g. https://management.azure.com/ but we need management.azure.com
endpoint := strings.TrimPrefix(env.ResourceManagerEndpoint, "https://")
endpoint = strings.TrimSuffix(endpoint, "/")
location.CacheSupportedLocations(ctx, endpoint)
}

// client declarations:
account, err := NewResourceManagerAccount(ctx, *builder.AuthConfig, *env)
if err != nil {
Expand Down
18 changes: 18 additions & 0 deletions azurerm/internal/features/enhanced_validation.go
@@ -0,0 +1,18 @@
package features

import (
"os"
"strings"
)

// EnhancedValidationEnabled returns whether or not the feature for Enhanced Validation is
// enabled.
//
// This functionality calls out to the Azure MetaData Service to cache the list of supported
// Azure Locations for the specified Endpoint - and then uses that to provide enhanced validation
//
// This can be enabled using the Environment Variable `ARM_PROVIDER_ENHANCED_VALIDATION` and
// defaults to 'false' at the present time - but may change in a future release.
func EnhancedValidationEnabled() bool {
return strings.EqualFold(os.Getenv("ARM_PROVIDER_ENHANCED_VALIDATION"), "true")
}
3 changes: 1 addition & 2 deletions azurerm/internal/location/schema.go
Expand Up @@ -3,7 +3,6 @@ package location
import (
"github.com/hashicorp/terraform-plugin-sdk/helper/hashcode"
"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
"github.com/terraform-providers/terraform-provider-azuread/azuread/helpers/validate"
)

// Schema returns the default Schema which should be used for Location fields
Expand All @@ -13,7 +12,7 @@ func Schema() *schema.Schema {
Type: schema.TypeString,
Required: true,
ForceNew: true,
ValidateFunc: validate.NoEmptyStrings,
ValidateFunc: EnhancedValidate,
StateFunc: StateFunc,
DiffSuppressFunc: DiffSuppressFunc,
}
Expand Down
23 changes: 23 additions & 0 deletions azurerm/internal/location/supported.go
@@ -0,0 +1,23 @@
package location

import (
"context"
"log"

"github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/sdk"
)

// supportedLocations can be (validly) nil - as such this shouldn't be relied on
var supportedLocations *[]string

// CacheSupportedLocations attempts to retrieve the supported locations from the Azure MetaData Service
// and caches them, for used in enhanced validation
func CacheSupportedLocations(ctx context.Context, endpoint string) {
locs, err := sdk.AvailableAzureLocations(ctx, endpoint)
if err != nil {
log.Printf("[DEBUG] error retrieving locations: %s. Enhanced validation will be unavailable", err)
return
}

supportedLocations = locs.Locations
}
57 changes: 57 additions & 0 deletions azurerm/internal/location/validation.go
@@ -0,0 +1,57 @@
package location

import (
"fmt"
"strings"

"github.com/terraform-providers/terraform-provider-azuread/azuread/helpers/validate"
"github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/features"
)

// this is only here to aid testing
var enhancedEnabled = features.EnhancedValidationEnabled()

// EnhancedValidate returns a validation function which attempts to validate the location
// against the list of Locations supported by this Azure Location.
//
// NOTE: this is best-effort - if the users offline, or the API doesn't return it we'll
// fall back to the original approach
func EnhancedValidate(i interface{}, k string) ([]string, []error) {
if !enhancedEnabled || supportedLocations == nil {
return validate.NoEmptyStrings(i, k)
}

return enhancedValidation(i, k)
}

func enhancedValidation(i interface{}, k string) ([]string, []error) {
v, ok := i.(string)
if !ok {
return nil, []error{fmt.Errorf("expected type of %q to be string", k)}
}

normalizedUserInput := Normalize(v)
if normalizedUserInput == "" {
return nil, []error{fmt.Errorf("%q must not be empty", k)}
}

// supportedLocations can be nil if the users offline
if supportedLocations != nil {
found := false
for _, loc := range *supportedLocations {
if normalizedUserInput == Normalize(loc) {
found = true
break
}
}

if !found {
locations := strings.Join(*supportedLocations, ",")
return nil, []error{
fmt.Errorf("%q was not found in the list of supported Azure Locations: %q", normalizedUserInput, locations),
}
}
}

return nil, nil
}
211 changes: 211 additions & 0 deletions azurerm/internal/location/validation_test.go
@@ -0,0 +1,211 @@
package location

import (
"testing"

"github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/features"
)

func TestEnhancedValidationDisabled(t *testing.T) {
testCases := []struct {
input string
valid bool
}{
{
input: "",
valid: false,
},
{
input: "chinanorth",
valid: true,
},
{
input: "China North",
valid: true,
},
{
input: "westeurope",
valid: true,
},
{
input: "West Europe",
valid: true,
},
}
enhancedEnabled = false
defer func() {
enhancedEnabled = features.EnhancedValidationEnabled()
}()

for _, testCase := range testCases {
t.Logf("Testing %q..", testCase.input)

warnings, errors := EnhancedValidate(testCase.input, "location")
valid := len(warnings) == 0 && len(errors) == 0
if testCase.valid != valid {
t.Errorf("Expected %t but got %t", testCase.valid, valid)
}
}
}

func TestEnhancedValidationEnabledButIsOffline(t *testing.T) {
testCases := []struct {
input string
valid bool
}{
{
input: "",
valid: false,
},
{
input: "chinanorth",
valid: true,
},
{
input: "China North",
valid: true,
},
{
input: "westeurope",
valid: true,
},
{
input: "West Europe",
valid: true,
},
}
enhancedEnabled = true
supportedLocations = nil
defer func() {
enhancedEnabled = features.EnhancedValidationEnabled()
}()

for _, testCase := range testCases {
t.Logf("Testing %q..", testCase.input)

warnings, errors := EnhancedValidate(testCase.input, "location")
valid := len(warnings) == 0 && len(errors) == 0
if testCase.valid != valid {
t.Logf("Expected %t but got %t", testCase.valid, valid)
t.Fail()
}
}
}

func TestEnhancedValidationEnabled(t *testing.T) {
testCases := []struct {
availableLocations []string
input string
valid bool
}{
{
availableLocations: publicLocations,
input: "",
valid: false,
},
{
availableLocations: publicLocations,
input: "chinanorth",
valid: false,
},
{
availableLocations: publicLocations,
input: "China North",
valid: false,
},
{
availableLocations: publicLocations,
input: "westeurope",
valid: true,
},
{
availableLocations: publicLocations,
input: "West Europe",
valid: true,
},
{
availableLocations: chinaLocations,
input: "chinanorth",
valid: true,
},
{
availableLocations: chinaLocations,
input: "China North",
valid: true,
},
{
availableLocations: chinaLocations,
input: "westeurope",
valid: false,
},
{
availableLocations: chinaLocations,
input: "West Europe",
valid: false,
},
}
enhancedEnabled = true
defer func() {
enhancedEnabled = features.EnhancedValidationEnabled()
supportedLocations = nil
}()

for _, testCase := range testCases {
t.Logf("Testing %q..", testCase.input)
supportedLocations = &testCase.availableLocations

warnings, errors := EnhancedValidate(testCase.input, "location")
valid := len(warnings) == 0 && len(errors) == 0
if testCase.valid != valid {
t.Logf("Expected %t but got %t", testCase.valid, valid)
t.Fail()
}
}
}

var chinaLocations = []string{"chinaeast", "chinanorth", "chinanorth2", "chinaeast2"}
var publicLocations = []string{
"westus",
"westus2",
"eastus",
"centralus",
"southcentralus",
"northcentralus",
"westcentralus",
"eastus2",
"brazilsouth",
"brazilus",
"northeurope",
"westeurope",
"eastasia",
"southeastasia",
"japanwest",
"japaneast",
"koreacentral",
"koreasouth",
"indiasouth",
"indiawest",
"indiacentral",
"australiaeast",
"australiasoutheast",
"canadacentral",
"canadaeast",
"uknorth",
"uksouth2",
"uksouth",
"ukwest",
"francecentral",
"francesouth",
"australiacentral",
"australiacentral2",
"uaecentral",
"uaenorth",
"southafricanorth",
"southafricawest",
"switzerlandnorth",
"switzerlandwest",
"germanynorth",
"germanywestcentral",
"norwayeast",
"norwaywest",
}

0 comments on commit 9404b60

Please sign in to comment.