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

feature: optionally enhanced location validation #6927

Merged
merged 1 commit into from May 18, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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",
}