diff --git a/azurerm/internal/services/servicebus/parse/namespace.go b/azurerm/internal/services/servicebus/parse/namespace.go new file mode 100644 index 0000000000000..e8cdeccce4523 --- /dev/null +++ b/azurerm/internal/services/servicebus/parse/namespace.go @@ -0,0 +1,33 @@ +package parse + +import ( + "fmt" + + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/azure" +) + +type ServiceBusNamespaceId struct { + Name string + ResourceGroup string +} + +func ServiceBusNamespaceID(input string) (*ServiceBusNamespaceId, error) { + id, err := azure.ParseAzureResourceID(input) + if err != nil { + return nil, fmt.Errorf("unable to parse Service Bus Namespace ID %q: %+v", input, err) + } + + namespace := ServiceBusNamespaceId{ + ResourceGroup: id.ResourceGroup, + } + + if namespace.Name, err = id.PopSegment("namespaces"); err != nil { + return nil, fmt.Errorf("unable to parse Service Bus Namespace ID %q: %+v", input, err) + } + + if err := id.ValidateNoEmptySegments(input); err != nil { + return nil, fmt.Errorf("unable to parse Service Bus Namespace ID %q: %+v", input, err) + } + + return &namespace, nil +} diff --git a/azurerm/internal/services/servicebus/parse/namespace_network_rule.go b/azurerm/internal/services/servicebus/parse/namespace_network_rule.go new file mode 100644 index 0000000000000..7a42aafec960a --- /dev/null +++ b/azurerm/internal/services/servicebus/parse/namespace_network_rule.go @@ -0,0 +1,38 @@ +package parse + +import ( + "fmt" + + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/azure" +) + +type ServiceBusNamespaceNetworkRuleId struct { + Name string + NamespaceName string + ResourceGroup string +} + +func ServiceBusNamespaceNetworkRuleID(input string) (*ServiceBusNamespaceNetworkRuleId, error) { + id, err := azure.ParseAzureResourceID(input) + if err != nil { + return nil, fmt.Errorf("unable to parse Service Bus Namespace Network Rule ID %q: %+v", input, err) + } + + rule := ServiceBusNamespaceNetworkRuleId{ + ResourceGroup: id.ResourceGroup, + } + + if rule.Name, err = id.PopSegment("networkrulesets"); err != nil { + return nil, fmt.Errorf("unable to parse Service Bus Namespace Network Rule ID %q: %+v", input, err) + } + + if rule.NamespaceName, err = id.PopSegment("namespaces"); err != nil { + return nil, fmt.Errorf("unable to parse Service Bus Namespace Network Rule ID %q: %+v", input, err) + } + + if err := id.ValidateNoEmptySegments(input); err != nil { + return nil, fmt.Errorf("unable to parse Service Bus Namespace Network Rule ID %q: %+v", input, err) + } + + return &rule, nil +} diff --git a/azurerm/internal/services/servicebus/parse/namespace_network_rule_test.go b/azurerm/internal/services/servicebus/parse/namespace_network_rule_test.go new file mode 100644 index 0000000000000..853513eebc600 --- /dev/null +++ b/azurerm/internal/services/servicebus/parse/namespace_network_rule_test.go @@ -0,0 +1,87 @@ +package parse + +import "testing" + +func TestServiceBusNamespaceNetworkRuleID(t *testing.T) { + testData := []struct { + Name string + Input string + Error bool + Expected *ServiceBusNamespaceNetworkRuleId + }{ + { + Name: "Empty", + Input: "", + Error: true, + }, + { + Name: "No Resource Groups Segment", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000", + Error: true, + }, + { + Name: "No Resource Groups Value", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/", + Error: true, + }, + { + Name: "Resource Group ID", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/foo/", + Error: true, + }, + { + Name: "Missing Service Bus Namespace Name", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resGroup1/providers/Microsoft.ServiceBus/namespaces/", + Error: true, + }, + { + Name: "Service Bus Namespace ID", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resGroup1/providers/Microsoft.ServiceBus/namespaces/namespace1", + Error: true, + }, + { + Name: "Missing Network Rule Name", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resGroup1/providers/Microsoft.ServiceBus/namespaces/namespace1/networkrulesets/", + Error: true, + }, + { + Name: "Service Bus Namespace Network Rule ID", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resGroup1/providers/Microsoft.ServiceBus/namespaces/namespace1/networkrulesets/default", + Expected: &ServiceBusNamespaceNetworkRuleId{ + Name: "default", + NamespaceName: "namespace1", + ResourceGroup: "resGroup1", + }, + }, + { + Name: "Wrong casing", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resGroup1/providers/Microsoft.ServiceBus/Namespaces/namespace1", + Error: true, + }, + } + + for _, v := range testData { + t.Logf("[DEBUG] Testing %q", v.Name) + + actual, err := ServiceBusNamespaceNetworkRuleID(v.Input) + if err != nil { + if v.Error { + continue + } + + t.Fatalf("Expected a value but got an error: %s", err) + } + + if actual.Name != v.Expected.Name { + t.Fatalf("Expected %q but got %q for Name", v.Expected.Name, actual.Name) + } + + if actual.NamespaceName != v.Expected.NamespaceName { + t.Fatalf("Expected %q but got %q for Name", v.Expected.NamespaceName, actual.NamespaceName) + } + + if actual.ResourceGroup != v.Expected.ResourceGroup { + t.Fatalf("Expected %q but got %q for Resource Group", v.Expected.ResourceGroup, actual.ResourceGroup) + } + } +} diff --git a/azurerm/internal/services/servicebus/parse/namespace_test.go b/azurerm/internal/services/servicebus/parse/namespace_test.go new file mode 100644 index 0000000000000..98d154e8767d6 --- /dev/null +++ b/azurerm/internal/services/servicebus/parse/namespace_test.go @@ -0,0 +1,74 @@ +package parse + +import ( + "testing" +) + +func TestServiceBusNamespaceID(t *testing.T) { + testData := []struct { + Name string + Input string + Error bool + Expected *ServiceBusNamespaceId + }{ + { + Name: "Empty", + Input: "", + Error: true, + }, + { + Name: "No Resource Groups Segment", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000", + Error: true, + }, + { + Name: "No Resource Groups Value", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/", + Error: true, + }, + { + Name: "Resource Group ID", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/foo/", + Error: true, + }, + { + Name: "Missing Service Bus Namespace Name", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resGroup1/providers/Microsoft.ServiceBus/namespaces/", + Error: true, + }, + { + Name: "Service Bus Namespace ID", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resGroup1/providers/Microsoft.ServiceBus/namespaces/namespace1", + Expected: &ServiceBusNamespaceId{ + ResourceGroup: "resGroup1", + Name: "namespace1", + }, + }, + { + Name: "Wrong casing", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resGroup1/providers/Microsoft.ServiceBus/Namespaces/namespace1", + Error: true, + }, + } + + for _, v := range testData { + t.Logf("[DEBUG] Testing %q", v.Name) + + actual, err := ServiceBusNamespaceID(v.Input) + if err != nil { + if v.Error { + continue + } + + t.Fatalf("Expected a value but got an error: %s", err) + } + + if actual.Name != v.Expected.Name { + t.Fatalf("Expected %q but got %q for Name", v.Expected.Name, actual.Name) + } + + if actual.ResourceGroup != v.Expected.ResourceGroup { + t.Fatalf("Expected %q but got %q for Resource Group", v.Expected.ResourceGroup, actual.ResourceGroup) + } + } +} diff --git a/azurerm/internal/services/servicebus/registration.go b/azurerm/internal/services/servicebus/registration.go index 71d7f529a6716..e2f7bec5e7d1e 100644 --- a/azurerm/internal/services/servicebus/registration.go +++ b/azurerm/internal/services/servicebus/registration.go @@ -31,12 +31,13 @@ func (r Registration) SupportedDataSources() map[string]*schema.Resource { // SupportedResources returns the supported Resources supported by this Service func (r Registration) SupportedResources() map[string]*schema.Resource { return map[string]*schema.Resource{ - "azurerm_servicebus_namespace_authorization_rule": resourceArmServiceBusNamespaceAuthorizationRule(), "azurerm_servicebus_namespace": resourceArmServiceBusNamespace(), - "azurerm_servicebus_queue_authorization_rule": resourceArmServiceBusQueueAuthorizationRule(), + "azurerm_servicebus_namespace_authorization_rule": resourceArmServiceBusNamespaceAuthorizationRule(), + "azurerm_servicebus_namespace_network_rule": resourceServiceBusNamespaceNetworkRule(), "azurerm_servicebus_queue": resourceArmServiceBusQueue(), - "azurerm_servicebus_subscription_rule": resourceArmServiceBusSubscriptionRule(), + "azurerm_servicebus_queue_authorization_rule": resourceArmServiceBusQueueAuthorizationRule(), "azurerm_servicebus_subscription": resourceArmServiceBusSubscription(), + "azurerm_servicebus_subscription_rule": resourceArmServiceBusSubscriptionRule(), "azurerm_servicebus_topic_authorization_rule": resourceArmServiceBusTopicAuthorizationRule(), "azurerm_servicebus_topic": resourceArmServiceBusTopic(), } diff --git a/azurerm/internal/services/servicebus/resource_arm_servicebus_namespace.go b/azurerm/internal/services/servicebus/resource_arm_servicebus_namespace.go index a9c9fc624e524..175cedc43090e 100644 --- a/azurerm/internal/services/servicebus/resource_arm_servicebus_namespace.go +++ b/azurerm/internal/services/servicebus/resource_arm_servicebus_namespace.go @@ -3,7 +3,6 @@ package servicebus import ( "fmt" "log" - "regexp" "strings" "time" @@ -16,7 +15,10 @@ import ( "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/tf" "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/clients" "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/features" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/servicebus/parse" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/servicebus/validate" "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/tags" + azSchema "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/tf/schema" "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/timeouts" "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" ) @@ -32,9 +34,10 @@ func resourceArmServiceBusNamespace() *schema.Resource { Update: resourceArmServiceBusNamespaceCreateUpdate, Delete: resourceArmServiceBusNamespaceDelete, - Importer: &schema.ResourceImporter{ - State: schema.ImportStatePassthrough, - }, + Importer: azSchema.ValidateResourceIDPriorToImport(func(id string) error { + _, err := parse.ServiceBusNamespaceID(id) + return err + }), MigrateState: ResourceAzureRMServiceBusNamespaceMigrateState, SchemaVersion: 1, @@ -48,13 +51,10 @@ func resourceArmServiceBusNamespace() *schema.Resource { Schema: map[string]*schema.Schema{ "name": { - Type: schema.TypeString, - Required: true, - ForceNew: true, - ValidateFunc: validation.StringMatch( - regexp.MustCompile("^[a-zA-Z][-a-zA-Z0-9]{0,100}[a-zA-Z0-9]$"), - "The namespace can contain only letters, numbers, and hyphens. The namespace must start with a letter, and it must end with a letter or number.", - ), + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validate.ServiceBusNamespaceName, }, "location": azure.SchemaLocation(), diff --git a/azurerm/internal/services/servicebus/resource_arm_servicebus_namespace_network_rule.go b/azurerm/internal/services/servicebus/resource_arm_servicebus_namespace_network_rule.go new file mode 100644 index 0000000000000..ed707013778d4 --- /dev/null +++ b/azurerm/internal/services/servicebus/resource_arm_servicebus_namespace_network_rule.go @@ -0,0 +1,308 @@ +package servicebus + +import ( + "fmt" + "log" + "time" + + "github.com/Azure/azure-sdk-for-go/services/preview/servicebus/mgmt/2018-01-01-preview/servicebus" + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/helper/validation" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/azure" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/set" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/suppress" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/tf" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/clients" + validateNetwork "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/network/validate" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/servicebus/parse" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/servicebus/validate" + azSchema "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/tf/schema" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/timeouts" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" +) + +func resourceServiceBusNamespaceNetworkRule() *schema.Resource { + return &schema.Resource{ + Create: resourceServiceBusNamespaceNetworkRuleCreateUpdate, + Read: resourceServiceBusNamespaceNetworkRuleRead, + Update: resourceServiceBusNamespaceNetworkRuleCreateUpdate, + Delete: resourceServiceBusNamespaceNetworkRuleDelete, + + Importer: azSchema.ValidateResourceIDPriorToImport(func(id string) error { + _, err := parse.ServiceBusNamespaceNetworkRuleID(id) + return err + }), + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(30 * time.Minute), + Read: schema.DefaultTimeout(5 * time.Minute), + Update: schema.DefaultTimeout(30 * time.Minute), + Delete: schema.DefaultTimeout(30 * time.Minute), + }, + + Schema: map[string]*schema.Schema{ + "resource_group_name": azure.SchemaResourceGroupName(), + + "namespace_name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validate.ServiceBusNamespaceName, + }, + + "default_action": { + Type: schema.TypeString, + Optional: true, + Default: string(servicebus.Allow), + ValidateFunc: validation.StringInSlice([]string{ + string(servicebus.Allow), + string(servicebus.Deny), + }, false), + }, + + "ip_masks": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + + "network_rules": { + Type: schema.TypeSet, + Optional: true, + Set: networkRuleHash, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "subnet_id": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validateNetwork.SubnetID, + // The subnet ID returned from the service will have `resourceGroup/{resourceGroupName}` all in lower cases... + DiffSuppressFunc: suppress.CaseDifference, + }, + "ignore_missing_vnet_service_endpoint": { + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + }, + }, + }, + }, + } +} + +func resourceServiceBusNamespaceNetworkRuleCreateUpdate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*clients.Client).ServiceBus.NamespacesClientPreview + ctx, cancel := timeouts.ForCreateUpdate(meta.(*clients.Client).StopContext, d) + defer cancel() + + resourceGroup := d.Get("resource_group_name").(string) + namespaceName := d.Get("namespace_name").(string) + + if d.IsNewResource() { + existing, err := client.GetNetworkRuleSet(ctx, resourceGroup, namespaceName) + if err != nil { + if !utils.ResponseWasNotFound(existing.Response) { + return fmt.Errorf("failed to check for presence of existing Service Bus Namespace Network Rule (Namespace %q / Resource Group %q): %+v", namespaceName, resourceGroup, err) + } + } + + // This resource is unique to the corresponding service bus namespace. + // It will be created automatically along with the namespace, therefore we check whether this resource is identical to a "deleted" one + if !CheckNetworkRuleNullified(existing) { + return tf.ImportAsExistsError("azurerm_servicebus_namespace_network_rule", *existing.ID) + } + } + + parameters := servicebus.NetworkRuleSet{ + NetworkRuleSetProperties: &servicebus.NetworkRuleSetProperties{ + DefaultAction: servicebus.DefaultAction(d.Get("default_action").(string)), + VirtualNetworkRules: expandServiceBusNamespaceVirtualNetworkRules(d.Get("network_rules").(*schema.Set).List()), + IPRules: expandServiceBusNamespaceIPRules(d.Get("ip_masks").(*schema.Set).List()), + }, + } + + if _, err := client.CreateOrUpdateNetworkRuleSet(ctx, resourceGroup, namespaceName, parameters); err != nil { + return fmt.Errorf("failed to create Service Bus Namespace Network Rule (Namespace %q / Resource Group %q): %+v", namespaceName, resourceGroup, err) + } + + resp, err := client.GetNetworkRuleSet(ctx, resourceGroup, namespaceName) + if err != nil { + return fmt.Errorf("failed to retrieve Service Bus Namespace Network Rule (Namespace %q / Resource Group %q): %+v", namespaceName, resourceGroup, err) + } + if resp.ID == nil || *resp.ID == "" { + return fmt.Errorf("cannot read Service Bus Namespace Network Rule (Namespace %q / Resource Group %q) ID", namespaceName, resourceGroup) + } + d.SetId(*resp.ID) + + return resourceServiceBusNamespaceNetworkRuleRead(d, meta) +} + +func resourceServiceBusNamespaceNetworkRuleRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*clients.Client).ServiceBus.NamespacesClientPreview + ctx, cancel := timeouts.ForRead(meta.(*clients.Client).StopContext, d) + defer cancel() + + id, err := parse.ServiceBusNamespaceNetworkRuleID(d.Id()) + if err != nil { + return err + } + + resp, err := client.GetNetworkRuleSet(ctx, id.ResourceGroup, id.NamespaceName) + if err != nil { + if utils.ResponseWasNotFound(resp.Response) { + log.Printf("[INFO] Service Bus Namespace Network Rule %q does not exist - removing from state", d.Id()) + d.SetId("") + return nil + } + return fmt.Errorf("failed to read Service Bus Namespace Network Rule %q (Namespace %q / Resource Group %q): %+v", id.Name, id.NamespaceName, id.ResourceGroup, err) + } + + d.Set("namespace_name", id.NamespaceName) + d.Set("resource_group_name", id.ResourceGroup) + + if props := resp.NetworkRuleSetProperties; props != nil { + d.Set("default_action", string(props.DefaultAction)) + + if err := d.Set("network_rules", schema.NewSet(networkRuleHash, flattenServiceBusNamespaceVirtualNetworkRules(props.VirtualNetworkRules))); err != nil { + return fmt.Errorf("failed to set `network_rules`: %+v", err) + } + + if err := d.Set("ip_masks", flattenServiceBusNamespaceIPRules(props.IPRules)); err != nil { + return fmt.Errorf("failed to set `ip_masks`: %+v", err) + } + } + + return nil +} + +func resourceServiceBusNamespaceNetworkRuleDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*clients.Client).ServiceBus.NamespacesClientPreview + ctx, cancel := timeouts.ForDelete(meta.(*clients.Client).StopContext, d) + defer cancel() + + id, err := parse.ServiceBusNamespaceNetworkRuleID(d.Id()) + if err != nil { + return err + } + + // A network rule is unique to a namespace, this rule cannot be deleted. + // Therefore we here are just disabling it by setting the default_action to allow and remove all its rules and masks + + parameters := servicebus.NetworkRuleSet{ + NetworkRuleSetProperties: &servicebus.NetworkRuleSetProperties{ + DefaultAction: servicebus.Deny, + }, + } + + if _, err := client.CreateOrUpdateNetworkRuleSet(ctx, id.ResourceGroup, id.NamespaceName, parameters); err != nil { + return fmt.Errorf("failed to delete Service Bus Namespace Network Rule %q (Namespace %q / Resource Group %q): %+v", id.Name, id.NamespaceName, id.ResourceGroup, err) + } + + return nil +} + +func expandServiceBusNamespaceVirtualNetworkRules(input []interface{}) *[]servicebus.NWRuleSetVirtualNetworkRules { + if len(input) == 0 { + return nil + } + + result := make([]servicebus.NWRuleSetVirtualNetworkRules, 0) + for _, v := range input { + raw := v.(map[string]interface{}) + result = append(result, servicebus.NWRuleSetVirtualNetworkRules{ + Subnet: &servicebus.Subnet{ + ID: utils.String(raw["subnet_id"].(string)), + }, + IgnoreMissingVnetServiceEndpoint: utils.Bool(raw["ignore_missing_vnet_service_endpoint"].(bool)), + }) + } + + return &result +} + +func flattenServiceBusNamespaceVirtualNetworkRules(input *[]servicebus.NWRuleSetVirtualNetworkRules) []interface{} { + result := make([]interface{}, 0) + if input == nil { + return result + } + + for _, v := range *input { + subnetId := "" + if v.Subnet != nil && v.Subnet.ID != nil { + subnetId = *v.Subnet.ID + } + + ignore := false + if v.IgnoreMissingVnetServiceEndpoint != nil { + ignore = *v.IgnoreMissingVnetServiceEndpoint + } + + result = append(result, map[string]interface{}{ + "subnet_id": subnetId, + "ignore_missing_vnet_service_endpoint": ignore, + }) + } + + return result +} + +func expandServiceBusNamespaceIPRules(input []interface{}) *[]servicebus.NWRuleSetIPRules { + if len(input) == 0 { + return nil + } + + result := make([]servicebus.NWRuleSetIPRules, 0) + for _, v := range input { + result = append(result, servicebus.NWRuleSetIPRules{ + IPMask: utils.String(v.(string)), + Action: servicebus.NetworkRuleIPActionAllow, + }) + } + + return &result +} + +func flattenServiceBusNamespaceIPRules(input *[]servicebus.NWRuleSetIPRules) []interface{} { + result := make([]interface{}, 0) + if input == nil || len(*input) == 0 { + return result + } + + for _, v := range *input { + if v.IPMask != nil { + result = append(result, *v.IPMask) + } + } + + return result +} + +func networkRuleHash(input interface{}) int { + v := input.(map[string]interface{}) + + // we are just taking subnet_id into the hash function and ignore the ignore_missing_vnet_service_endpoint to ensure there would be no duplicates of subnet id + // the service returns this ID with segment resourceGroup and resource group name all in lower cases, to avoid unnecessary diff, we extract this ID and reconstruct this hash code + return set.HashStringIgnoreCase(v["subnet_id"]) +} + +func CheckNetworkRuleNullified(resp servicebus.NetworkRuleSet) bool { + if resp.ID == nil || *resp.ID == "" { + return true + } + if resp.NetworkRuleSetProperties == nil { + return true + } + if resp.DefaultAction != servicebus.Deny { + return false + } + if resp.VirtualNetworkRules != nil && len(*resp.VirtualNetworkRules) > 0 { + return false + } + if resp.IPRules != nil && len(*resp.IPRules) > 0 { + return false + } + return true +} diff --git a/azurerm/internal/services/servicebus/tests/resource_arm_servicebus_namespace_network_rule_test.go b/azurerm/internal/services/servicebus/tests/resource_arm_servicebus_namespace_network_rule_test.go new file mode 100644 index 0000000000000..36e6e6812337e --- /dev/null +++ b/azurerm/internal/services/servicebus/tests/resource_arm_servicebus_namespace_network_rule_test.go @@ -0,0 +1,252 @@ +package tests + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/terraform" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/acceptance" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/clients" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/servicebus" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/servicebus/parse" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" +) + +func TestAccAzureRMServiceBusNamespaceNetworkRule_basic(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_servicebus_namespace_network_rule", "test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMServiceBusNamespaceNetworkRuleDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMServiceBusNamespaceNetworkRule_basic(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMServiceBusNamespaceNetworkRuleExists(data.ResourceName), + ), + }, + data.ImportStep(), + }, + }) +} + +func TestAccAzureRMServiceBusNamespaceNetworkRule_complete(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_servicebus_namespace_network_rule", "test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMServiceBusNamespaceNetworkRuleDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMServiceBusNamespaceNetworkRule_complete(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMServiceBusNamespaceNetworkRuleExists(data.ResourceName), + ), + }, + data.ImportStep(), + }, + }) +} + +func TestAccAzureRMServiceBusNamespaceNetworkRule_update(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_servicebus_namespace_network_rule", "test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMServiceBusNamespaceNetworkRuleDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMServiceBusNamespaceNetworkRule_basic(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMServiceBusNamespaceNetworkRuleExists(data.ResourceName), + ), + }, + data.ImportStep(), + { + Config: testAccAzureRMServiceBusNamespaceNetworkRule_complete(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMServiceBusNamespaceNetworkRuleExists(data.ResourceName), + ), + }, + data.ImportStep(), + { + Config: testAccAzureRMServiceBusNamespaceNetworkRule_basic(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMServiceBusNamespaceNetworkRuleExists(data.ResourceName), + ), + }, + data.ImportStep(), + }, + }) +} + +func TestAccAzureRMServiceBusNamespaceNetworkRule_requiresImport(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_servicebus_namespace_network_rule", "test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMServiceBusNamespaceNetworkRuleDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMServiceBusNamespaceNetworkRule_basic(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMServiceBusNamespaceNetworkRuleExists(data.ResourceName), + ), + }, + data.RequiresImportErrorStep(testAccAzureRMServiceBusNamespaceNetworkRule_requiresImport), + }, + }) +} + +func testCheckAzureRMServiceBusNamespaceNetworkRuleExists(resourceName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + client := acceptance.AzureProvider.Meta().(*clients.Client).ServiceBus.NamespacesClientPreview + ctx := acceptance.AzureProvider.Meta().(*clients.Client).StopContext + + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("Service Bus Namespace Network Rule not found: %s", resourceName) + } + + id, err := parse.ServiceBusNamespaceNetworkRuleID(rs.Primary.ID) + if err != nil { + return err + } + + if resp, err := client.GetNetworkRuleSet(ctx, id.ResourceGroup, id.NamespaceName); err != nil { + if utils.ResponseWasNotFound(resp.Response) { + return fmt.Errorf("Service Bus Namespace Network Rule (Namespace %q / Resource Group %q) does not exist", id.NamespaceName, id.ResourceGroup) + } + return fmt.Errorf("failed to GetNetworkRuleSet on ServiceBus.NamespacesClientPreview: %+v", err) + } + + return nil + } +} + +func testCheckAzureRMServiceBusNamespaceNetworkRuleDestroy(s *terraform.State) error { + client := acceptance.AzureProvider.Meta().(*clients.Client).ServiceBus.NamespacesClientPreview + ctx := acceptance.AzureProvider.Meta().(*clients.Client).StopContext + + for _, rs := range s.RootModule().Resources { + if rs.Type != "azurerm_servicebus_namespace_network_rule" { + continue + } + + id, err := parse.ServiceBusNamespaceNetworkRuleID(rs.Primary.ID) + if err != nil { + return err + } + + // this resource cannot be deleted, instead, we check if this setting was set back to empty + resp, err := client.GetNetworkRuleSet(ctx, id.ResourceGroup, id.NamespaceName) + if err != nil { + if !utils.ResponseWasNotFound(resp.Response) { + return fmt.Errorf("failed to GetNetworkRuleSet on ServiceBus.NamespacesClientPreview: %+v", err) + } + return nil + } + + if !servicebus.CheckNetworkRuleNullified(resp) { + return fmt.Errorf("the Service Bus Namespace Network Rule (Namespace %q / Resource Group %q) still exists", id.NamespaceName, id.ResourceGroup) + } + } + + return nil +} + +func testAccAzureRMServiceBusNamespaceNetworkRule_basic(data acceptance.TestData) string { + template := testAccAzureRMServiceBusNamespaceNetworkRule_template(data) + return fmt.Sprintf(` +%s + +resource "azurerm_servicebus_namespace_network_rule" "test" { + namespace_name = azurerm_servicebus_namespace.test.name + resource_group_name = azurerm_resource_group.test.name + + default_action = "Deny" + + network_rules { + subnet_id = azurerm_subnet.test.id + ignore_missing_vnet_service_endpoint = false + } +} +`, template) +} + +func testAccAzureRMServiceBusNamespaceNetworkRule_complete(data acceptance.TestData) string { + template := testAccAzureRMServiceBusNamespaceNetworkRule_template(data) + return fmt.Sprintf(` +%s + +resource "azurerm_servicebus_namespace_network_rule" "test" { + namespace_name = azurerm_servicebus_namespace.test.name + resource_group_name = azurerm_resource_group.test.name + + default_action = "Deny" + + network_rules { + subnet_id = azurerm_subnet.test.id + ignore_missing_vnet_service_endpoint = false + } + + ip_masks = ["1.1.1.1"] +} +`, template) +} + +func testAccAzureRMServiceBusNamespaceNetworkRule_template(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +resource "azurerm_resource_group" "test" { + name = "acctestRG-sb-%[1]d" + location = "%[2]s" +} + +resource "azurerm_servicebus_namespace" "test" { + name = "acctest-sb-namespace-%[1]d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + sku = "Premium" + + capacity = 1 +} + +resource "azurerm_virtual_network" "test" { + name = "acctest-vnet-%[1]d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + address_space = ["172.17.0.0/16"] + dns_servers = ["10.0.0.4", "10.0.0.5"] +} + +resource "azurerm_subnet" "test" { + name = "${azurerm_virtual_network.test.name}-default" + resource_group_name = azurerm_resource_group.test.name + virtual_network_name = azurerm_virtual_network.test.name + address_prefix = "172.17.0.0/24" + + service_endpoints = ["Microsoft.ServiceBus"] +} +`, data.RandomInteger, data.Locations.Primary) +} + +func testAccAzureRMServiceBusNamespaceNetworkRule_requiresImport(data acceptance.TestData) string { + template := testAccAzureRMServiceBusNamespaceNetworkRule_basic(data) + return fmt.Sprintf(` +%s + +resource "azurerm_servicebus_namespace_network_rule" "import" { + namespace_name = azurerm_servicebus_namespace_network_rule.test.namespace_name + resource_group_name = azurerm_servicebus_namespace_network_rule.test.resource_group_name +} +`, template) +} diff --git a/azurerm/internal/services/servicebus/validate/namespace.go b/azurerm/internal/services/servicebus/validate/namespace.go new file mode 100644 index 0000000000000..fb488f0c69099 --- /dev/null +++ b/azurerm/internal/services/servicebus/validate/namespace.go @@ -0,0 +1,38 @@ +package validate + +import ( + "fmt" + "regexp" + + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/servicebus/parse" +) + +func ServiceBusNamespaceID(i interface{}, k string) (warnings []string, errors []error) { + v, ok := i.(string) + if !ok { + errors = append(errors, fmt.Errorf("expected type of %q to be string", k)) + return + } + + if _, err := parse.ServiceBusNamespaceID(v); err != nil { + errors = append(errors, fmt.Errorf("cannot parse %q as a Service Bus Namespace ID: %+v", k, err)) + return + } + + return warnings, errors +} + +func ServiceBusNamespaceName(i interface{}, k string) (warnings []string, errors []error) { + v, ok := i.(string) + if !ok { + errors = append(errors, fmt.Errorf("expected type of %q to be string", k)) + return + } + + if matched := regexp.MustCompile("^[a-zA-Z][-a-zA-Z0-9]{0,100}[a-zA-Z0-9]$").MatchString(v); !matched { + errors = append(errors, fmt.Errorf("%q can contain only letters, numbers, and hyphens. The namespace must start with a letter, and it must end with a letter or number", k)) + return + } + + return warnings, errors +} diff --git a/azurerm/internal/services/servicebus/validate/namespace_network_rule.go b/azurerm/internal/services/servicebus/validate/namespace_network_rule.go new file mode 100644 index 0000000000000..a75ebb6e48299 --- /dev/null +++ b/azurerm/internal/services/servicebus/validate/namespace_network_rule.go @@ -0,0 +1,34 @@ +package validate + +import ( + "fmt" + + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/servicebus/parse" +) + +func ServiceBusNamespaceNetworkRuleID(i interface{}, k string) (warnings []string, errors []error) { + v, ok := i.(string) + if !ok { + errors = append(errors, fmt.Errorf("expected type of %q to be string", k)) + return + } + + if _, err := parse.ServiceBusNamespaceNetworkRuleID(v); err != nil { + errors = append(errors, fmt.Errorf("cannot parse %q as a Service Bus Namespace Network Rule ID: %+v", k, err)) + return + } + + return warnings, errors +} + +func ServiceBusNamespaceNetworkRuleName(i interface{}, k string) (warnings []string, errors []error) { + _, ok := i.(string) + if !ok { + errors = append(errors, fmt.Errorf("expected type of %q to be string", k)) + return + } + + // TODO -- investigate the naming rule + + return warnings, errors +} diff --git a/website/azurerm.erb b/website/azurerm.erb index 82495facfd3a2..871a5693309dd 100644 --- a/website/azurerm.erb +++ b/website/azurerm.erb @@ -1783,6 +1783,10 @@ azurerm_servicebus_namespace_authorization_rule +
  • + azurerm_servicebus_namespace_network_rule +
  • +
  • azurerm_servicebus_queue
  • diff --git a/website/docs/r/servicebus_namespace.html.markdown b/website/docs/r/servicebus_namespace.html.markdown index 72f44a81d3b65..f0da7e9ca3bc8 100644 --- a/website/docs/r/servicebus_namespace.html.markdown +++ b/website/docs/r/servicebus_namespace.html.markdown @@ -13,13 +13,17 @@ Manages a ServiceBus Namespace. ## Example Usage ```hcl +provider "azurerm" { + features {} +} + resource "azurerm_resource_group" "example" { name = "terraform-servicebus" location = "West Europe" } resource "azurerm_servicebus_namespace" "example" { - name = "tfex_servicebus_namespace" + name = "tfex-servicebus-namespace" location = azurerm_resource_group.example.location resource_group_name = azurerm_resource_group.example.name sku = "Standard" diff --git a/website/docs/r/servicebus_namespace_network_rule.html.markdown b/website/docs/r/servicebus_namespace_network_rule.html.markdown new file mode 100644 index 0000000000000..823b173f2c827 --- /dev/null +++ b/website/docs/r/servicebus_namespace_network_rule.html.markdown @@ -0,0 +1,111 @@ +--- +subcategory: "Messaging" +layout: "azurerm" +page_title: "Azure Resource Manager: azurerm_servicebus_namespace_network_rule" +description: |- + Manages a ServiceBus Namespace Network Rule. +--- + +# azurerm_servicebus_namespace_network_rule + +Manages a ServiceBus Namespace Network Rule. + +## Example Usage + +```hcl +provider "azurerm" { + features {} +} + +resource "azurerm_resource_group" "example" { + name = "example-resources" + location = "West Europe" +} + +resource "azurerm_servicebus_namespace" "example" { + name = "example-sb-namespace" + location = azurerm_resource_group.example.location + resource_group_name = azurerm_resource_group.example.name + sku = "Premium" + + capacity = 1 +} + +resource "azurerm_virtual_network" "example" { + name = "example-vnet" + location = azurerm_resource_group.example.location + resource_group_name = azurerm_resource_group.example.name + address_space = ["172.17.0.0/16"] + dns_servers = ["10.0.0.4", "10.0.0.5"] +} + +resource "azurerm_subnet" "example" { + name = "default" + resource_group_name = azurerm_resource_group.example.name + virtual_network_name = azurerm_virtual_network.example.name + address_prefix = "172.17.0.0/24" + + service_endpoints = ["Microsoft.ServiceBus"] +} + +resource "azurerm_servicebus_namespace_network_rule" "example" { + namespace_name = azurerm_servicebus_namespace.example.name + resource_group_name = azurerm_resource_group.example.name + + default_action = "Deny" + + network_rules { + subnet_id = azurerm_subnet.example.id + ignore_missing_vnet_service_endpoint = false + } + + ip_masks = ["1.1.1.1"] +} +``` + +## Argument Reference + +The following arguments are supported: + +* `resource_group_name` - (Required) Specifies the name of the Resource Group where the ServiceBus Namespace Network Rule should exist. Changing this forces a new resource to be created. + +* `namespace_name` - (Required) Specifies the ServiceBus Namespace name to which to attach the ServiceBus Namespace Network Rule. Changing this forces a new resource to be created. + +~> **NOTE:** The ServiceBus Namespace must be `Premium` in order to attach a ServiceBus Namespace Network Rule. + +* `default_action` - (Optional) Specifies the default action for the ServiceBus Namespace Network Rule. Possible values are `Allow` and `Deny`. Defaults to `Deny`. + +* `ip_masks` - (Optional) A list of IP filter masks that are added to allow access to the ServiceBus Namespace. + +* `network_rules` - (Optional) One or more `network_rules` blocks defined below. + +--- + +A `network_rules` block supports the following: + +* `subnet_id` - (Required) The ID of the subnet you want to allow to access the corresponding ServiceBus Namespace. + +* `ignore_missing_vnet_service_endpoint` - (Optional) Should the ServiceBus Namespace Network Rule ignore missing Virtual Network Service Endpoint option in the Subnet? Defaults to `false`. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The ID of the ServiceBus Namespace Network Rule. + +## Timeouts + +The `timeouts` block allows you to specify [timeouts](https://www.terraform.io/docs/configuration/resources.html#timeouts) for certain actions: + +* `create` - (Defaults to 30 minutes) Used when creating the ServiceBus Namespace Network Rule. +* `update` - (Defaults to 30 minutes) Used when updating the ServiceBus Namespace Network Rule. +* `read` - (Defaults to 5 minutes) Used when retrieving the ServiceBus Namespace Network Rule. +* `delete` - (Defaults to 30 minutes) Used when deleting the ServiceBus Namespace Network Rule. + +## Import + +Service Bus Namespace can be imported using the `resource id`, e.g. + +```shell +terraform import azurerm_servicebus_namespace_network_rule.example /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/group1/providers/Microsoft.Servicebus/namespaces/sbns1/networkrulesets/default +```