Repository: gruntwork-io/terratest (language: go)

Issue:
[Azure] Add Azure Front Door module
Add functionality to support [Azure Front Door (AFD)](https://docs.microsoft.com/en-us/azure/frontdoor/front-door-overview)
[Azure] Add Azure Front Door module
Add functionality to support [Azure Front Door (AFD)](https://docs.microsoft.com/en-us/azure/frontdoor/front-door-overview)


Discussion / hints:

Fix patch:
diff --git a/examples/azure/terraform-azure-frontdoor-example/README.md b/examples/azure/terraform-azure-frontdoor-example/README.md
new file mode 100644
index 000000000..3beaf90db
--- /dev/null
+++ b/examples/azure/terraform-azure-frontdoor-example/README.md
@@ -0,0 +1,30 @@
+# Terraform Azure Front Door Example
+
+This folder contains a simple Terraform module that deploys resources in [Azure](https://azure.microsoft.com/) to demonstrate
+how you can use Terratest to write automated tests for your Azure Terraform code. This module deploys an [Azure Front Door](https://azure.microsoft.com/en-us/services/frontdoor/).
+
+Check out [test/azure/terraform_azure_frontdoor_example_test.go](./../../../test/azure/terraform_azure_frontdoor_example_test.go) to see how you can write automated tests for this module and validate the configuration of the parameters and options. 
+
+**WARNING**: This module and the automated tests for it deploy real resources into your Azure account which can cost you money. 
+
+## Running this module manually
+
+1. Sign up for [Azure](https://azure.microsoft.com/).
+1. Configure your Azure credentials using one of the [supported methods for Azure CLI
+   tools](https://docs.microsoft.com/en-us/cli/azure/azure-cli-configuration?view=azure-cli-latest)
+1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`.
+1. Ensure [environment variables](../README.md#review-environment-variables) are available
+1. Run `terraform init`
+1. Run `terraform apply`
+1. When you're done, run `terraform destroy`.
+
+## Running automated tests against this module
+
+1. Sign up for [Azure](https://azure.microsoft.com/)
+1. Configure your Azure credentials using one of the [supported methods for Azure CLI
+   tools](https://docs.microsoft.com/en-us/cli/azure/azure-cli-configuration?view=azure-cli-latest)
+1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`
+1. Configure your Terratest [Go test environment](../README.md) 
+1. `cd test/azure`
+1. `go build terraform_azure_frontdoor_example_test.go`
+1. `go test -v -timeout 60m -tags azure -run TestTerraformAzureFrontDoorExample`
\ No newline at end of file
diff --git a/examples/azure/terraform-azure-frontdoor-example/main.tf b/examples/azure/terraform-azure-frontdoor-example/main.tf
new file mode 100644
index 000000000..ed0b2b3ba
--- /dev/null
+++ b/examples/azure/terraform-azure-frontdoor-example/main.tf
@@ -0,0 +1,70 @@
+# ---------------------------------------------------------------------------------------------------------------------
+# DEPLOY AN AZURE FRONT DOOR
+# This is an example of how to deploy an Azure Front Door with the minimum resources.
+# ---------------------------------------------------------------------------------------------------------------------
+# See test/azure/terraform_azure_frontdoor_example_test.go for how to write automated tests for this code.
+# ---------------------------------------------------------------------------------------------------------------------
+
+terraform {
+  required_version = ">=0.14.0"
+}
+
+provider "azurerm" {
+  features {}
+}
+
+# ---------------------------------------------------------------------------------------------------------------------
+# DEPLOY A RESOURCE GROUP
+# ---------------------------------------------------------------------------------------------------------------------
+
+resource "azurerm_resource_group" "rg" {
+  name     = "terratest-frontdoor-rg-${var.postfix}"
+  location = var.location
+}
+
+# ---------------------------------------------------------------------------------------------------------------------
+# DEPLOY FRONT DOOR
+# ---------------------------------------------------------------------------------------------------------------------
+
+resource "azurerm_frontdoor" "frontdoor" {
+  name                                         = "terratest-afd-${var.postfix}"
+  resource_group_name                          = azurerm_resource_group.rg.name
+  enforce_backend_pools_certificate_name_check = false
+
+  routing_rule {
+    name               = "terratestRoutingRule1"
+    accepted_protocols = ["Http", "Https"]
+    patterns_to_match  = ["/*"]
+    frontend_endpoints = ["terratestEndpoint"]
+    forwarding_configuration {
+      forwarding_protocol = "MatchRequest"
+      backend_pool_name   = "terratestBackend"
+    }
+  }
+
+  backend_pool_load_balancing {
+    name = "terratestLoadBalanceSetting"
+  }
+
+  backend_pool_health_probe {
+    name = "terratestHealthProbeSetting"
+  }
+
+  backend_pool {
+    name = "terratestBackend"
+    backend {
+      host_header = var.backend_host
+      address     = var.backend_host
+      http_port   = 80
+      https_port  = 443
+    }
+
+    load_balancing_name = "terratestLoadBalanceSetting"
+    health_probe_name   = "terratestHealthProbeSetting"
+  }
+
+  frontend_endpoint {
+    name      = "terratestEndpoint"
+    host_name = "terratest-afd-${var.postfix}.azurefd.net"
+  }
+}
diff --git a/examples/azure/terraform-azure-frontdoor-example/output.tf b/examples/azure/terraform-azure-frontdoor-example/output.tf
new file mode 100644
index 000000000..5c54cbd5d
--- /dev/null
+++ b/examples/azure/terraform-azure-frontdoor-example/output.tf
@@ -0,0 +1,18 @@
+output "resource_group_name" {
+  value = azurerm_resource_group.rg.name
+}
+
+output "front_door_name" {
+  description = "Specifies the name of the Front Door service."
+  value       = azurerm_frontdoor.frontdoor.name
+}
+
+output "front_door_url" {
+  description = "Specifies the host name of the frontend_endpoint. Must be a domain name."
+  value       = azurerm_frontdoor.frontdoor.frontend_endpoint[0].host_name
+}
+
+output "front_door_endpoint_name" {
+  description = "Specifies the friendly name of the frontend_endpoint"
+  value       = azurerm_frontdoor.frontdoor.frontend_endpoint[0].name
+}
\ No newline at end of file
diff --git a/examples/azure/terraform-azure-frontdoor-example/variables.tf b/examples/azure/terraform-azure-frontdoor-example/variables.tf
new file mode 100644
index 000000000..478092287
--- /dev/null
+++ b/examples/azure/terraform-azure-frontdoor-example/variables.tf
@@ -0,0 +1,32 @@
+# ---------------------------------------------------------------------------------------------------------------------
+# ENVIRONMENT VARIABLES
+# Define these secrets as environment variables
+# ---------------------------------------------------------------------------------------------------------------------
+
+# ARM_CLIENT_ID
+# ARM_CLIENT_SECRET
+# ARM_SUBSCRIPTION_ID
+# ARM_TENANT_ID
+
+# ---------------------------------------------------------------------------------------------------------------------
+# OPTIONAL PARAMETERS
+# These parameters have reasonable defaults.
+# ---------------------------------------------------------------------------------------------------------------------
+
+variable "location" {
+  description = "The Azure location where to deploy your resources too"
+  type        = string
+  default     = "East US"
+}
+
+variable "postfix" {
+  description = "A postfix string to centrally mitigate resource name collisions"
+  type        = string
+  default     = "resource"
+}
+
+variable "backend_host" {
+  description = "The IP address or FQDN of the backend"
+  type        = string
+  default     = "www.bing.com"
+}
\ No newline at end of file
diff --git a/modules/azure/client_factory.go b/modules/azure/client_factory.go
index 10d9e1b5c..57b3e7412 100644
--- a/modules/azure/client_factory.go
+++ b/modules/azure/client_factory.go
@@ -14,6 +14,7 @@ import (
 	"os"
 	"reflect"
 
+	"github.com/Azure/azure-sdk-for-go/profiles/latest/frontdoor/mgmt/frontdoor"
 	"github.com/Azure/azure-sdk-for-go/profiles/latest/mysql/mgmt/mysql"
 	"github.com/Azure/azure-sdk-for-go/profiles/latest/resources/mgmt/resources"
 	"github.com/Azure/azure-sdk-for-go/profiles/latest/sql/mgmt/sql"
@@ -707,6 +708,46 @@ func CreateContainerInstanceClientE(subscriptionID string) (*containerinstance.C
 	return &instanceClient, nil
 }
 
+// CreateFrontDoorClientE returns an AFD client instance configured with the
+// correct BaseURI depending on the Azure environment that is currently setup (or "Public", if none is setup).
+func CreateFrontDoorClientE(subscriptionID string) (*frontdoor.FrontDoorsClient, error) {
+	// Validate Azure subscription ID
+	subscriptionID, err := getTargetAzureSubscription(subscriptionID)
+	if err != nil {
+		return nil, err
+	}
+
+	// Lookup environment URI
+	baseURI, err := getEnvironmentEndpointE(ResourceManagerEndpointName)
+	if err != nil {
+		return nil, err
+	}
+
+	// create client
+	client := frontdoor.NewFrontDoorsClientWithBaseURI(baseURI, subscriptionID)
+	return &client, nil
+}
+
+// CreateFrontDoorFrontendEndpointClientE returns an AFD Frontend Endpoints client instance configured with the
+// correct BaseURI depending on the Azure environment that is currently setup (or "Public", if none is setup).
+func CreateFrontDoorFrontendEndpointClientE(subscriptionID string) (*frontdoor.FrontendEndpointsClient, error) {
+	// Validate Azure subscription ID
+	subscriptionID, err := getTargetAzureSubscription(subscriptionID)
+	if err != nil {
+		return nil, err
+	}
+
+	// Lookup environment URI
+	baseURI, err := getEnvironmentEndpointE(ResourceManagerEndpointName)
+	if err != nil {
+		return nil, err
+	}
+
+	// create client
+	client := frontdoor.NewFrontendEndpointsClientWithBaseURI(baseURI, subscriptionID)
+	return &client, nil
+}
+
 // GetKeyVaultURISuffixE returns the proper KeyVault URI suffix for the configured Azure environment.
 // This function would fail the test if there is an error.
 func GetKeyVaultURISuffixE() (string, error) {
diff --git a/modules/azure/frontdoor.go b/modules/azure/frontdoor.go
new file mode 100644
index 000000000..e3a5ac25e
--- /dev/null
+++ b/modules/azure/frontdoor.go
@@ -0,0 +1,127 @@
+package azure
+
+import (
+	"context"
+
+	"github.com/Azure/azure-sdk-for-go/profiles/latest/frontdoor/mgmt/frontdoor"
+	"github.com/gruntwork-io/terratest/modules/testing"
+	"github.com/stretchr/testify/require"
+)
+
+// FrontDoorExists indicates whether the Front Door exists for the subscription.
+// This function would fail the test if there is an error.
+func FrontDoorExists(t testing.TestingT, frontDoorName string, resourceGroupName string, subscriptionID string) bool {
+	exists, err := FrontDoorExistsE(frontDoorName, resourceGroupName, subscriptionID)
+	require.NoError(t, err)
+	return exists
+}
+
+// GetFrontDoor gets a Front Door by name if it exists for the subscription.
+// This function would fail the test if there is an error.
+func GetFrontDoor(t testing.TestingT, frontDoorName string, resourceGroupName string, subscriptionID string) *frontdoor.FrontDoor {
+	fd, err := GetFrontDoorE(frontDoorName, resourceGroupName, subscriptionID)
+	require.NoError(t, err)
+	return fd
+}
+
+// FrontDoorFrontendEndpointExists indicates whether the frontend endpoint exists for the provided Front Door.
+// This function would fail the test if there is an error.
+func FrontDoorFrontendEndpointExists(t testing.TestingT, endpointName string, frontDoorName string, resourceGroupName string, subscriptionID string) bool {
+	exists, err := FrontDoorFrontendEndpointExistsE(endpointName, frontDoorName, resourceGroupName, subscriptionID)
+	require.NoError(t, err)
+	return exists
+}
+
+// GetFrontDoorFrontendEndpoint gets a frontend endpoint by name for the provided Front Door if it exists for the subscription.
+// This function would fail the test if there is an error.
+func GetFrontDoorFrontendEndpoint(t testing.TestingT, endpointName string, frontDoorName string, resourceGroupName string, subscriptionID string) *frontdoor.FrontendEndpoint {
+	ep, err := GetFrontDoorFrontendEndpointE(endpointName, frontDoorName, resourceGroupName, subscriptionID)
+	require.NoError(t, err)
+	return ep
+}
+
+// FrontDoorExistsE indicates whether the specified Front Door exists and may return an error.
+func FrontDoorExistsE(frontDoorName string, resourceGroupName string, subscriptionID string) (bool, error) {
+	_, err := GetFrontDoorE(frontDoorName, resourceGroupName, subscriptionID)
+	if err != nil {
+		if ResourceNotFoundErrorExists(err) {
+			return false, nil
+		}
+		return false, err
+	}
+	return true, nil
+}
+
+// FrontDoorFrontendEndpointExistsE indicates whether the specified endpoint exists for the provided Front Door and may return an error.
+func FrontDoorFrontendEndpointExistsE(endpointName string, frontDoorName string, resourceGroupName string, subscriptionID string) (bool, error) {
+	_, err := GetFrontDoorFrontendEndpointE(endpointName, frontDoorName, resourceGroupName, subscriptionID)
+	if err != nil {
+		if ResourceNotFoundErrorExists(err) {
+			return false, nil
+		}
+		return false, err
+	}
+	return true, nil
+}
+
+// GetFrontDoorE gets the specified Front Door if it exists and may return an error.
+func GetFrontDoorE(frontDoorName, resoureGroupName, subscriptionID string) (*frontdoor.FrontDoor, error) {
+	client, err := GetFrontDoorClientE(subscriptionID)
+	if err != nil {
+		return nil, err
+	}
+
+	fd, err := client.Get(context.Background(), resoureGroupName, frontDoorName)
+	if err != nil {
+		return nil, err
+	}
+
+	return &fd, nil
+}
+
+// GetFrontDoorFrontendEndpointE gets the specified Frontend Endpoint for the provided Front Door if it exists and may return an error.
+func GetFrontDoorFrontendEndpointE(endpointName, frontDoorName, resourceGroupName, subscriptionID string) (*frontdoor.FrontendEndpoint, error) {
+	client, err := GetFrontDoorFrontendEndpointClientE(subscriptionID)
+	if err != nil {
+		return nil, err
+	}
+
+	endpoint, err := client.Get(context.Background(), resourceGroupName, frontDoorName, endpointName)
+	if err != nil {
+		return nil, err
+	}
+
+	return &endpoint, nil
+}
+
+// GetFrontDoorClientE return a front door client; otherwise error.
+func GetFrontDoorClientE(subscriptionID string) (*frontdoor.FrontDoorsClient, error) {
+	client, err := CreateFrontDoorClientE(subscriptionID)
+	if err != nil {
+		return nil, err
+	}
+
+	authorizer, err := NewAuthorizer()
+	if err != nil {
+		return nil, err
+	}
+
+	client.Authorizer = *authorizer
+	return client, nil
+}
+
+// GetFrontDoorFrontendEndpointClientE returns a front door frontend endpoints client; otherwise error.
+func GetFrontDoorFrontendEndpointClientE(subscriptionID string) (*frontdoor.FrontendEndpointsClient, error) {
+	client, err := CreateFrontDoorFrontendEndpointClientE(subscriptionID)
+	if err != nil {
+		return nil, err
+	}
+
+	authorizer, err := NewAuthorizer()
+	if err != nil {
+		return nil, err
+	}
+
+	client.Authorizer = *authorizer
+	return client, nil
+}


Test patch:
diff --git a/modules/azure/client_factory_test.go b/modules/azure/client_factory_test.go
index 81cf00dbb..9aba33273 100644
--- a/modules/azure/client_factory_test.go
+++ b/modules/azure/client_factory_test.go
@@ -288,3 +288,71 @@ func TestLoadBalancerClientBaseURISetCorrectly(t *testing.T) {
 		})
 	}
 }
+
+func TestFrontDoorClientBaseURISetCorrectly(t *testing.T) {
+	var cases = []struct {
+		CaseName        string
+		EnvironmentName string
+		ExpectedBaseURI string
+	}{
+		{"GovCloud/FrontDoorClient", govCloudEnvName, autorest.USGovernmentCloud.ResourceManagerEndpoint},
+		{"PublicCloud/FrontDoorClient", publicCloudEnvName, autorest.PublicCloud.ResourceManagerEndpoint},
+		{"ChinaCloud/FrontDoorClient", chinaCloudEnvName, autorest.ChinaCloud.ResourceManagerEndpoint},
+		{"GermanCloud/FrontDoorClient", germanyCloudEnvName, autorest.GermanCloud.ResourceManagerEndpoint},
+	}
+
+	// save any current env value and restore on exit
+	currentEnv := os.Getenv(AzureEnvironmentEnvName)
+	defer os.Setenv(AzureEnvironmentEnvName, currentEnv)
+
+	for _, tt := range cases {
+		// The following is necessary to make sure testCase's values don't
+		// get updated due to concurrency within the scope of t.Run(..) below
+		tt := tt
+		t.Run(tt.CaseName, func(t *testing.T) {
+			// Override env setting
+			os.Setenv(AzureEnvironmentEnvName, tt.EnvironmentName)
+
+			// Get a Front Door client
+			client, err := CreateFrontDoorClientE("")
+			require.NoError(t, err)
+
+			// Check for correct ARM URI
+			assert.Equal(t, tt.ExpectedBaseURI, client.BaseURI)
+		})
+	}
+}
+
+func TestFrontDoorFrontendEndpointClientBaseURISetCorrectly(t *testing.T) {
+	var cases = []struct {
+		CaseName        string
+		EnvironmentName string
+		ExpectedBaseURI string
+	}{
+		{"GovCloud/FrontDoorClient", govCloudEnvName, autorest.USGovernmentCloud.ResourceManagerEndpoint},
+		{"PublicCloud/FrontDoorClient", publicCloudEnvName, autorest.PublicCloud.ResourceManagerEndpoint},
+		{"ChinaCloud/FrontDoorClient", chinaCloudEnvName, autorest.ChinaCloud.ResourceManagerEndpoint},
+		{"GermanCloud/FrontDoorClient", germanyCloudEnvName, autorest.GermanCloud.ResourceManagerEndpoint},
+	}
+
+	// save any current env value and restore on exit
+	currentEnv := os.Getenv(AzureEnvironmentEnvName)
+	defer os.Setenv(AzureEnvironmentEnvName, currentEnv)
+
+	for _, tt := range cases {
+		// The following is necessary to make sure testCase's values don't
+		// get updated due to concurrency within the scope of t.Run(..) below
+		tt := tt
+		t.Run(tt.CaseName, func(t *testing.T) {
+			// Override env setting
+			os.Setenv(AzureEnvironmentEnvName, tt.EnvironmentName)
+
+			// Get a AFD frontend endpoint client
+			client, err := CreateFrontDoorFrontendEndpointClientE("")
+			require.NoError(t, err)
+
+			// Check for correct ARM URI
+			assert.Equal(t, tt.ExpectedBaseURI, client.BaseURI)
+		})
+	}
+}
diff --git a/modules/azure/frontdoor_test.go b/modules/azure/frontdoor_test.go
new file mode 100644
index 000000000..55aecbed9
--- /dev/null
+++ b/modules/azure/frontdoor_test.go
@@ -0,0 +1,71 @@
+// +build azure
+
+// NOTE: We use build tags to differentiate azure testing because we currently do not have azure access setup for
+// CircleCI.
+
+package azure
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+/*
+The below tests are currently stubbed out, with the expectation that they will throw errors.
+If/when methods to create and delete front door are added, these tests can be extended.
+*/
+
+func TestFrontDoorExists(t *testing.T) {
+	t.Parallel()
+
+	frontDoorName := "TestFrontDoor"
+	resourceGroupName := "TestResourceGroup"
+	subscriptionID := ""
+
+	exists, err := FrontDoorExistsE(frontDoorName, resourceGroupName, subscriptionID)
+
+	require.False(t, exists)
+	require.Error(t, err)
+}
+
+func TestGetFrontDoor(t *testing.T) {
+	t.Parallel()
+
+	frontDoorName := "TestFrontDoor"
+	resourceGroupName := "TestResourceGroup"
+	subscriptionID := ""
+
+	instance, err := GetFrontDoorE(frontDoorName, resourceGroupName, subscriptionID)
+
+	require.Nil(t, instance)
+	require.Error(t, err)
+}
+
+func TestFrontDoorFrontendEndpointExists(t *testing.T) {
+	t.Parallel()
+
+	endpointName := "TestFrontendEndpoint"
+	frontDoorName := "TestFrontDoor"
+	resourceGroupName := "TestResourceGroup"
+	subscriptionID := ""
+
+	endpoint, err := FrontDoorFrontendEndpointExistsE(endpointName, frontDoorName, resourceGroupName, subscriptionID)
+
+	require.False(t, endpoint)
+	require.Error(t, err)
+}
+
+func TestGetFrontDoorFrontendEndpoint(t *testing.T) {
+	t.Parallel()
+
+	endpointName := "TestFrontendEndpoint"
+	frontDoorName := "TestFrontDoor"
+	resourceGroupName := "TestResourceGroup"
+	subscriptionID := ""
+
+	endpoint, err := GetFrontDoorFrontendEndpointE(endpointName, frontDoorName, resourceGroupName, subscriptionID)
+
+	require.Nil(t, endpoint)
+	require.Error(t, err)
+}
diff --git a/test/azure/terraform_azure_frontdoor_example_test.go b/test/azure/terraform_azure_frontdoor_example_test.go
new file mode 100644
index 000000000..1d4a3e789
--- /dev/null
+++ b/test/azure/terraform_azure_frontdoor_example_test.go
@@ -0,0 +1,60 @@
+// +build azure
+
+// NOTE: We use build tags to differentiate azure testing because we currently do not have azure access setup for
+// CircleCI.
+
+package test
+
+import (
+	"testing"
+
+	"github.com/gruntwork-io/terratest/modules/azure"
+	"github.com/gruntwork-io/terratest/modules/random"
+	"github.com/gruntwork-io/terratest/modules/terraform"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestTerraformAzureFrontDoorExample(t *testing.T) {
+	t.Parallel()
+
+	subscriptionID := ""
+	uniquePostfix := random.UniqueId()
+
+	// website::tag::1:: Configure Terraform setting up a path to Terraform code.
+	terraformOptions := &terraform.Options{
+		// The path to where our Terraform code is located
+		TerraformDir: "../../examples/azure/terraform-azure-frontdoor-example",
+		Vars: map[string]interface{}{
+			"postfix": uniquePostfix,
+		},
+	}
+
+	// website::tag::4:: At the end of the test, run `terraform destroy` to clean up any resources that were created
+	defer terraform.Destroy(t, terraformOptions)
+
+	// website::tag::2:: Run `terraform init` and `terraform apply`. Fail the test if there are any errors.
+	terraform.InitAndApply(t, terraformOptions)
+
+	// website::tag::3:: Run `terraform output` to get the values of output variables
+	resourceGroupName := terraform.Output(t, terraformOptions, "resource_group_name")
+	frontDoorName := terraform.Output(t, terraformOptions, "front_door_name")
+	frontDoorUrl := terraform.Output(t, terraformOptions, "front_door_url")
+	frontendEndpointName := terraform.Output(t, terraformOptions, "front_door_endpoint_name")
+
+	// website::tag::4:: Get FrontDoor details and assert them against the terraform output
+	// NOTE: the value of subscriptionID can be left blank, it will be replaced by the value
+	//       of the environment variable ARM_SUBSCRIPTION_ID
+
+	frontDoorExists := azure.FrontDoorExists(t, frontDoorName, resourceGroupName, subscriptionID)
+	assert.True(t, frontDoorExists)
+
+	actualFrontDoorInstance := azure.GetFrontDoor(t, frontDoorName, resourceGroupName, subscriptionID)
+	assert.Equal(t, frontDoorName, *actualFrontDoorInstance.Name)
+
+	endpointExists := azure.FrontDoorFrontendEndpointExists(t, frontendEndpointName, frontDoorName, resourceGroupName, subscriptionID)
+	assert.True(t, endpointExists)
+
+	actualFrontDoorEndpoint := azure.GetFrontDoorFrontendEndpoint(t, frontendEndpointName, frontDoorName, resourceGroupName, subscriptionID)
+	endpointProperties := *actualFrontDoorEndpoint.FrontendEndpointProperties
+	assert.Equal(t, frontDoorUrl, *endpointProperties.HostName)
+}