Skip to content

Commit

Permalink
New Resource: Azure Front Door (#3933)
Browse files Browse the repository at this point in the history
* WIP: Initial checkin

* Updates to Frontdoor

* PIP

* PIP

* Finalized Schema

* WIP Current state

* WIP adding validation

* WIP Finished validation code

* WIP Expand RoutingRules working

* WIP All expand func implemented

* WIP: Functional CreateUpdate done

* Cast values and start on flatten functions

* WIP: Last fix routing rules

* WIP: Fully working Frontdoor

* WIP: Wasnt exactly done... fixed destroy

* gofmt code

* WIP: Added first test

* Fixed casing issue when interacting with portal

* Fix bug in fronend endpoint enum

* Bug fix for Frontend Endpoint flatten

* Add data source

* Slight update to documentation

* Added Frontend client code

* Extending frontend endpoints schema

* schema and exposed enable and disable for frontend

* Enabled custom HTTPS domain setting

* [WIP] Last code changes

* Tweeking defaults and adding validation rules

* [WIP] Update test case

* Updated documentation and exposed cname

* fixed flatten  for Cert Source AzureKeyVault

* Update to latest

* Few changes per PR review

* Fixed lint errors

* Missed one client declaration

* Fix test client

* Removed data source fix lint errs

* update CheckDestroy func

* Update website/docs/r/front_door.html.markdown

Co-Authored-By: kt <kt@katbyte.me>

* Update azurerm/resource_arm_front_door.go

Co-Authored-By: kt <kt@katbyte.me>

* Update azurerm/resource_arm_front_door.go

Co-Authored-By: kt <kt@katbyte.me>

* Update test and docs

* Added test case

* Update test per PR

* Address PR issues

* Update website/docs/r/front_door.html.markdown

Co-Authored-By: kt <kt@katbyte.me>

* Added link to internal bug tracking casing issue

* gofmt

* Fixes per PR comments

* Moving package(s) and refactor

* gofmt
  • Loading branch information
WodansSon authored Sep 7, 2019
1 parent 9b7536b commit 7c1971d
Show file tree
Hide file tree
Showing 21 changed files with 10,288 additions and 1 deletion.
3 changes: 3 additions & 0 deletions azurerm/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import (
"github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/dns"
"github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/eventgrid"
"github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/eventhub"
"github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/frontdoor"
"github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/graph"
"github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/hdinsight"
"github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/iothub"
Expand Down Expand Up @@ -102,6 +103,7 @@ type ArmClient struct {
privateDns *privatedns.Client
eventGrid *eventgrid.Client
eventhub *eventhub.Client
frontdoor *frontdoor.Client
graph *graph.Client
hdinsight *hdinsight.Client
iothub *iothub.Client
Expand Down Expand Up @@ -226,6 +228,7 @@ func getArmClient(c *authentication.Config, skipProviderRegistration bool, partn
client.dns = dns.BuildClient(o)
client.eventGrid = eventgrid.BuildClient(o)
client.eventhub = eventhub.BuildClient(o)
client.frontdoor = frontdoor.BuildClient(o)
client.graph = graph.BuildClient(o)
client.hdinsight = hdinsight.BuildClient(o)
client.iothub = iothub.BuildClient(o)
Expand Down
24 changes: 24 additions & 0 deletions azurerm/internal/services/frontdoor/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package frontdoor

import (
"github.com/Azure/azure-sdk-for-go/services/preview/frontdoor/mgmt/2019-04-01/frontdoor"
"github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/common"
)

type Client struct {
FrontDoorsClient *frontdoor.FrontDoorsClient
FrontDoorsFrontendClient *frontdoor.FrontendEndpointsClient
}

func BuildClient(o *common.ClientOptions) *Client {
frontDoorsClient := frontdoor.NewFrontDoorsClientWithBaseURI(o.ResourceManagerEndpoint, o.SubscriptionId)
o.ConfigureClient(&frontDoorsClient.Client, o.ResourceManagerAuthorizer)

frontDoorsFrontendClient := frontdoor.NewFrontendEndpointsClientWithBaseURI(o.ResourceManagerEndpoint, o.SubscriptionId)
o.ConfigureClient(&frontDoorsFrontendClient.Client, o.ResourceManagerAuthorizer)

return &Client{
FrontDoorsClient: &frontDoorsClient,
FrontDoorsFrontendClient: &frontDoorsFrontendClient,
}
}
180 changes: 180 additions & 0 deletions azurerm/internal/services/frontdoor/helper.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
package frontdoor

import (
"fmt"
"strings"

"github.com/Azure/azure-sdk-for-go/services/preview/frontdoor/mgmt/2019-04-01/frontdoor"
)

func VerifyBackendPoolExists(backendPoolName string, backendPools []interface{}) error {
if backendPoolName == "" {
return fmt.Errorf(`"backend_pool_name" cannot be empty`)
}

for _, bps := range backendPools {
backendPool := bps.(map[string]interface{})
if backendPool["name"].(string) == backendPoolName {
return nil
}
}

return fmt.Errorf(`unable to locate "backend_pool_name":%q in configuration file`, backendPoolName)
}

func AzureKeyVaultCertificateHasValues(customHttpsConfiguration map[string]interface{}, MatchAllKeys bool) bool {
certificateSecretName := customHttpsConfiguration["azure_key_vault_certificate_secret_name"]
certificateSecretVersion := customHttpsConfiguration["azure_key_vault_certificate_secret_version"]
certificateVaultId := customHttpsConfiguration["azure_key_vault_certificate_vault_id"]

if MatchAllKeys {
if strings.TrimSpace(certificateSecretName.(string)) != "" && strings.TrimSpace(certificateSecretVersion.(string)) != "" && strings.TrimSpace(certificateVaultId.(string)) != "" {
return true
}
} else {
if strings.TrimSpace(certificateSecretName.(string)) != "" || strings.TrimSpace(certificateSecretVersion.(string)) != "" || strings.TrimSpace(certificateVaultId.(string)) != "" {
return true
}
}

return false
}

func IsFrontDoorFrontendEndpointConfigurable(currentState frontdoor.CustomHTTPSProvisioningState, customHttpsProvisioningEnabled bool, frontendEndpointName string, resourceGroup string) error {
action := "disable"
if customHttpsProvisioningEnabled {
action = "enable"
}

switch currentState {
case frontdoor.CustomHTTPSProvisioningStateDisabling, frontdoor.CustomHTTPSProvisioningStateEnabling, frontdoor.CustomHTTPSProvisioningStateFailed:
return fmt.Errorf("Unable to %s the Front Door Frontend Endpoint %q (Resource Group %q) Custom Domain HTTPS state because the Frontend Endpoint is currently in the %q state", action, frontendEndpointName, resourceGroup, currentState)
default:
return nil
}
}

func NormalizeCustomHTTPSProvisioningStateToBool(provisioningState frontdoor.CustomHTTPSProvisioningState) bool {
isEnabled := false
if provisioningState == frontdoor.CustomHTTPSProvisioningStateEnabled || provisioningState == frontdoor.CustomHTTPSProvisioningStateEnabling {
isEnabled = true
}

return isEnabled
}

func GetFrontDoorBasicRouteConfigurationType(i interface{}) string {
_, ok := i.(frontdoor.ForwardingConfiguration)
if !ok {
_, ok := i.(frontdoor.RedirectConfiguration)
if !ok {
return ""
}
return "RedirectConfiguration"
} else {
return "ForwardingConfiguration"
}
}
func VerifyRoutingRuleFrontendEndpoints(routingRuleFrontends []interface{}, configFrontendEndpoints []interface{}) error {
for _, routingRuleFrontend := range routingRuleFrontends {
// Get the name of the frontend defined in the routing rule
routingRulefrontendName := routingRuleFrontend.(string)
found := false

// Loop over all of the defined frontend endpoints in the config
// seeing if we find the routing rule frontend in the list
for _, configFrontendEndpoint := range configFrontendEndpoints {
configFrontend := configFrontendEndpoint.(map[string]interface{})
configFrontendName := configFrontend["name"]
if routingRulefrontendName == configFrontendName {
found = true
break
}
}

if !found {
return fmt.Errorf(`"frontend_endpoints":%q was not found in the configuration file. verify you have the "frontend_endpoint":%q defined in the configuration file`, routingRulefrontendName, routingRulefrontendName)
}
}

return nil
}

func VerifyLoadBalancingAndHealthProbeSettings(backendPools []interface{}, loadBalancingSettings []interface{}, healthProbeSettings []interface{}) error {
for _, bps := range backendPools {
backendPool := bps.(map[string]interface{})
backendPoolName := backendPool["name"]
backendPoolLoadBalancingName := backendPool["load_balancing_name"]
backendPoolHealthProbeName := backendPool["health_probe_name"]
found := false

// Verify backend pool load balancing settings name exists
if len(loadBalancingSettings) > 0 {
for _, lbs := range loadBalancingSettings {
loadBalancing := lbs.(map[string]interface{})
loadBalancingName := loadBalancing["name"]

if loadBalancingName == backendPoolLoadBalancingName {
found = true
break
}
}

if !found {
return fmt.Errorf(`"backend_pool":%q "load_balancing_name":%q was not found in the configuration file. verify you have the "backend_pool_load_balancing":%q defined in the configuration file`, backendPoolName, backendPoolLoadBalancingName, backendPoolLoadBalancingName)
}
}

found = false

// Verify health probe settings name exists
if len(healthProbeSettings) > 0 {
for _, hps := range healthProbeSettings {
healthProbe := hps.(map[string]interface{})
healthProbeName := healthProbe["name"]

if healthProbeName == backendPoolHealthProbeName {
found = true
break
}
}

if !found {
return fmt.Errorf(`"backend_pool":%q "health_probe_name":%q was not found in the configuration file. verify you have the "backend_pool_health_probe":%q defined in the configuration file`, backendPoolName, backendPoolHealthProbeName, backendPoolHealthProbeName)
}
}
}

return nil
}

func VerifyCustomHttpsConfiguration(configFrontendEndpoints []interface{}) error {
for _, configFrontendEndpoint := range configFrontendEndpoints {
if configFrontend := configFrontendEndpoint.(map[string]interface{}); len(configFrontend) > 0 {
FrontendName := configFrontend["name"]
customHttpsEnabled := configFrontend["custom_https_provisioning_enabled"].(bool)

if chc := configFrontend["custom_https_configuration"].([]interface{}); len(chc) > 0 {
if !customHttpsEnabled {
return fmt.Errorf(`"frontend_endpoint":%q "custom_https_configuration" is invalid because "custom_https_provisioning_enabled" is set to "false". please remove the "custom_https_configuration" block from the configuration file`, FrontendName)
}

customHttpsConfiguration := chc[0].(map[string]interface{})
certificateSource := customHttpsConfiguration["certificate_source"]
if certificateSource == string(frontdoor.CertificateSourceAzureKeyVault) {
if !AzureKeyVaultCertificateHasValues(customHttpsConfiguration, true) {
return fmt.Errorf(`"frontend_endpoint":%q "custom_https_configuration" is invalid, all of the following keys must have values in the "custom_https_configuration" block: "azure_key_vault_certificate_secret_name", "azure_key_vault_certificate_secret_version", and "azure_key_vault_certificate_vault_id"`, FrontendName)
}
} else {
if AzureKeyVaultCertificateHasValues(customHttpsConfiguration, false) {
return fmt.Errorf(`"frontend_endpoint":%q "custom_https_configuration" is invalid, all of the following keys must be removed from the "custom_https_configuration" block: "azure_key_vault_certificate_secret_name", "azure_key_vault_certificate_secret_version", and "azure_key_vault_certificate_vault_id"`, FrontendName)
}
}
} else if customHttpsEnabled {
return fmt.Errorf(`"frontend_endpoint":%q configuration is invalid because "custom_https_provisioning_enabled" is set to "true" and the "custom_https_configuration" block is undefined. please add the "custom_https_configuration" block to the configuration file`, FrontendName)
}
}
}

return nil
}
83 changes: 83 additions & 0 deletions azurerm/internal/services/frontdoor/validate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package frontdoor

import (
"fmt"

"github.com/hashicorp/terraform/helper/schema"
"github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/validate"
)

func ValidateFrontDoorName(i interface{}, k string) (_ []string, errors []error) {
if m, regexErrs := validate.RegExHelper(i, k, `(^[\da-zA-Z])([-\da-zA-Z]{3,61})([\da-zA-Z]$)`); !m {
errors = append(regexErrs, fmt.Errorf(`%q must be between 5 and 63 characters in length and begin with a letter or number, end with a letter or number and may contain only letters, numbers or hyphens.`, k))
}

return nil, errors
}

func ValidateBackendPoolRoutingRuleName(i interface{}, k string) (_ []string, errors []error) {
if m, regexErrs := validate.RegExHelper(i, k, `(^[\da-zA-Z])([-\da-zA-Z]{1,88})([\da-zA-Z]$)`); !m {
errors = append(regexErrs, fmt.Errorf(`%q must be between 1 and 90 characters in length and begin with a letter or number, end with a letter or number and may contain only letters, numbers or hyphens.`, k))
}

return nil, errors
}

func ValidateFrontdoorSettings(d *schema.ResourceDiff) error {
routingRules := d.Get("routing_rule").([]interface{})
configFrontendEndpoints := d.Get("frontend_endpoint").([]interface{})
backendPools := d.Get("backend_pool").([]interface{})
loadBalancingSettings := d.Get("backend_pool_load_balancing").([]interface{})
healthProbeSettings := d.Get("backend_pool_health_probe").([]interface{})

if len(configFrontendEndpoints) == 0 {
return fmt.Errorf(`"frontend_endpoint": must have at least one "frontend_endpoint" defined, found 0`)
}

// Loop over all of the Routing Rules and validate that only one type of configuration is defined per Routing Rule
for _, rr := range routingRules {
routingRule := rr.(map[string]interface{})
routingRuleName := routingRule["name"]
redirectConfig := routingRule["redirect_configuration"].([]interface{})
forwardConfig := routingRule["forwarding_configuration"].([]interface{})

// Check 0. validate that at least one routing configuration exists per routing rule
if len(redirectConfig) == 0 && len(forwardConfig) == 0 {
return fmt.Errorf(`"routing_rule":%q is invalid. you must have either a "redirect_configuration" or a "forwarding_configuration" defined for the "routing_rule":%q `, routingRuleName, routingRuleName)
}

// Check 1. validate that only one configuration type is defined per routing rule
if len(redirectConfig) == 1 && len(forwardConfig) == 1 {
return fmt.Errorf(`"routing_rule":%q is invalid. "redirect_configuration" conflicts with "forwarding_configuration". You can only have one configuration type per each routing rule`, routingRuleName)
}

// Check 2. routing rule is a forwarding_configuration type make sure the backend_pool_name exists in the configuration file
if len(forwardConfig) > 0 {
fc := forwardConfig[0].(map[string]interface{})
if err := VerifyBackendPoolExists(fc["backend_pool_name"].(string), backendPools); err != nil {
return fmt.Errorf(`"routing_rule":%q is invalid. %+v`, routingRuleName, err)
}
}

// Check 3. validate that each routing rule frontend_endpoints are actually defined in the resource schema
if routingRuleFrontends := routingRule["frontend_endpoints"].([]interface{}); len(routingRuleFrontends) > 0 {
if err := VerifyRoutingRuleFrontendEndpoints(routingRuleFrontends, configFrontendEndpoints); err != nil {
return fmt.Errorf(`"routing_rule":%q %+v`, routingRuleName, err)
}
} else {
return fmt.Errorf(`"routing_rule": %q must have at least one "frontend_endpoints" defined`, routingRuleName)
}
}

// Verify backend pool load balancing settings and health probe settings are defined in the resource schema
if err := VerifyLoadBalancingAndHealthProbeSettings(backendPools, loadBalancingSettings, healthProbeSettings); err != nil {
return fmt.Errorf(`%+v`, err)
}

// Verify frontend endpoints custom https configuration is valid if defined
if err := VerifyCustomHttpsConfiguration(configFrontendEndpoints); err != nil {
return fmt.Errorf(`%+v`, err)
}

return nil
}
1 change: 1 addition & 0 deletions azurerm/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,7 @@ func Provider() terraform.ResourceProvider {
"azurerm_firewall_nat_rule_collection": resourceArmFirewallNatRuleCollection(),
"azurerm_firewall_network_rule_collection": resourceArmFirewallNetworkRuleCollection(),
"azurerm_firewall": resourceArmFirewall(),
"azurerm_frontdoor": resourceArmFrontDoor(),
"azurerm_function_app": resourceArmFunctionApp(),
"azurerm_hdinsight_hadoop_cluster": resourceArmHDInsightHadoopCluster(),
"azurerm_hdinsight_hbase_cluster": resourceArmHDInsightHBaseCluster(),
Expand Down
Loading

0 comments on commit 7c1971d

Please sign in to comment.