diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 03813b91b..3733b6ad0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -107,7 +107,7 @@ jobs: env: AZURE_CREDENTIALS: ${{ secrets.AZURE_CREDENTIALS }} run: | - cd test + cd test/azure APP_ID=`echo $AZURE_CREDENTIALS | jq -r -c ".clientId"` APP_PASSWORD=`echo $AZURE_CREDENTIALS | jq -r -c ".clientSecret"` @@ -131,7 +131,7 @@ jobs: export TF_VAR_client_secret="$APP_PASSWORD" # run the actual tests under the `azure` subfolder - go test ./azure/* -v -timeout 90m + go test --tags=azure -v -timeout 90m - name: report back the result if: always() env: diff --git a/examples/azure/terraform-azure-sqlmanagedinstance-example/README.md b/examples/azure/terraform-azure-sqlmanagedinstance-example/README.md new file mode 100644 index 000000000..b4f7fcedb --- /dev/null +++ b/examples/azure/terraform-azure-sqlmanagedinstance-example/README.md @@ -0,0 +1,35 @@ +# Terraform Azure SQL DB Example + +This folder contains a 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 a SQL Managed Instance, and a SQL Managed Instance database. + +- A [SQL Managed Instance](https://azure.microsoft.com/en-us/products/azure-sql/managed-instance/). +- A SQL Managed Database. + +Check out [test/azure/terraform_azure_sqlmanagedinstance_example_test.go](./../../../test/azure/terraform_azure_sqlmanagedinstance_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/en-us/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/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 + +**WARNING**: The deploymnet for this module usually takes more than 4-6 hours as stated in the [microsoft docs](https://learn.microsoft.com/en-us/azure/azure-sql/managed-instance/management-operations-overview?view=azuresql#duration), so please make sure to set the timeout accordingly in the below go test command. + +1. Sign up for [Azure](https://azure.microsoft.com/) +2. 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) +3. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH` +4. Configure your Terratest [Go test environment](../README.md) +5. `cd test/azure` +6. `go build terraform_azure_sqlmanagedinstance_example_test.go` +7. `go test -v -run TestTerraformAzureSQLManagedInstanceExample -timeout ` diff --git a/examples/azure/terraform-azure-sqlmanagedinstance-example/main.tf b/examples/azure/terraform-azure-sqlmanagedinstance-example/main.tf new file mode 100644 index 000000000..9e1b386b8 --- /dev/null +++ b/examples/azure/terraform-azure-sqlmanagedinstance-example/main.tf @@ -0,0 +1,128 @@ +# --------------------------------------------------------------------------------------------------------------------- +# DEPLOY AN AZURE SQL Managed Instance +# This is an example of how to deploy an AZURE SQL Managed Instance +# See test/terraform_azure_example_test.go for how to write automated tests for this code. +# --------------------------------------------------------------------------------------------------------------------- + + +# --------------------------------------------------------------------------------------------------------------------- +# CONFIGURE OUR AZURE CONNECTION +# --------------------------------------------------------------------------------------------------------------------- + +provider "azurerm" { + version = "~>3.13.0" + features {} +} + +# --------------------------------------------------------------------------------------------------------------------- +# CREATE RANDOM PASSWORD +# --------------------------------------------------------------------------------------------------------------------- + +# Random password is used as an example to simplify the deployment and improve the security of the database. +# This is not as a production recommendation as the password is stored in the Terraform state file. +resource "random_password" "password" { + length = 16 + override_special = "-_%@" + min_upper = "1" + min_lower = "1" + min_numeric = "1" + min_special = "1" +} + +# --------------------------------------------------------------------------------------------------------------------- +# DEPLOY A RESOURCE GROUP +# --------------------------------------------------------------------------------------------------------------------- + +resource "azurerm_resource_group" "sqlmi_rg" { + name = "terratest-sqlmi-${var.postfix}" + location = var.location +} + +# --------------------------------------------------------------------------------------------------------------------- +# DEPLOY NETWORK RESOURCES +# This network includes a public address for integration test demonstration purposes +# --------------------------------------------------------------------------------------------------------------------- + +resource "azurerm_network_security_group" "sqlmi_nt_sec_grp" { + name = "securitygroup-${var.postfix}" + location = azurerm_resource_group.sqlmi_rg.location + resource_group_name = azurerm_resource_group.sqlmi_rg.name +} + +resource "azurerm_network_security_rule" "allow_misubnet_inbound" { + name = "allow_subnet_${var.postfix}" + priority = 200 + direction = "Inbound" + access = "Allow" + protocol = "*" + source_port_range = "*" + destination_port_range = "*" + source_address_prefix = "10.0.0.0/24" + destination_address_prefix = "*" + resource_group_name = azurerm_resource_group.sqlmi_rg.name + network_security_group_name = azurerm_network_security_group.sqlmi_nt_sec_grp.name +} + +resource "azurerm_virtual_network" "sqlmi_vm" { + name = "vnet-${var.postfix}" + resource_group_name = azurerm_resource_group.sqlmi_rg.name + address_space = ["10.0.0.0/16"] + location = azurerm_resource_group.sqlmi_rg.location +} + +resource "azurerm_subnet" "sqlmi_sub" { + name = "subnet-${var.postfix}" + resource_group_name = azurerm_resource_group.sqlmi_rg.name + virtual_network_name = azurerm_virtual_network.sqlmi_vm.name + address_prefixes = ["10.0.0.0/24"] + + delegation { + name = "managedinstancedelegation" + + service_delegation { + name = "Microsoft.Sql/managedInstances" + actions = ["Microsoft.Network/virtualNetworks/subnets/join/action", "Microsoft.Network/virtualNetworks/subnets/prepareNetworkPolicies/action", "Microsoft.Network/virtualNetworks/subnets/unprepareNetworkPolicies/action"] + } + } +} + +resource "azurerm_subnet_network_security_group_association" "sqlmi_sb_assoc" { + subnet_id = azurerm_subnet.sqlmi_sub.id + network_security_group_id = azurerm_network_security_group.sqlmi_nt_sec_grp.id +} + +resource "azurerm_route_table" "sqlmi_rt" { + name = "routetable-${var.postfix}" + location = azurerm_resource_group.sqlmi_rg.location + resource_group_name = azurerm_resource_group.sqlmi_rg.name + disable_bgp_route_propagation = false + depends_on = [ + azurerm_subnet.sqlmi_sub, + ] +} + +resource "azurerm_subnet_route_table_association" "sqlmi_sb_rt_assoc" { + subnet_id = azurerm_subnet.sqlmi_sub.id + route_table_id = azurerm_route_table.sqlmi_rt.id +} + +# DEPLOY managed sql instance ## This depends on vnet ## +resource "azurerm_mssql_managed_instance" "sqlmi_mi" { + name = "sqlmi${var.postfix}" + resource_group_name = azurerm_resource_group.sqlmi_rg.name + location = azurerm_resource_group.sqlmi_rg.location + + license_type = var.sqlmi_license_type + sku_name = var.sku_name + storage_size_in_gb = var.storage_size + subnet_id = azurerm_subnet.sqlmi_sub.id + vcores = var.cores + + administrator_login = var.admin_login + administrator_login_password = "thisIsDog11" +} + +resource "azurerm_mssql_managed_database" "sqlmi_db" { + name = var.sqlmi_db_name + managed_instance_id = azurerm_mssql_managed_instance.sqlmi_mi.id +} \ No newline at end of file diff --git a/examples/azure/terraform-azure-sqlmanagedinstance-example/outputs.tf b/examples/azure/terraform-azure-sqlmanagedinstance-example/outputs.tf new file mode 100644 index 000000000..5cade5cd7 --- /dev/null +++ b/examples/azure/terraform-azure-sqlmanagedinstance-example/outputs.tf @@ -0,0 +1,23 @@ +output "resource_group_name" { + value = azurerm_resource_group.sqlmi_rg.name +} + +output "network_security_group_name" { + value = azurerm_network_security_group.sqlmi_nt_sec_grp.name +} + +output "virtual_network_name" { + value = azurerm_virtual_network.sqlmi_vm.name +} + +output "subnet_name" { + value = azurerm_subnet.sqlmi_sub.name +} + +output "managed_instance_name" { + value = azurerm_mssql_managed_instance.sqlmi_mi.name +} + +output "managed_instance_db_name" { + value = azurerm_mssql_managed_database.sqlmi_db.name +} \ No newline at end of file diff --git a/examples/azure/terraform-azure-sqlmanagedinstance-example/variables.tf b/examples/azure/terraform-azure-sqlmanagedinstance-example/variables.tf new file mode 100644 index 000000000..41411dd3b --- /dev/null +++ b/examples/azure/terraform-azure-sqlmanagedinstance-example/variables.tf @@ -0,0 +1,68 @@ +# --------------------------------------------------------------------------------------------------------------------- +# 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 "sqlmi_license_type" { + description = "The license type for the sql managed instance" + type = string + default = "BasePrice" +} + +variable "sku_name" { + description = "The sku name for the sql managed instance" + type = string + default = "GP_Gen5" +} + +variable "storage_size" { + description = "The storage for the sql managed instance" + type = string + default = 32 +} + +variable "cores" { + description = "The vcores for the sql managed instance" + type = string + default = 4 +} + +variable "admin_login" { + description = "The login for the sql managed instance" + type = string + default = "sqlmiadmin" +} + + +variable "sqlmi_db_name" { + description = "The Database for the sql managed instance" + type = string + default = "testdb" +} + +variable "postfix" { + description = "A postfix string to centrally mitigate resource name collisions." + type = string + default = "resource" +} \ No newline at end of file diff --git a/modules/azure/client_factory.go b/modules/azure/client_factory.go index 4807785cd..dbfc3201c 100644 --- a/modules/azure/client_factory.go +++ b/modules/azure/client_factory.go @@ -27,6 +27,7 @@ import ( "github.com/Azure/azure-sdk-for-go/services/datafactory/mgmt/2018-06-01/datafactory" kvmng "github.com/Azure/azure-sdk-for-go/services/keyvault/mgmt/2016-10-01/keyvault" "github.com/Azure/azure-sdk-for-go/services/network/mgmt/2019-09-01/network" + sqlmi "github.com/Azure/azure-sdk-for-go/services/preview/sql/mgmt/v3.0/sql" "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/synapse/mgmt/2020-12-01/synapse" @@ -321,6 +322,64 @@ func CreateSQLServerClient(subscriptionID string) (*sql.ServersClient, error) { return &sqlClient, nil } +// CreateSQLMangedInstanceClient is a helper function that will create and setup a sql server client +func CreateSQLMangedInstanceClient(subscriptionID string) (*sqlmi.ManagedInstancesClient, error) { + // Validate Azure subscription ID + subscriptionID, err := getTargetAzureSubscription(subscriptionID) + if err != nil { + return nil, err + } + + // Lookup environment URI + baseURI, err := getBaseURI() + if err != nil { + return nil, err + } + + // Create a sql server client + sqlmiClient := sqlmi.NewManagedInstancesClientWithBaseURI(baseURI, subscriptionID) + + // Create an authorizer + authorizer, err := NewAuthorizer() + if err != nil { + return nil, err + } + + // Attach authorizer to the client + sqlmiClient.Authorizer = *authorizer + + return &sqlmiClient, nil +} + +// CreateSQLMangedDatabasesClient is a helper function that will create and setup a sql server client +func CreateSQLMangedDatabasesClient(subscriptionID string) (*sqlmi.ManagedDatabasesClient, error) { + // Validate Azure subscription ID + subscriptionID, err := getTargetAzureSubscription(subscriptionID) + if err != nil { + return nil, err + } + + // Lookup environment URI + baseURI, err := getBaseURI() + if err != nil { + return nil, err + } + + // Create a sql server client + sqlmidbClient := sqlmi.NewManagedDatabasesClientWithBaseURI(baseURI, subscriptionID) + + // Create an authorizer + authorizer, err := NewAuthorizer() + if err != nil { + return nil, err + } + + // Attach authorizer to the client + sqlmidbClient.Authorizer = *authorizer + + return &sqlmidbClient, nil +} + // CreateDatabaseClient is a helper function that will create and setup a SQL DB client func CreateDatabaseClient(subscriptionID string) (*sql.DatabasesClient, error) { // Validate Azure subscription ID diff --git a/modules/azure/sql_managedinstance.go b/modules/azure/sql_managedinstance.go new file mode 100644 index 000000000..43ea0f9fe --- /dev/null +++ b/modules/azure/sql_managedinstance.go @@ -0,0 +1,83 @@ +package azure + +import ( + "context" + + "github.com/Azure/azure-sdk-for-go/services/preview/sql/mgmt/v3.0/sql" + "github.com/gruntwork-io/terratest/modules/testing" + "github.com/stretchr/testify/require" +) + +// SQLManagedInstanceExists indicates whether the SQL Managed Instance exists for the subscription. +// This function would fail the test if there is an error. +func SQLManagedInstanceExists(t testing.TestingT, managedInstanceName string, resourceGroupName string, subscriptionID string) bool { + exists, err := SQLManagedInstanceExistsE(managedInstanceName, resourceGroupName, subscriptionID) + require.NoError(t, err) + return exists +} + +// SQLManagedInstanceExistsE indicates whether the specified SQL Managed Instance exists and may return an error. +func SQLManagedInstanceExistsE(managedInstanceName string, resourceGroupName string, subscriptionID string) (bool, error) { + _, err := GetManagedInstanceE(subscriptionID, resourceGroupName, managedInstanceName) + if err != nil { + if ResourceNotFoundErrorExists(err) { + return false, nil + } + return false, err + } + return true, nil +} + +// GetManagedInstance is a helper function that gets the sql server object. +// This function would fail the test if there is an error. +func GetManagedInstance(t testing.TestingT, resGroupName string, managedInstanceName string, subscriptionID string) *sql.ManagedInstance { + managedInstance, err := GetManagedInstanceE(subscriptionID, resGroupName, managedInstanceName) + require.NoError(t, err) + + return managedInstance +} + +// GetManagedInstanceDatabase is a helper function that gets the sql server object. +// This function would fail the test if there is an error. +func GetManagedInstanceDatabase(t testing.TestingT, resGroupName string, managedInstanceName string, databaseName string, subscriptionID string) *sql.ManagedDatabase { + managedDatabase, err := GetManagedInstanceDatabaseE(t, subscriptionID, resGroupName, managedInstanceName, databaseName) + require.NoError(t, err) + + return managedDatabase +} + +// GetManagedInstanceE is a helper function that gets the sql server object. +func GetManagedInstanceE(subscriptionID string, resGroupName string, managedInstanceName string) (*sql.ManagedInstance, error) { + // Create a SQl Server client + sqlmiClient, err := CreateSQLMangedInstanceClient(subscriptionID) + if err != nil { + return nil, err + } + + //Get the corresponding server client + sqlmi, err := sqlmiClient.Get(context.Background(), resGroupName, managedInstanceName) + if err != nil { + return nil, err + } + + //Return sql mi + return &sqlmi, nil +} + +// GetManagedInstanceDatabaseE is a helper function that gets the sql server object. +func GetManagedInstanceDatabaseE(t testing.TestingT, subscriptionID string, resGroupName string, managedInstanceName string, databaseName string) (*sql.ManagedDatabase, error) { + // Create a SQlMI db client + sqlmiDbClient, err := CreateSQLMangedDatabasesClient(subscriptionID) + if err != nil { + return nil, err + } + + //Get the corresponding client + sqlmidb, err := sqlmiDbClient.Get(context.Background(), resGroupName, managedInstanceName, databaseName) + if err != nil { + return nil, err + } + + //Return sql mi db + return &sqlmidb, nil +} diff --git a/modules/azure/sql_managedinstance_test.go b/modules/azure/sql_managedinstance_test.go new file mode 100644 index 000000000..f8a0f1cb8 --- /dev/null +++ b/modules/azure/sql_managedinstance_test.go @@ -0,0 +1,53 @@ +//go:build azure +// +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 SQL DB, these tests can be extended +*/ + +func TestSQLManagedInstanceExists(t *testing.T) { + t.Parallel() + + managedInstanceName := "" + resourceGroupName := "" + subscriptionID := "" + + exists, err := SQLManagedInstanceExistsE(managedInstanceName, resourceGroupName, subscriptionID) + + require.False(t, exists) + require.Error(t, err) +} + +func TestGetManagedInstanceE(t *testing.T) { + t.Parallel() + + resGroupName := "" + managedInstanceName := "" + subscriptionID := "" + + _, err := GetManagedInstanceE(subscriptionID, resGroupName, managedInstanceName) + require.Error(t, err) +} + +func TestGetManagedInstanceDatabasesE(t *testing.T) { + t.Parallel() + + resGroupName := "" + managedInstanceName := "" + databaseName := "" + subscriptionID := "" + + _, err := GetManagedInstanceDatabaseE(t, subscriptionID, resGroupName, managedInstanceName, databaseName) + require.Error(t, err) +} diff --git a/test/azure/terraform_azure_sqlmanagedinstance_example_test.go b/test/azure/terraform_azure_sqlmanagedinstance_example_test.go new file mode 100644 index 000000000..fb9146ad3 --- /dev/null +++ b/test/azure/terraform_azure_sqlmanagedinstance_example_test.go @@ -0,0 +1,70 @@ +//go:build azure_ci_excluded +// +build azure_ci_excluded + +// This test is tagged as !azure to prevent it from being executed from CI workflow, as SQL Managed Instance takes 6-8 hours for deployment +// Please refer to examples/azure/terraform-azure-sqlmanagedinstance-example/README.md for more details + +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 TestTerraformAzureSQLManagedInstanceExample(t *testing.T) { + if testing.Short() { + t.Skip("Skipping long-running test") + } + t.Parallel() + + uniquePostfix := strings.ToLower(random.UniqueId()) + expectedLocation := "westus" + expectedAdminLogin := "sqlmiadmin" + expectedSQLMIState := "Ready" + expectedSKUName := "GP_Gen5" + expectedDatabaseName := "testdb" + + // 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-sqlmanagedinstance-example", + Vars: map[string]interface{}{ + "postfix": uniquePostfix, + "location": expectedLocation, + "admin_login": expectedAdminLogin, + "sku_name": expectedSKUName, + "sqlmi_db_name": expectedDatabaseName, + }, + } + + // At the end of the test, run `terraform destroy` to clean up any resources that were created + defer terraform.Destroy(t, terraformOptions) + + // Run `terraform init` and `terraform apply`. Fail the test if there are any errors. + terraform.InitAndApply(t, terraformOptions) + + // Run `terraform output` to get the values of output variables + expectedResourceGroupName := terraform.Output(t, terraformOptions, "resource_group_name") + expectedManagedInstanceName := terraform.Output(t, terraformOptions, "managed_instance_name") + + // check for if data factory exists + actualManagedInstanceExits := azure.SQLManagedInstanceExists(t, expectedManagedInstanceName, expectedResourceGroupName, "") + assert.True(t, actualManagedInstanceExits) + + // Get the SQL Managed Instance details and assert them against the terraform output + actualSQLManagedInstance := azure.GetManagedInstance(t, expectedResourceGroupName, expectedManagedInstanceName, "") + actualSQLManagedInstanceDatabase := azure.GetManagedInstanceDatabase(t, expectedResourceGroupName, expectedManagedInstanceName, expectedDatabaseName, "") + + assert.Equal(t, expectedManagedInstanceName, *actualSQLManagedInstance.Name) + assert.Equal(t, expectedLocation, *actualSQLManagedInstance.Location) + assert.Equal(t, expectedSKUName, *actualSQLManagedInstance.Sku.Name) + assert.Equal(t, expectedSQLMIState, *actualSQLManagedInstance.ManagedInstanceProperties.State) + + assert.Equal(t, expectedDatabaseName, *actualSQLManagedInstanceDatabase.Name) + +}