-
Notifications
You must be signed in to change notification settings - Fork 4.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feature: optionally enhanced validation
This commit introduces a new optional feature, for the enhanced validation of Locations. The Azure MetaData Service - that is: /metadata/endpoints?api-version=2018-01-01 returns information about the locations which are suppported on the Azure Instance that we're connected to. As such, this commit optionally caches this information with the intent of providing more granular validation - to avoid cases where an unsupported location is specified. This allows Terraform to catch this error during `terraform plan` - rather than failing during `terraform apply` - which is a better user experience. This functionality is disabled by default at this time - but can conditionally be enabled via the Feature Flag using the Environment Variable `ARM_PROVIDER_ENHANCED_VALIDATION` to `true`. Example before/with this feature disabled: ``` $ ARM_PROVIDER_ENHANCED_VALIDATION=false tf plan Refreshing Terraform state in-memory prior to plan... The refreshed state will be used to calculate this plan, but will not be persisted to local or remote state storage. ------------------------------------------------------------------------ An execution plan has been generated and is shown below. Resource actions are indicated with the following symbols: + create Terraform will perform the following actions: # azurerm_resource_group.test will be created + resource "azurerm_resource_group" "test" { + id = (known after apply) + location = "chinanorth" + name = "tom-dev99" + timeouts { + create = "60m" } } Plan: 1 to add, 0 to change, 0 to destroy ``` Example with this feature enabled: ``` $ ARM_PROVIDER_ENHANCED_VALIDATION=true tf plan Refreshing Terraform state in-memory prior to plan... The refreshed state will be used to calculate this plan, but will not be persisted to local or remote state storage. ------------------------------------------------------------------------ Error: "chinanorth" was not found in the list of supported Azure Locations: "westus,westus2,eastus,centralus,centraluseuap,southcentralus,northcentralus,westcentralus,eastus2,eastus2euap,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" on main.tf line 5, in resource "azurerm_resource_group" "test": 5: resource "azurerm_resource_group" "test" { ```
- Loading branch information
1 parent
5fd5868
commit 4e8b906
Showing
7 changed files
with
378 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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") | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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", | ||
} |
Oops, something went wrong.