diff --git a/azurerm/config.go b/azurerm/config.go index 2a382a35c330..392ed5017425 100644 --- a/azurerm/config.go +++ b/azurerm/config.go @@ -142,6 +142,7 @@ type ArmClient struct { postgresqlDatabasesClient postgresql.DatabasesClient postgresqlFirewallRulesClient postgresql.FirewallRulesClient postgresqlServersClient postgresql.ServersClient + postgresqlVirtualNetworkRulesClient postgresql.VirtualNetworkRulesClient sqlDatabasesClient sql.DatabasesClient sqlElasticPoolsClient sql.ElasticPoolsClient sqlFirewallRulesClient sql.FirewallRulesClient @@ -605,6 +606,10 @@ func (c *ArmClient) registerDatabases(endpoint, subscriptionId string, auth auto c.configureClient(&postgresqlSrvClient.Client, auth) c.postgresqlServersClient = postgresqlSrvClient + postgresqlVNRClient := postgresql.NewVirtualNetworkRulesClientWithBaseURI(endpoint, subscriptionId) + c.configureClient(&postgresqlVNRClient.Client, auth) + c.postgresqlVirtualNetworkRulesClient = postgresqlVNRClient + // SQL Azure sqlDBClient := sql.NewDatabasesClientWithBaseURI(endpoint, subscriptionId) c.configureClient(&sqlDBClient.Client, auth) diff --git a/azurerm/import_arm_postgresql_virtual_network_rule_test.go b/azurerm/import_arm_postgresql_virtual_network_rule_test.go new file mode 100644 index 000000000000..ed5232b296a6 --- /dev/null +++ b/azurerm/import_arm_postgresql_virtual_network_rule_test.go @@ -0,0 +1,31 @@ +package azurerm + +import ( + "testing" + + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" +) + +func TestAccAzureRMPostgreSQLVirtualNetworkRule_importBasic(t *testing.T) { + resourceName := "azurerm_postgresql_virtual_network_rule.test" + + ri := acctest.RandInt() + config := testAccAzureRMPostgreSQLVirtualNetworkRule_basic(ri, testLocation()) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMPostgreSQLVirtualNetworkRuleDestroy, + Steps: []resource.TestStep{ + { + Config: config, + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} diff --git a/azurerm/provider.go b/azurerm/provider.go index bea22be40e88..984ab99f4fa7 100644 --- a/azurerm/provider.go +++ b/azurerm/provider.go @@ -216,6 +216,7 @@ func Provider() terraform.ResourceProvider { "azurerm_postgresql_database": resourceArmPostgreSQLDatabase(), "azurerm_postgresql_firewall_rule": resourceArmPostgreSQLFirewallRule(), "azurerm_postgresql_server": resourceArmPostgreSQLServer(), + "azurerm_postgresql_virtual_network_rule": resourceArmPostgreSQLVirtualNetworkRule(), "azurerm_public_ip": resourceArmPublicIp(), "azurerm_relay_namespace": resourceArmRelayNamespace(), "azurerm_recovery_services_vault": resourceArmRecoveryServicesVault(), diff --git a/azurerm/resource_arm_postgresql_virtual_network_rule.go b/azurerm/resource_arm_postgresql_virtual_network_rule.go new file mode 100644 index 000000000000..c0d0ecc3fa96 --- /dev/null +++ b/azurerm/resource_arm_postgresql_virtual_network_rule.go @@ -0,0 +1,284 @@ +package azurerm + +import ( + "context" + "fmt" + "log" + "regexp" + "strings" + "time" + + "github.com/Azure/azure-sdk-for-go/services/postgresql/mgmt/2017-12-01/postgresql" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/helper/validation" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/azure" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/response" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" +) + +func resourceArmPostgreSQLVirtualNetworkRule() *schema.Resource { + return &schema.Resource{ + Create: resourceArmPostgreSQLVirtualNetworkRuleCreateUpdate, + Read: resourceArmPostgreSQLVirtualNetworkRuleRead, + Update: resourceArmPostgreSQLVirtualNetworkRuleCreateUpdate, + Delete: resourceArmPostgreSQLVirtualNetworkRuleDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validatePostgreSQLVirtualNetworkRuleName, + }, + + "resource_group_name": resourceGroupNameSchema(), + + "server_name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.NoZeroValues, + }, + + "subnet_id": { + Type: schema.TypeString, + Required: true, + ValidateFunc: azure.ValidateResourceID, + }, + }, + } +} + +func resourceArmPostgreSQLVirtualNetworkRuleCreateUpdate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*ArmClient).postgresqlVirtualNetworkRulesClient + ctx := meta.(*ArmClient).StopContext + + name := d.Get("name").(string) + serverName := d.Get("server_name").(string) + resourceGroup := d.Get("resource_group_name").(string) + subnetId := d.Get("subnet_id").(string) + + // due to a bug in the API we have to ensure the Subnet's configured correctly or the API call will timeout + // BUG: https://github.com/Azure/azure-rest-api-specs/issues/3719 + subnetsClient := meta.(*ArmClient).subnetClient + subnetParsedId, err := parseAzureResourceID(subnetId) + + subnetResourceGroup := subnetParsedId.ResourceGroup + virtualNetwork := subnetParsedId.Path["virtualNetworks"] + subnetName := subnetParsedId.Path["subnets"] + subnet, err := subnetsClient.Get(ctx, subnetResourceGroup, virtualNetwork, subnetName, "") + if err != nil { + if utils.ResponseWasNotFound(subnet.Response) { + return fmt.Errorf("Subnet with ID %q was not found: %+v", subnetId, err) + } + + return fmt.Errorf("Error obtaining Subnet %q (Virtual Network %q / Resource Group %q: %+v", subnetName, virtualNetwork, subnetResourceGroup, err) + } + + containsEndpoint := false + if props := subnet.SubnetPropertiesFormat; props != nil { + if endpoints := props.ServiceEndpoints; endpoints != nil { + for _, e := range *endpoints { + if e.Service == nil { + continue + } + + if strings.EqualFold(*e.Service, "Microsoft.Sql") { + containsEndpoint = true + break + } + } + } + } + + if !containsEndpoint { + return fmt.Errorf("Error creating PostgreSQL Virtual Network Rule: Subnet %q (Virtual Network %q / Resource Group %q) must contain a Service Endpoint for `Microsoft.Sql`", subnetName, virtualNetwork, subnetResourceGroup) + } + + parameters := postgresql.VirtualNetworkRule{ + VirtualNetworkRuleProperties: &postgresql.VirtualNetworkRuleProperties{ + VirtualNetworkSubnetID: utils.String(subnetId), + IgnoreMissingVnetServiceEndpoint: utils.Bool(false), + }, + } + + _, err = client.CreateOrUpdate(ctx, resourceGroup, serverName, name, parameters) + if err != nil { + return fmt.Errorf("Error creating PostgreSQL Virtual Network Rule %q (PostgreSQL Server: %q, Resource Group: %q): %+v", name, serverName, resourceGroup, err) + } + + //Wait for the provisioning state to become ready + log.Printf("[DEBUG] Waiting for PostgreSQL Virtual Network Rule %q (PostgreSQL Server: %q, Resource Group: %q) to become ready: %+v", name, serverName, resourceGroup, err) + stateConf := &resource.StateChangeConf{ + Pending: []string{"Initializing", "InProgress", "Unknown", "ResponseNotFound"}, + Target: []string{"Ready"}, + Refresh: postgreSQLVirtualNetworkStateStatusCodeRefreshFunc(ctx, client, resourceGroup, serverName, name), + Timeout: 10 * time.Minute, + MinTimeout: 1 * time.Minute, + ContinuousTargetOccurence: 5, + } + + if _, err := stateConf.WaitForState(); err != nil { + return fmt.Errorf("Error waiting for PostgreSQL Virtual Network Rule %q (PostgreSQL Server: %q, Resource Group: %q) to be created or updated: %+v", name, serverName, resourceGroup, err) + } + + resp, err := client.Get(ctx, resourceGroup, serverName, name) + if err != nil { + return fmt.Errorf("Error retrieving PostgreSQL Virtual Network Rule %q (PostgreSQL Server: %q, Resource Group: %q): %+v", name, serverName, resourceGroup, err) + } + + d.SetId(*resp.ID) + + return resourceArmPostgreSQLVirtualNetworkRuleRead(d, meta) +} + +func resourceArmPostgreSQLVirtualNetworkRuleRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*ArmClient).postgresqlVirtualNetworkRulesClient + ctx := meta.(*ArmClient).StopContext + + id, err := parseAzureResourceID(d.Id()) + if err != nil { + return err + } + + resourceGroup := id.ResourceGroup + serverName := id.Path["servers"] + name := id.Path["virtualNetworkRules"] + + resp, err := client.Get(ctx, resourceGroup, serverName, name) + if err != nil { + if utils.ResponseWasNotFound(resp.Response) { + log.Printf("[INFO] Error reading PostgreSQL Virtual Network Rule %q - removing from state", d.Id()) + d.SetId("") + return nil + } + + return fmt.Errorf("Error reading PostgreSQL Virtual Network Rule: %q (PostgreSQL Server: %q, Resource Group: %q): %+v", name, serverName, resourceGroup, err) + } + + d.Set("name", resp.Name) + d.Set("resource_group_name", resourceGroup) + d.Set("server_name", serverName) + + if props := resp.VirtualNetworkRuleProperties; props != nil { + d.Set("subnet_id", props.VirtualNetworkSubnetID) + } + + return nil +} + +func resourceArmPostgreSQLVirtualNetworkRuleDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*ArmClient).postgresqlVirtualNetworkRulesClient + ctx := meta.(*ArmClient).StopContext + + id, err := parseAzureResourceID(d.Id()) + if err != nil { + return err + } + + resourceGroup := id.ResourceGroup + serverName := id.Path["servers"] + name := id.Path["virtualNetworkRules"] + + future, err := client.Delete(ctx, resourceGroup, serverName, name) + if err != nil { + if response.WasNotFound(future.Response()) { + return nil + } + + return fmt.Errorf("Error deleting PostgreSQL Virtual Network Rule %q (PostgreSQL Server: %q, Resource Group: %q): %+v", name, serverName, resourceGroup, err) + } + + err = future.WaitForCompletionRef(ctx, client.Client) + if err != nil { + if response.WasNotFound(future.Response()) { + return nil + } + + return fmt.Errorf("Error deleting PostgreSQL Virtual Network Rule %q (PostgreSQL Server: %q, Resource Group: %q): %+v", name, serverName, resourceGroup, err) + } + + return nil +} + +/* + This function checks the format of the PostgreSQL Virtual Network Rule Name to make sure that + it does not contain any potentially invalid values. +*/ +func validatePostgreSQLVirtualNetworkRuleName(v interface{}, k string) (ws []string, errors []error) { + value := v.(string) + + // Cannot be empty + if len(value) == 0 { + errors = append(errors, fmt.Errorf( + "%q cannot be an empty string: %q", k, value)) + } + + // Cannot be more than 128 characters + if len(value) > 128 { + errors = append(errors, fmt.Errorf( + "%q cannot be longer than 128 characters: %q", k, value)) + } + + // Must only contain alphanumeric characters or hyphens + if !regexp.MustCompile(`^[A-Za-z0-9-]*$`).MatchString(value) { + errors = append(errors, fmt.Errorf( + "%q can only contain alphanumeric characters and hyphens: %q", + k, value)) + } + + // Cannot end in a hyphen + if regexp.MustCompile(`-$`).MatchString(value) { + errors = append(errors, fmt.Errorf( + "%q cannot end with a hyphen: %q", k, value)) + } + + // Cannot start with a number or hyphen + if regexp.MustCompile(`^[0-9-]`).MatchString(value) { + errors = append(errors, fmt.Errorf( + "%q cannot start with a number or hyphen: %q", k, value)) + } + + // There are multiple returns in the case that there is more than one invalid + // case applied to the name. + return +} + +/* + This function refreshes and checks the state of the PostgreSQL Virtual Network Rule. + + Response will contain a VirtualNetworkRuleProperties struct with a State property. The state property contain one of the following states (except ResponseNotFound). + * Deleting + * Initializing + * InProgress + * Unknown + * Ready + * ResponseNotFound (Custom state in case of 404) +*/ +func postgreSQLVirtualNetworkStateStatusCodeRefreshFunc(ctx context.Context, client postgresql.VirtualNetworkRulesClient, resourceGroup string, serverName string, name string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + resp, err := client.Get(ctx, resourceGroup, serverName, name) + + if err != nil { + if utils.ResponseWasNotFound(resp.Response) { + log.Printf("[DEBUG] Retrieving PostgreSQL Virtual Network Rule %q (PostgreSQL Server: %q, Resource Group: %q) returned 404.", resourceGroup, serverName, name) + return nil, "ResponseNotFound", nil + } + + return nil, "", fmt.Errorf("Error polling for the state of the PostgreSQL Virtual Network Rule %q (PostgreSQL Server: %q, Resource Group: %q): %+v", name, serverName, resourceGroup, err) + } + + if props := resp.VirtualNetworkRuleProperties; props != nil { + log.Printf("[DEBUG] Retrieving PostgreSQL Virtual Network Rule %q (PostgreSQL Server: %q, Resource Group: %q) returned Status %s", resourceGroup, serverName, name, props.State) + return resp, fmt.Sprintf("%s", props.State), nil + } + + //Valid response was returned but VirtualNetworkRuleProperties was nil. Basically the rule exists, but with no properties for some reason. Assume Unknown instead of returning error. + log.Printf("[DEBUG] Retrieving PostgreSQL Virtual Network Rule %q (PostgreSQL Server: %q, Resource Group: %q) returned empty VirtualNetworkRuleProperties", resourceGroup, serverName, name) + return resp, "Unknown", nil + } +} diff --git a/azurerm/resource_arm_postgresql_virtual_network_rule_test.go b/azurerm/resource_arm_postgresql_virtual_network_rule_test.go new file mode 100644 index 000000000000..c050eed97d29 --- /dev/null +++ b/azurerm/resource_arm_postgresql_virtual_network_rule_test.go @@ -0,0 +1,617 @@ +package azurerm + +import ( + "fmt" + "regexp" + "testing" + + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/response" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" +) + +/* + ---Testing for Success--- + Test a basic PostgreSQL virtual network rule configuration setup scenario. +*/ +func TestAccAzureRMPostgreSQLVirtualNetworkRule_basic(t *testing.T) { + resourceName := "azurerm_postgresql_virtual_network_rule.test" + ri := acctest.RandInt() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMPostgreSQLVirtualNetworkRuleDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMPostgreSQLVirtualNetworkRule_basic(ri, testLocation()), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMPostgreSQLVirtualNetworkRuleExists(resourceName), + ), + }, + }, + }) +} + +/* + ---Testing for Success--- + Test an update to the PostgreSQL Virtual Network Rule to connect to a different subnet, and + validate that new subnet is set correctly. +*/ +func TestAccAzureRMPostgreSQLVirtualNetworkRule_switchSubnets(t *testing.T) { + resourceName := "azurerm_postgresql_virtual_network_rule.test" + ri := acctest.RandInt() + + preConfig := testAccAzureRMPostgreSQLVirtualNetworkRule_subnetSwitchPre(ri, testLocation()) + postConfig := testAccAzureRMPostgreSQLVirtualNetworkRule_subnetSwitchPost(ri, testLocation()) + + // Create regex strings that will ensure that one subnet name exists, but not the other + preConfigRegex := regexp.MustCompile(fmt.Sprintf("(subnet1%d)$|(subnet[^2]%d)$", ri, ri)) //subnet 1 but not 2 + postConfigRegex := regexp.MustCompile(fmt.Sprintf("(subnet2%d)$|(subnet[^1]%d)$", ri, ri)) //subnet 2 but not 1 + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMPostgreSQLVirtualNetworkRuleDestroy, + Steps: []resource.TestStep{ + { + Config: preConfig, + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMPostgreSQLVirtualNetworkRuleExists(resourceName), + resource.TestMatchResourceAttr(resourceName, "subnet_id", preConfigRegex), + ), + }, + { + Config: postConfig, + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMPostgreSQLVirtualNetworkRuleExists(resourceName), + resource.TestMatchResourceAttr(resourceName, "subnet_id", postConfigRegex), + ), + }, + }, + }) +} + +/* + ---Testing for Success--- +*/ +func TestAccAzureRMPostgreSQLVirtualNetworkRule_disappears(t *testing.T) { + resourceName := "azurerm_postgresql_virtual_network_rule.test" + ri := acctest.RandInt() + config := testAccAzureRMPostgreSQLVirtualNetworkRule_basic(ri, testLocation()) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMPostgreSQLVirtualNetworkRuleDestroy, + Steps: []resource.TestStep{ + { + Config: config, + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMPostgreSQLVirtualNetworkRuleExists(resourceName), + testCheckAzureRMPostgreSQLVirtualNetworkRuleDisappears(resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +/* + --Testing for Success-- + Test if we are able to create multiple subnets and connect multiple subnets to the + PostgreSQL server. +*/ +func TestAccAzureRMPostgreSQLVirtualNetworkRule_multipleSubnets(t *testing.T) { + resourceName1 := "azurerm_postgresql_virtual_network_rule.rule1" + resourceName2 := "azurerm_postgresql_virtual_network_rule.rule2" + resourceName3 := "azurerm_postgresql_virtual_network_rule.rule3" + ri := acctest.RandInt() + config := testAccAzureRMPostgreSQLVirtualNetworkRule_multipleSubnets(ri, testLocation()) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMPostgreSQLVirtualNetworkRuleDestroy, + Steps: []resource.TestStep{ + { + Config: config, + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMPostgreSQLVirtualNetworkRuleExists(resourceName1), + testCheckAzureRMPostgreSQLVirtualNetworkRuleExists(resourceName2), + testCheckAzureRMPostgreSQLVirtualNetworkRuleExists(resourceName3), + ), + }, + }, + }) +} + +/* + --Testing for Failure-- + Validation Function Tests - Invalid Name Validations +*/ +func TestResourceAzureRMPostgreSQLVirtualNetworkRule_invalidNameValidation(t *testing.T) { + cases := []struct { + Value string + ErrCount int + }{ + // Must only contain alphanumeric characters or hyphens (4 cases) + { + Value: "test!Rule", + ErrCount: 1, + }, + { + Value: "test_Rule", + ErrCount: 1, + }, + { + Value: "test:Rule", + ErrCount: 1, + }, + { + Value: "test'Rule", + ErrCount: 1, + }, + // Cannot be more than 128 characters (1 case - ensure starts with a letter) + { + Value: fmt.Sprintf("v%s", acctest.RandString(128)), + ErrCount: 1, + }, + // Cannot be empty (1 case) + { + Value: "", + ErrCount: 1, + }, + // Cannot end in a hyphen (1 case) + { + Value: "testRule-", + ErrCount: 1, + }, + // Cannot start with a number or hyphen (2 cases) + { + Value: "7testRule", + ErrCount: 1, + }, + { + Value: "-testRule", + ErrCount: 1, + }, + } + + for _, tc := range cases { + _, errors := validatePostgreSQLVirtualNetworkRuleName(tc.Value, "azurerm_postgresql_virtual_network_rule") + + if len(errors) != tc.ErrCount { + t.Fatalf("Bad: Expected the Azure RM PostgreSQL Virtual Network Rule Name to trigger a validation error.") + } + } +} + +/* + --Testing for Success-- + Validation Function Tests - (Barely) Valid Name Validations +*/ +func TestResourceAzureRMPostgreSQLVirtualNetworkRule_validNameValidation(t *testing.T) { + cases := []struct { + Value string + ErrCount int + }{ + // Test all lowercase + { + Value: "thisisarule", + ErrCount: 0, + }, + // Test all uppercase + { + Value: "THISISARULE", + ErrCount: 0, + }, + // Test alternating cases + { + Value: "tHiSiSaRuLe", + ErrCount: 0, + }, + // Test hyphens + { + Value: "this-is-a-rule", + ErrCount: 0, + }, + // Test multiple hyphens in a row + { + Value: "this----1s----a----ru1e", + ErrCount: 0, + }, + // Test numbers (except for first character) + { + Value: "v1108501298509850810258091285091820-5", + ErrCount: 0, + }, + // Test a lot of hyphens and numbers + { + Value: "x-5-4-1-2-5-2-6-1-5-2-5-1-2-5-6-2-2", + ErrCount: 0, + }, + // Test exactly 128 characters + { + Value: fmt.Sprintf("v%s", acctest.RandString(127)), + ErrCount: 0, + }, + // Test short, 1-letter name + { + Value: "V", + ErrCount: 0, + }, + } + + for _, tc := range cases { + _, errors := validatePostgreSQLVirtualNetworkRuleName(tc.Value, "azurerm_postgresql_virtual_network_rule") + + if len(errors) != tc.ErrCount { + t.Fatalf("Bad: Expected the Azure RM PostgreSQL Virtual Network Rule Name pass name validation successfully but triggered a validation error.") + } + } +} + +/* + Test Check function to assert if a rule exists or not. +*/ +func testCheckAzureRMPostgreSQLVirtualNetworkRuleExists(name string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + if !ok { + return fmt.Errorf("Not found: %s", name) + } + + resourceGroup := rs.Primary.Attributes["resource_group_name"] + serverName := rs.Primary.Attributes["server_name"] + ruleName := rs.Primary.Attributes["name"] + + client := testAccProvider.Meta().(*ArmClient).postgresqlVirtualNetworkRulesClient + ctx := testAccProvider.Meta().(*ArmClient).StopContext + + resp, err := client.Get(ctx, resourceGroup, serverName, ruleName) + if err != nil { + if utils.ResponseWasNotFound(resp.Response) { + return fmt.Errorf("Bad: PostgreSQL Firewall Rule %q (server %q / resource group %q) was not found", ruleName, serverName, resourceGroup) + } + + return err + } + + return nil + } +} + +/* + Test Check function to delete a rule. +*/ +func testCheckAzureRMPostgreSQLVirtualNetworkRuleDestroy(s *terraform.State) error { + for _, rs := range s.RootModule().Resources { + if rs.Type != "azurerm_postgresql_virtual_network_rule" { + continue + } + + resourceGroup := rs.Primary.Attributes["resource_group_name"] + serverName := rs.Primary.Attributes["server_name"] + ruleName := rs.Primary.Attributes["name"] + + client := testAccProvider.Meta().(*ArmClient).postgresqlVirtualNetworkRulesClient + ctx := testAccProvider.Meta().(*ArmClient).StopContext + + resp, err := client.Get(ctx, resourceGroup, serverName, ruleName) + if err != nil { + if utils.ResponseWasNotFound(resp.Response) { + return nil + } + + return err + } + + return fmt.Errorf("Bad: PostgreSQL Firewall Rule %q (server %q / resource group %q) still exists: %+v", ruleName, serverName, resourceGroup, resp) + } + + return nil +} + +/* + Test Check function to assert if that a rule gets deleted. +*/ +func testCheckAzureRMPostgreSQLVirtualNetworkRuleDisappears(name string) resource.TestCheckFunc { + return func(s *terraform.State) error { + // Ensure we have enough information in state to look up in API + rs, ok := s.RootModule().Resources[name] + if !ok { + return fmt.Errorf("Not found: %s", name) + } + + resourceGroup := rs.Primary.Attributes["resource_group_name"] + serverName := rs.Primary.Attributes["server_name"] + ruleName := rs.Primary.Attributes["name"] + + client := testAccProvider.Meta().(*ArmClient).postgresqlVirtualNetworkRulesClient + ctx := testAccProvider.Meta().(*ArmClient).StopContext + + future, err := client.Delete(ctx, resourceGroup, serverName, ruleName) + if err != nil { + //If the error is that the resource we want to delete does not exist in the first + //place (404), then just return with no error. + if response.WasNotFound(future.Response()) { + return nil + } + + return fmt.Errorf("Error deleting PostgreSQL Virtual Network Rule: %+v", err) + } + + err = future.WaitForCompletionRef(ctx, client.Client) + if err != nil { + //Same deal as before. Just in case. + if response.WasNotFound(future.Response()) { + return nil + } + + return fmt.Errorf("Error deleting PostgreSQL Virtual Network Rule: %+v", err) + } + + return nil + } +} + +/* + (This test configuration is intended to succeed.) + Basic Provisioning Configuration +*/ +func testAccAzureRMPostgreSQLVirtualNetworkRule_basic(rInt int, location string) string { + return fmt.Sprintf(` +resource "azurerm_resource_group" "test" { + name = "acctestRG-%d" + location = "%s" +} +resource "azurerm_virtual_network" "test" { + name = "acctestvnet%d" + address_space = ["10.7.29.0/29"] + location = "${azurerm_resource_group.test.location}" + resource_group_name = "${azurerm_resource_group.test.name}" +} +resource "azurerm_subnet" "test" { + name = "acctestsubnet%d" + resource_group_name = "${azurerm_resource_group.test.name}" + virtual_network_name = "${azurerm_virtual_network.test.name}" + address_prefix = "10.7.29.0/29" + service_endpoints = ["Microsoft.Sql"] +} +resource "azurerm_postgresql_server" "test" { + name = "acctestpostgresqlsvr-%d" + location = "${azurerm_resource_group.test.location}" + resource_group_name = "${azurerm_resource_group.test.name}" + sku { + name = "GP_Gen5_2" + capacity = 2 + tier = "GeneralPurpose" + family = "Gen5" + } + storage_profile { + storage_mb = 51200 + backup_retention_days = 7 + geo_redundant_backup = "Disabled" + } + administrator_login = "acctestun" + administrator_login_password = "H@Sh1CoR3!" + version = "9.5" + ssl_enforcement = "Enabled" +} +resource "azurerm_postgresql_virtual_network_rule" "test" { + name = "acctestpostgresqlvnetrule%d" + resource_group_name = "${azurerm_resource_group.test.name}" + server_name = "${azurerm_postgresql_server.test.name}" + subnet_id = "${azurerm_subnet.test.id}" +} +`, rInt, location, rInt, rInt, rInt, rInt) +} + +/* + (This test configuration is intended to succeed.) + This test is designed to set up a scenario where a user would want to update the subnet + on a given PostgreSQL virtual network rule. This configuration sets up the resources initially. +*/ +func testAccAzureRMPostgreSQLVirtualNetworkRule_subnetSwitchPre(rInt int, location string) string { + return fmt.Sprintf(` +resource "azurerm_resource_group" "test" { + name = "acctestRG-%d" + location = "%s" +} +resource "azurerm_virtual_network" "test" { + name = "acctestvnet%d" + address_space = ["10.7.29.0/24"] + location = "${azurerm_resource_group.test.location}" + resource_group_name = "${azurerm_resource_group.test.name}" +} +resource "azurerm_subnet" "test1" { + name = "subnet1%d" + resource_group_name = "${azurerm_resource_group.test.name}" + virtual_network_name = "${azurerm_virtual_network.test.name}" + address_prefix = "10.7.29.0/25" + service_endpoints = ["Microsoft.Sql"] +} +resource "azurerm_subnet" "test2" { + name = "subnet2%d" + resource_group_name = "${azurerm_resource_group.test.name}" + virtual_network_name = "${azurerm_virtual_network.test.name}" + address_prefix = "10.7.29.128/25" + service_endpoints = ["Microsoft.Sql"] +} +resource "azurerm_postgresql_server" "test" { + name = "acctestpostgresqlsvr-%d" + location = "${azurerm_resource_group.test.location}" + resource_group_name = "${azurerm_resource_group.test.name}" + sku { + name = "GP_Gen5_2" + capacity = 2 + tier = "GeneralPurpose" + family = "Gen5" + } + storage_profile { + storage_mb = 51200 + backup_retention_days = 7 + geo_redundant_backup = "Disabled" + } + administrator_login = "acctestun" + administrator_login_password = "H@Sh1CoR3!" + version = "9.5" + ssl_enforcement = "Enabled" +} +resource "azurerm_postgresql_virtual_network_rule" "test" { + name = "acctestpostgresqlvnetrule%d" + resource_group_name = "${azurerm_resource_group.test.name}" + server_name = "${azurerm_postgresql_server.test.name}" + subnet_id = "${azurerm_subnet.test1.id}" +} +`, rInt, location, rInt, rInt, rInt, rInt, rInt) +} + +/* + (This test configuration is intended to succeed.) + This test is designed to set up a scenario where a user would want to update the subnet + on a given PostgreSQL virtual network rule. This configuration contains the update from + azurerm_subnet.test1 to azurerm_subnet.test2. +*/ +func testAccAzureRMPostgreSQLVirtualNetworkRule_subnetSwitchPost(rInt int, location string) string { + return fmt.Sprintf(` +resource "azurerm_resource_group" "test" { + name = "acctestRG-%d" + location = "%s" +} +resource "azurerm_virtual_network" "test" { + name = "acctestvnet%d" + address_space = ["10.7.29.0/24"] + location = "${azurerm_resource_group.test.location}" + resource_group_name = "${azurerm_resource_group.test.name}" +} +resource "azurerm_subnet" "test1" { + name = "subnet1%d" + resource_group_name = "${azurerm_resource_group.test.name}" + virtual_network_name = "${azurerm_virtual_network.test.name}" + address_prefix = "10.7.29.0/25" + service_endpoints = ["Microsoft.Sql"] +} +resource "azurerm_subnet" "test2" { + name = "subnet2%d" + resource_group_name = "${azurerm_resource_group.test.name}" + virtual_network_name = "${azurerm_virtual_network.test.name}" + address_prefix = "10.7.29.128/25" + service_endpoints = ["Microsoft.Sql"] +} +resource "azurerm_postgresql_server" "test" { + name = "acctestpostgresqlsvr-%d" + location = "${azurerm_resource_group.test.location}" + resource_group_name = "${azurerm_resource_group.test.name}" + sku { + name = "GP_Gen5_2" + capacity = 2 + tier = "GeneralPurpose" + family = "Gen5" + } + storage_profile { + storage_mb = 51200 + backup_retention_days = 7 + geo_redundant_backup = "Disabled" + } + administrator_login = "acctestun" + administrator_login_password = "H@Sh1CoR3!" + version = "9.5" + ssl_enforcement = "Enabled" +} +resource "azurerm_postgresql_virtual_network_rule" "test" { + name = "acctestpostgresqlvnetrule%d" + resource_group_name = "${azurerm_resource_group.test.name}" + server_name = "${azurerm_postgresql_server.test.name}" + subnet_id = "${azurerm_subnet.test2.id}" +} +`, rInt, location, rInt, rInt, rInt, rInt, rInt) +} + +/* + (This test configuration is intended to succeed.) + This configuration sets up 3 subnets in 2 different virtual networks, and adds + PostgreSQL virtual network rules for all 3 subnets to the SQL server. +*/ +func testAccAzureRMPostgreSQLVirtualNetworkRule_multipleSubnets(rInt int, location string) string { + return fmt.Sprintf(` +resource "azurerm_resource_group" "test" { + name = "acctestRG-%d" + location = "%s" +} +resource "azurerm_virtual_network" "vnet1" { + name = "acctestvnet1%d" + address_space = ["10.7.29.0/24"] + location = "${azurerm_resource_group.test.location}" + resource_group_name = "${azurerm_resource_group.test.name}" +} +resource "azurerm_virtual_network" "vnet2" { + name = "acctestvnet2%d" + address_space = ["10.1.29.0/29"] + location = "${azurerm_resource_group.test.location}" + resource_group_name = "${azurerm_resource_group.test.name}" +} +resource "azurerm_subnet" "vnet1_subnet1" { + name = "acctestsubnet1%d" + resource_group_name = "${azurerm_resource_group.test.name}" + virtual_network_name = "${azurerm_virtual_network.vnet1.name}" + address_prefix = "10.7.29.0/29" + service_endpoints = ["Microsoft.Sql"] +} +resource "azurerm_subnet" "vnet1_subnet2" { + name = "acctestsubnet2%d" + resource_group_name = "${azurerm_resource_group.test.name}" + virtual_network_name = "${azurerm_virtual_network.vnet1.name}" + address_prefix = "10.7.29.128/29" + service_endpoints = ["Microsoft.Sql"] +} +resource "azurerm_subnet" "vnet2_subnet1" { + name = "acctestsubnet3%d" + resource_group_name = "${azurerm_resource_group.test.name}" + virtual_network_name = "${azurerm_virtual_network.vnet2.name}" + address_prefix = "10.1.29.0/29" + service_endpoints = ["Microsoft.Sql"] +} +resource "azurerm_postgresql_server" "test" { + name = "acctestpostgresqlsvr-%d" + location = "${azurerm_resource_group.test.location}" + resource_group_name = "${azurerm_resource_group.test.name}" + sku { + name = "GP_Gen5_2" + capacity = 2 + tier = "GeneralPurpose" + family = "Gen5" + } + storage_profile { + storage_mb = 51200 + backup_retention_days = 7 + geo_redundant_backup = "Disabled" + } + administrator_login = "acctestun" + administrator_login_password = "H@Sh1CoR3!" + version = "9.5" + ssl_enforcement = "Enabled" +} +resource "azurerm_postgresql_virtual_network_rule" "rule1" { + name = "acctestpostgresqlvnetrule1%d" + resource_group_name = "${azurerm_resource_group.test.name}" + server_name = "${azurerm_postgresql_server.test.name}" + subnet_id = "${azurerm_subnet.vnet1_subnet1.id}" +} +resource "azurerm_postgresql_virtual_network_rule" "rule2" { + name = "acctestpostgresqlvnetrule2%d" + resource_group_name = "${azurerm_resource_group.test.name}" + server_name = "${azurerm_postgresql_server.test.name}" + subnet_id = "${azurerm_subnet.vnet1_subnet2.id}" +} +resource "azurerm_postgresql_virtual_network_rule" "rule3" { + name = "acctestpostgresqlvnetrule3%d" + resource_group_name = "${azurerm_resource_group.test.name}" + server_name = "${azurerm_postgresql_server.test.name}" + subnet_id = "${azurerm_subnet.vnet2_subnet1.id}" +} +`, rInt, location, rInt, rInt, rInt, rInt, rInt, rInt, rInt, rInt, rInt) +} diff --git a/website/azurerm.erb b/website/azurerm.erb index 743ea0f024d2..ce469aaad84a 100644 --- a/website/azurerm.erb +++ b/website/azurerm.erb @@ -416,6 +416,10 @@ azurerm_postgresql_server + > + azurerm_postgresql_virtual_network_rule + + > azurerm_sql_database diff --git a/website/docs/r/postgresql_virtual_network_rule.html.markdown b/website/docs/r/postgresql_virtual_network_rule.html.markdown new file mode 100644 index 000000000000..18147e37d4e6 --- /dev/null +++ b/website/docs/r/postgresql_virtual_network_rule.html.markdown @@ -0,0 +1,99 @@ +--- +layout: "azurerm" +page_title: "Azure Resource Manager: azurerm_postgresql_virtual_network_rule" +sidebar_current: "docs-azurerm-resource-database-sql-virtual-network-rule" +description: |- + Manages a PostgreSQL Virtual Network Rule. +--- + +# azurerm_postgresql_virtual_network_rule + +Allows you to add, update, or remove an Azure PostgreSQL server to a subnet of a virtual network. + +## Example Usage + +```hcl +resource "azurerm_resource_group" "example" { + name = "example-postgresql-server-vnet-rule" + location = "West US" +} + +resource "azurerm_virtual_network" "vnet" { + name = "example-vnet" + address_space = ["10.7.29.0/29"] + location = "${azurerm_resource_group.example.location}" + resource_group_name = "${azurerm_resource_group.example.name}" +} + +resource "azurerm_subnet" "subnet" { + name = "example-subnet" + resource_group_name = "${azurerm_resource_group.example.name}" + virtual_network_name = "${azurerm_virtual_network.vnet.name}" + address_prefix = "10.7.29.0/29" + service_endpoints = ["Microsoft.Sql"] +} + +resource "azurerm_postgresql_server" "postgresql_server" { + name = "postgresql-server-1" + location = "${azurerm_resource_group.test.location}" + resource_group_name = "${azurerm_resource_group.test.name}" + + sku { + name = "B_Gen4_2" + capacity = 2 + tier = "Basic" + family = "Gen4" + } + + storage_profile { + storage_mb = 5120 + backup_retention_days = 7 + geo_redundant_backup = "Disabled" + } + + administrator_login = "psqladminun" + administrator_login_password = "H@Sh1CoR3!" + version = "9.5" + ssl_enforcement = "Enabled" +} + +resource "azurerm_postgresql_virtual_network_rule" "postgresqlsql_virtual_network_rule" { + name = "postgresql-vnet-rule" + resource_group_name = "${azurerm_resource_group.example.name}" + server_name = "${azurerm_postgresql_server.postgresql_server.name}" + subnet_id = "${azurerm_subnet.subnet.id}" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) The name of the SQL virtual network rule. Cannot be empty and must only contain alphanumeric characters and hyphens. Cannot start with a number, and cannot start or end with a hyphen. Changing this forces a new resource to be created. + +~> **NOTE:** `name` must be between 1-128 characters long and must satisfy all of the requirements below: +1. Contains only alphanumeric and hyphen characters +2. Cannot start with a number or hyphen +3. Cannot end with a hyphen + +* `resource_group_name` - (Required) The name of the resource group where the PostgreSQL server resides. Changing this forces a new resource to be created. + +* `server_name` - (Required) The name of the SQL Server to which this PostgreSQL virtual network rule will be applied to. Changing this forces a new resource to be created. + +* `subnet_id` - (Required) The ID of the subnet that the PostgreSQL server will be connected to. + +~> **NOTE:** Due to [a bug in the Azure API](https://github.com/Azure/azure-rest-api-specs/issues/3719) this resource currently doesn't expose the `ignore_missing_vnet_service_endpoint` field and defaults this to `false`. Terraform will check during the provisioning of the Virtual Network Rule that the Subnet contains the Service Rule to verify that the Virtual Network Rule can be created. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The ID of the PostgreSQL Virtual Network Rule. + +## Import + +PostgreSQL Virtual Network Rules can be imported using the `resource id`, e.g. + +```shell +terraform import azurerm_postgresql_virtual_network_rule.rule1 /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/myresourcegroup/providers/Microsoft.DBforPostgreSQL/servers/myserver/virtualNetworkRules/vnetrulename +```