From 3d07a977796fb851a802288d89ff7f42a9071f0a Mon Sep 17 00:00:00 2001 From: Engin Polat Date: Sat, 17 Jul 2021 01:40:52 +0300 Subject: [PATCH] feat: add Azure FunctionApp Module (#591) * Azure FunctionApp Module * Update Azure fucntions example * add appservice module and test * update azurefunction test * fix appservice_tests * fix TestGetAppServiceClientE test * fix typo Co-authored-by: Hadwa Abdelhalem Co-authored-by: Hadwa Gaber --- .../README.md | 33 ++++++++ .../main.tf | 81 +++++++++++++++++++ .../output.tf | 19 +++++ .../variables.tf | 31 +++++++ modules/azure/appService.go | 77 ++++++++++++++++++ modules/azure/appService_test.go | 48 +++++++++++ modules/azure/client_factory.go | 22 +++++ ...erraform_azure_functionapp_example_test.go | 55 +++++++++++++ 8 files changed, 366 insertions(+) create mode 100644 examples/azure/terraform-azure-functionapp-example/README.md create mode 100644 examples/azure/terraform-azure-functionapp-example/main.tf create mode 100644 examples/azure/terraform-azure-functionapp-example/output.tf create mode 100644 examples/azure/terraform-azure-functionapp-example/variables.tf create mode 100644 modules/azure/appService.go create mode 100644 modules/azure/appService_test.go create mode 100644 test/azure/terraform_azure_functionapp_example_test.go diff --git a/examples/azure/terraform-azure-functionapp-example/README.md b/examples/azure/terraform-azure-functionapp-example/README.md new file mode 100644 index 000000000..8e766a9a0 --- /dev/null +++ b/examples/azure/terraform-azure-functionapp-example/README.md @@ -0,0 +1,33 @@ +# Terraform Azure Function App Example + +This folder contains a Terraform module that deploys a Function App 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 [Azure Storage](https://azure.microsoft.com/en-us/services/storage/), [Azure Function App](https://azure.microsoft.com/en-us/services/functions/), [Azure Function App](https://azure.microsoft.com/en-us/services/functions/). + +Check out [test/azure/terraform_azure_functionapp_example_test.go](/test/azure/terraform_azure_functionapp_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. The resources are all part of the [Azure Free Account](https://azure.microsoft.com/free/), so if you haven't used that up, +it should be free, but you are completely responsible for all Azure charges. + +## 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/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/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_functionapp_example_test.go` +1. `go test -v -run TestTerraformAzureFunctionAppExample` diff --git a/examples/azure/terraform-azure-functionapp-example/main.tf b/examples/azure/terraform-azure-functionapp-example/main.tf new file mode 100644 index 000000000..7b2bf4271 --- /dev/null +++ b/examples/azure/terraform-azure-functionapp-example/main.tf @@ -0,0 +1,81 @@ +# --------------------------------------------------------------------------------------------------------------------- +# Deploy an Azure storage account, service plan, function app, and application insights +# This is an example of how to deploy an Azure function app. +# See test/terraform_azure_functionapp_example_test.go for how to write automated tests for this code. +# --------------------------------------------------------------------------------------------------------------------- + + +# --------------------------------------------------------------------------------------------------------------------- +# CONFIGURE OUR AZURE CONNECTION +# --------------------------------------------------------------------------------------------------------------------- +provider "azurerm" { + version = "~>2.29.0" + features {} +} + +# --------------------------------------------------------------------------------------------------------------------- +# DEPLOY THE RESOURCE GROUP +# --------------------------------------------------------------------------------------------------------------------- + +resource "azurerm_resource_group" "app_rg" { + name = "terratest-functionapp-rg-${var.postfix}" + location = var.location +} + +# --------------------------------------------------------------------------------------------------------------------- +# DEPLOY THE AZURE STORAGE ACCOUNT +# --------------------------------------------------------------------------------------------------------------------- + +resource "azurerm_storage_account" "storage" { + name = "storageaccount${var.postfix}" + resource_group_name = azurerm_resource_group.app_rg.name + location = azurerm_resource_group.app_rg.location + account_tier = "Standard" + account_replication_type = "LRS" +} + +# --------------------------------------------------------------------------------------------------------------------- +# DEPLOY THE APP SERVICE PLAN +# --------------------------------------------------------------------------------------------------------------------- + +resource "azurerm_app_service_plan" "app_service_plan" { + name = "appservice-plan-${var.postfix}" + location = azurerm_resource_group.app_rg.location + resource_group_name = azurerm_resource_group.app_rg.name + kind = "FunctionApp" + + sku { + tier = "Standard" + size = "S1" + } +} + +# --------------------------------------------------------------------------------------------------------------------- +# DEPLOY THE APPLICATION INSIGHTS +# --------------------------------------------------------------------------------------------------------------------- + +resource "azurerm_application_insights" "application_insights" { + name = "appinsights-${var.postfix}" + location = azurerm_resource_group.app_rg.location + resource_group_name = azurerm_resource_group.app_rg.name + application_type = "web" +} + +# --------------------------------------------------------------------------------------------------------------------- +# DEPLOY THE AZURE FUNCTION APP +# --------------------------------------------------------------------------------------------------------------------- + +resource "azurerm_function_app" "function_app" { + name = "functionapp-${var.postfix}" + location = azurerm_resource_group.app_rg.location + resource_group_name = azurerm_resource_group.app_rg.name + app_service_plan_id = azurerm_app_service_plan.app_service_plan.id + storage_account_name = azurerm_storage_account.storage.name + storage_account_access_key = azurerm_storage_account.storage.primary_access_key + + + app_settings = { + "APPINSIGHTS_INSTRUMENTATIONKEY" = azurerm_application_insights.application_insights.instrumentation_key + "APPLICATIONINSIGHTS_CONNECTION_STRING" = "InstrumentationKey=${azurerm_application_insights.application_insights.instrumentation_key}" + } +} diff --git a/examples/azure/terraform-azure-functionapp-example/output.tf b/examples/azure/terraform-azure-functionapp-example/output.tf new file mode 100644 index 000000000..faf3c8c95 --- /dev/null +++ b/examples/azure/terraform-azure-functionapp-example/output.tf @@ -0,0 +1,19 @@ +output "resource_group_name" { + value = azurerm_resource_group.app_rg.name +} + +output "function_app_id" { + value = azurerm_function_app.function_app.id +} + +output "default_hostname" { + value = azurerm_function_app.function_app.default_hostname +} + +output "function_app_kind" { + value = azurerm_function_app.function_app.kind +} + +output "function_app_name" { + value = azurerm_function_app.function_app.name +} diff --git a/examples/azure/terraform-azure-functionapp-example/variables.tf b/examples/azure/terraform-azure-functionapp-example/variables.tf new file mode 100644 index 000000000..fba88385d --- /dev/null +++ b/examples/azure/terraform-azure-functionapp-example/variables.tf @@ -0,0 +1,31 @@ +# --------------------------------------------------------------------------------------------------------------------- +# ENVIRONMENT VARIABLES +# Define these secrets as environment variables +# --------------------------------------------------------------------------------------------------------------------- + +# ARM_CLIENT_ID +# ARM_CLIENT_SECRET +# ARM_SUBSCRIPTION_ID +# ARM_TENANT_ID + +# --------------------------------------------------------------------------------------------------------------------- +# REQUIRED PARAMETERS +# You must provide a value for each of these parameters. +# --------------------------------------------------------------------------------------------------------------------- + +# --------------------------------------------------------------------------------------------------------------------- +# OPTIONAL PARAMETERS +# These parameters have reasonable defaults. +# --------------------------------------------------------------------------------------------------------------------- + +variable "location" { + description = "The supported azure location where the resource exists" + type = string + default = "West US2" +} + +variable "postfix" { + description = "A postfix string to centrally mitigate resource name collisions." + type = string + default = "1276" +} diff --git a/modules/azure/appService.go b/modules/azure/appService.go new file mode 100644 index 000000000..28aceb220 --- /dev/null +++ b/modules/azure/appService.go @@ -0,0 +1,77 @@ +package azure + +import ( + "context" + "testing" + + "github.com/Azure/azure-sdk-for-go/services/web/mgmt/2019-08-01/web" + "github.com/stretchr/testify/require" +) + +// AppExists indicates whether the specified application exists. +// This function would fail the test if there is an error. +func AppExists(t *testing.T, appName string, resourceGroupName string, subscriptionID string) bool { + exists, err := AppExistsE(appName, resourceGroupName, subscriptionID) + require.NoError(t, err) + + return exists +} + +// AppExistsE indicates whether the specified application exists. +func AppExistsE(appName string, resourceGroupName string, subscriptionID string) (bool, error) { + _, err := GetAppServiceE(appName, resourceGroupName, subscriptionID) + if err != nil { + if ResourceNotFoundErrorExists(err) { + return false, nil + } + return false, err + } + return true, nil +} + +// GetAppService gets the App service object +// This function would fail the test if there is an error. +func GetAppService(t *testing.T, appName string, resGroupName string, subscriptionID string) *web.Site { + site, err := GetAppServiceE(appName, resGroupName, subscriptionID) + require.NoError(t, err) + + return site +} + +// GetAppServiceE gets the App service object +func GetAppServiceE(appName string, resGroupName string, subscriptionID string) (*web.Site, error) { + rgName, err := getTargetAzureResourceGroupName(resGroupName) + if err != nil { + return nil, err + } + + client, err := GetAppServiceClientE(subscriptionID) + if err != nil { + return nil, err + } + + resource, err := client.Get(context.Background(), rgName, appName) + if err != nil { + return nil, err + } + + return &resource, nil +} + +func GetAppServiceClientE(subscriptionID string) (*web.AppsClient, error) { + // Create an Apps client + appsClient, err := CreateAppServiceClientE(subscriptionID) + if err != nil { + return nil, err + } + + // Create an authorizer + authorizer, err := NewAuthorizer() + if err != nil { + return nil, err + } + + // Attach authorizer to the client + appsClient.Authorizer = *authorizer + return appsClient, nil +} diff --git a/modules/azure/appService_test.go b/modules/azure/appService_test.go new file mode 100644 index 000000000..d96ec118a --- /dev/null +++ b/modules/azure/appService_test.go @@ -0,0 +1,48 @@ +// +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 CRUD methods are introduced for Azure MySQL server and database, these tests can be extended +*/ + +func TestAppExistsE(t *testing.T) { + t.Parallel() + + resGroupName := "" + appName := "" + subscriptionID := "" + + _, err := AppExistsE(appName, resGroupName, subscriptionID) + require.Error(t, err) +} + +func TestGetAppServiceE(t *testing.T) { + t.Parallel() + + resGroupName := "" + appName := "" + subscriptionID := "" + + _, err := GetAppServiceE(appName, resGroupName, subscriptionID) + require.Error(t, err) +} + +func TestGetAppServiceClientE(t *testing.T) { + t.Parallel() + + subscriptionID := "" + + _, err := GetAppServiceClientE(subscriptionID) + require.NoError(t, err) +} diff --git a/modules/azure/client_factory.go b/modules/azure/client_factory.go index 1dd97d8e9..a627707af 100644 --- a/modules/azure/client_factory.go +++ b/modules/azure/client_factory.go @@ -25,6 +25,7 @@ import ( "github.com/Azure/azure-sdk-for-go/services/network/mgmt/2019-09-01/network" "github.com/Azure/azure-sdk-for-go/services/resources/mgmt/2019-06-01/subscriptions" "github.com/Azure/azure-sdk-for-go/services/storage/mgmt/2019-06-01/storage" + "github.com/Azure/azure-sdk-for-go/services/web/mgmt/2019-08-01/web" autorestAzure "github.com/Azure/go-autorest/autorest/azure" ) @@ -642,6 +643,27 @@ func CreateNewVirtualNetworkClientE(subscriptionID string) (*network.VirtualNetw return &vnetClient, nil } +// CreateAppServiceClientE returns an App service client instance configured with the +// correct BaseURI depending on the Azure environment that is currently setup (or "Public", if none is setup). +func CreateAppServiceClientE(subscriptionID string) (*web.AppsClient, 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 + appsClient := web.NewAppsClientWithBaseURI(baseURI, subscriptionID) + return &appsClient, 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/test/azure/terraform_azure_functionapp_example_test.go b/test/azure/terraform_azure_functionapp_example_test.go new file mode 100644 index 000000000..3ef8fb48b --- /dev/null +++ b/test/azure/terraform_azure_functionapp_example_test.go @@ -0,0 +1,55 @@ +// +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 ( + "strings" + "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 TestTerraformAzureFunctionAppExample(t *testing.T) { + t.Parallel() + + //_random := strings.ToLower(random.UniqueId()) + uniquePostfix := strings.ToLower(random.UniqueId()) + + // website::tag::1:: Configure Terraform setting up a path to Terraform code. + terraformOptions := &terraform.Options{ + TerraformDir: "../../examples/azure/terraform-azure-functionapp-example", + Vars: map[string]interface{}{ + "postfix": uniquePostfix, + }, + } + // website::tag::5:: 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") + appName := terraform.Output(t, terraformOptions, "function_app_name") + + appId := terraform.Output(t, terraformOptions, "function_app_id") + appDefaultHostName := terraform.Output(t, terraformOptions, "default_hostname") + appKind := terraform.Output(t, terraformOptions, "function_app_kind") + + // website::tag::4:: Assert + assert.True(t, azure.AppExists(t, appName, resourceGroupName, "")) + site := azure.GetAppService(t, appName, resourceGroupName, "") + + assert.Equal(t, appId, *site.ID) + assert.Equal(t, appDefaultHostName, *site.DefaultHostName) + assert.Equal(t, appKind, *site.Kind) + + assert.NotEmpty(t, *site.OutboundIPAddresses) + assert.Equal(t, "Running", *site.State) +}