diff --git a/azurerm/internal/services/network/client.go b/azurerm/internal/services/network/client.go index cb4a6a8d5fe0..08009bf37426 100644 --- a/azurerm/internal/services/network/client.go +++ b/azurerm/internal/services/network/client.go @@ -9,6 +9,7 @@ type Client struct { ApplicationGatewaysClient *network.ApplicationGatewaysClient ApplicationSecurityGroupsClient *network.ApplicationSecurityGroupsClient AzureFirewallsClient *network.AzureFirewallsClient + BastionHostsClient *network.BastionHostsClient ConnectionMonitorsClient *network.ConnectionMonitorsClient DDOSProtectionPlansClient *network.DdosProtectionPlansClient ExpressRouteAuthsClient *network.ExpressRouteCircuitAuthorizationsClient @@ -45,6 +46,9 @@ func BuildClient(o *common.ClientOptions) *Client { AzureFirewallsClient := network.NewAzureFirewallsClientWithBaseURI(o.ResourceManagerEndpoint, o.SubscriptionId) o.ConfigureClient(&AzureFirewallsClient.Client, o.ResourceManagerAuthorizer) + BastionHostsClient := network.NewBastionHostsClientWithBaseURI(o.ResourceManagerEndpoint, o.SubscriptionId) + o.ConfigureClient(&BastionHostsClient.Client, o.ResourceManagerAuthorizer) + ConnectionMonitorsClient := network.NewConnectionMonitorsClientWithBaseURI(o.ResourceManagerEndpoint, o.SubscriptionId) o.ConfigureClient(&ConnectionMonitorsClient.Client, o.ResourceManagerAuthorizer) @@ -121,6 +125,7 @@ func BuildClient(o *common.ClientOptions) *Client { ApplicationGatewaysClient: &ApplicationGatewaysClient, ApplicationSecurityGroupsClient: &ApplicationSecurityGroupsClient, AzureFirewallsClient: &AzureFirewallsClient, + BastionHostsClient: &BastionHostsClient, ConnectionMonitorsClient: &ConnectionMonitorsClient, DDOSProtectionPlansClient: &DDOSProtectionPlansClient, ExpressRouteAuthsClient: &ExpressRouteAuthsClient, diff --git a/azurerm/provider.go b/azurerm/provider.go index 6aafed3f3585..b1912fef249a 100644 --- a/azurerm/provider.go +++ b/azurerm/provider.go @@ -199,6 +199,7 @@ func Provider() terraform.ResourceProvider { "azurerm_azuread_application": resourceArmActiveDirectoryApplication(), "azurerm_azuread_service_principal_password": resourceArmActiveDirectoryServicePrincipalPassword(), "azurerm_azuread_service_principal": resourceArmActiveDirectoryServicePrincipal(), + "azurerm_bastion_host": resourceArmBastionHost(), "azurerm_batch_account": resourceArmBatchAccount(), "azurerm_batch_application": resourceArmBatchApplication(), "azurerm_batch_certificate": resourceArmBatchCertificate(), diff --git a/azurerm/resource_arm_bastion_host.go b/azurerm/resource_arm_bastion_host.go new file mode 100644 index 000000000000..a7fe13888098 --- /dev/null +++ b/azurerm/resource_arm_bastion_host.go @@ -0,0 +1,279 @@ +package azurerm + +import ( + "fmt" + "log" + "regexp" + + "github.com/Azure/azure-sdk-for-go/services/network/mgmt/2019-06-01/network" + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "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/helpers/tf" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/tags" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" +) + +func resourceArmBastionHost() *schema.Resource { + return &schema.Resource{ + Create: resourceArmBastionHostCreateUpdate, + Read: resourceArmBastionHostRead, + Update: resourceArmBastionHostCreateUpdate, + Delete: resourceArmBastionHostDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validateAzureRMBastionHostName, + }, + + "location": azure.SchemaLocation(), + + "resource_group_name": azure.SchemaResourceGroupName(), + + "ip_configuration": { + Type: schema.TypeList, + ForceNew: true, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validateAzureRMBastionIPConfigName, + }, + "subnet_id": { + Type: schema.TypeString, + Required: true, + ValidateFunc: azure.ValidateResourceID, + }, + "public_ip_address_id": { + Type: schema.TypeString, + Required: true, + ValidateFunc: azure.ValidateResourceID, + }, + }, + }, + }, + + "dns_name": { + Type: schema.TypeString, + Computed: true, + }, + + "tags": tags.Schema(), + }, + } +} + +func resourceArmBastionHostCreateUpdate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*ArmClient).Network.BastionHostsClient + ctx := meta.(*ArmClient).StopContext + + log.Println("[INFO] preparing arguments for Azure Bastion Host creation.") + + resourceGroup := d.Get("resource_group_name").(string) + name := d.Get("name").(string) + location := azure.NormalizeLocation(d.Get("location").(string)) + tags := d.Get("tags").(map[string]interface{}) + + if requireResourcesToBeImported && d.IsNewResource() { + existing, err := client.Get(ctx, resourceGroup, name) + if err != nil { + if !utils.ResponseWasNotFound(existing.Response) { + return fmt.Errorf("Error checking for presence of existing Bastion Host %q (Resource Group %q): %s", name, resourceGroup, err) + } + } + + if existing.ID != nil && *existing.ID != "" { + return tf.ImportAsExistsError("azurerm_bastion_host", *existing.ID) + } + } + + parameters := network.BastionHost{ + Location: &location, + BastionHostPropertiesFormat: &network.BastionHostPropertiesFormat{ + IPConfigurations: expandArmBastionHostIPConfiguration(d.Get("ip_configuration").([]interface{})), + }, + Tags: expandTags(tags), + } + + future, err := client.CreateOrUpdate(ctx, resourceGroup, name, parameters) + if err != nil { + return fmt.Errorf("Error creating/updating Bastion Host %q (Resource Group %q): %+v", name, resourceGroup, err) + } + + if err = future.WaitForCompletionRef(ctx, client.Client); err != nil { + return fmt.Errorf("Error waiting for creation/update of Bastion Host %q (Resource Group %q): %+v", name, resourceGroup, err) + } + + read, err := client.Get(ctx, resourceGroup, name) + if err != nil { + return fmt.Errorf("Error retrieving Bastion Host %q (Resource Group %q): %+v", name, resourceGroup, err) + } + + d.SetId(*read.ID) + + return resourceArmBastionHostRead(d, meta) +} + +func resourceArmBastionHostRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*ArmClient).Network.BastionHostsClient + ctx := meta.(*ArmClient).StopContext + + id, err := azure.ParseAzureResourceID(d.Id()) + if err != nil { + return err + } + + name := id.Path["bastionHosts"] + resourceGroup := id.ResourceGroup + + resp, err := client.Get(ctx, resourceGroup, name) + if err != nil { + if utils.ResponseWasNotFound(resp.Response) { + d.SetId("") + log.Printf("[DEBUG] Bastion Host %q was not found in Resource Group %q - removing from state!", name, resourceGroup) + return nil + } + return fmt.Errorf("Error reading the state of Bastion Host %q: %+v", name, err) + } + + d.Set("name", resp.Name) + d.Set("resource_group_name", resourceGroup) + + if location := resp.Location; location != nil { + d.Set("location", azure.NormalizeLocation(*location)) + } + + if props := resp.BastionHostPropertiesFormat; props != nil { + d.Set("dns_name", props.DNSName) + + if ipConfigs := props.IPConfigurations; ipConfigs != nil { + if err := d.Set("ip_configuration", flattenArmBastionHostIPConfiguration(ipConfigs)); err != nil { + return fmt.Errorf("Error flattening `ip_configuration`: %+v", err) + } + } + } + + return tags.FlattenAndSet(d, resp.Tags) +} + +func resourceArmBastionHostDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*ArmClient).Network.BastionHostsClient + ctx := meta.(*ArmClient).StopContext + + id, err := parseAzureResourceID(d.Id()) + if err != nil { + return err + } + + name := id.Path["bastionHosts"] + resourceGroup := id.ResourceGroup + + future, err := client.Delete(ctx, resourceGroup, name) + if err != nil { + return fmt.Errorf("Error deleting Bastion Host %q (Resource Group %q): %+v", name, resourceGroup, err) + } + + if err = future.WaitForCompletionRef(ctx, client.Client); err != nil { + if !response.WasNotFound(future.Response()) { + return fmt.Errorf("Error waiting for deletion of Bastion Host %q (Resource Group %q): %+v", name, resourceGroup, err) + } + } + + return nil +} + +func validateAzureRMBastionHostName(v interface{}, k string) (warnings []string, errors []error) { + value := v.(string) + if !regexp.MustCompile(`^[a-zA-Z0-9]+$`).MatchString(value) { + errors = append(errors, fmt.Errorf("lowercase letters, highercase letters numbers only are allowed in %q: %q", k, value)) + } + + if 1 > len(value) { + errors = append(errors, fmt.Errorf("%q cannot be less than 1 characters: %q", k, value)) + } + + if len(value) > 64 { + errors = append(errors, fmt.Errorf("%q cannot be longer than 64 characters: %q %d", k, value, len(value))) + } + + return warnings, errors +} + +func validateAzureRMBastionIPConfigName(v interface{}, k string) (warnings []string, errors []error) { + value := v.(string) + if !regexp.MustCompile(`^[a-zA-Z0-9]+$`).MatchString(value) { + errors = append(errors, fmt.Errorf("lowercase letters, highercase letters numbers only are allowed in %q: %q", k, value)) + } + + if 1 > len(value) { + errors = append(errors, fmt.Errorf("%q cannot be less than 1 characters: %q", k, value)) + } + + if len(value) > 32 { + errors = append(errors, fmt.Errorf("%q cannot be longer than 32 characters: %q %d", k, value, len(value))) + } + + return warnings, errors +} + +func expandArmBastionHostIPConfiguration(input []interface{}) (ipConfigs *[]network.BastionHostIPConfiguration) { + if len(input) == 0 { + return nil + } + + property := input[0].(map[string]interface{}) + ipConfName := property["name"].(string) + subID := property["subnet_id"].(string) + pipID := property["public_ip_address_id"].(string) + + return &[]network.BastionHostIPConfiguration{ + { + Name: &ipConfName, + BastionHostIPConfigurationPropertiesFormat: &network.BastionHostIPConfigurationPropertiesFormat{ + Subnet: &network.SubResource{ + ID: &subID, + }, + PublicIPAddress: &network.SubResource{ + ID: &pipID, + }, + }, + }, + } +} + +func flattenArmBastionHostIPConfiguration(ipConfigs *[]network.BastionHostIPConfiguration) []interface{} { + result := make([]interface{}, 0) + if ipConfigs == nil { + return result + } + + for _, config := range *ipConfigs { + ipConfig := make(map[string]interface{}) + + if config.Name != nil { + ipConfig["name"] = *config.Name + } + + if props := config.BastionHostIPConfigurationPropertiesFormat; props != nil { + if subnet := props.Subnet; subnet != nil { + ipConfig["subnet_id"] = *subnet.ID + } + + if pip := props.PublicIPAddress; pip != nil { + ipConfig["public_ip_address_id"] = *pip.ID + } + } + + result = append(result, ipConfig) + } + return result +} diff --git a/azurerm/resource_arm_bastion_host_test.go b/azurerm/resource_arm_bastion_host_test.go new file mode 100644 index 000000000000..59c3dc28135d --- /dev/null +++ b/azurerm/resource_arm_bastion_host_test.go @@ -0,0 +1,251 @@ +package azurerm + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/terraform" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/tf" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/features" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" +) + +func TestAccAzureRMBastionHost_basic(t *testing.T) { + resourceName := "azurerm_bastion_host.test" + ri := tf.AccRandTimeInt() + rs := acctest.RandString(4) + + config := testAccAzureRMBastionHost_basic(ri, rs, testLocation()) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMBastionHostDestroy, + Steps: []resource.TestStep{ + { + Config: config, + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMBastionHostExists(resourceName), + ), + }, + }, + }) +} + +func TestAccAzureRMBastionHost_complete(t *testing.T) { + resourceName := "azurerm_bastion_host.test" + ri := tf.AccRandTimeInt() + rs := acctest.RandString(4) + + config := testAccAzureRMBastionHost_complete(ri, rs, testLocation()) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMBastionHostDestroy, + Steps: []resource.TestStep{ + { + Config: config, + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMBastionHostExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.environment", "production"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAzureRMBastionHost_requiresImport(t *testing.T) { + if !features.ShouldResourcesBeImported() { + t.Skip("Skipping since resources aren't required to be imported") + return + } + + resourceName := "azurerm_bastion_host.test" + ri := tf.AccRandTimeInt() + rs := acctest.RandString(4) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMBastionHostDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMBastionHost_basic(ri, rs, testLocation()), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMBastionHostExists(resourceName), + ), + }, + { + Config: testAccAzureRMBastionHost_requiresImport(ri, rs, testLocation()), + ExpectError: testRequiresImportError("azurerm_bastion_host"), + }, + }, + }) +} + +func testAccAzureRMBastionHost_basic(rInt int, rString string, location string) string { + return fmt.Sprintf(` +resource "azurerm_resource_group" "test" { + name = "acctestRG-bastion-%d" + location = "%s" +} + +resource "azurerm_virtual_network" "test" { + name = "acctestVNet%s" + address_space = ["192.168.1.0/24"] + location = "${azurerm_resource_group.test.location}" + resource_group_name = "${azurerm_resource_group.test.name}" +} + +resource "azurerm_subnet" "test" { + name = "AzureBastionSubnet" + resource_group_name = "${azurerm_resource_group.test.name}" + virtual_network_name = "${azurerm_virtual_network.test.name}" + address_prefix = "192.168.1.224/27" +} + +resource "azurerm_public_ip" "test" { + name = "acctestBastionPIP%d" + location = "${azurerm_resource_group.test.location}" + resource_group_name = "${azurerm_resource_group.test.name}" + allocation_method = "Static" + sku = "Standard" +} + +resource "azurerm_bastion_host" "test" { + name = "acctestBastion%s" + location = "${azurerm_resource_group.test.location}" + resource_group_name = "${azurerm_resource_group.test.name}" + + ip_configuration { + name = "configuration" + subnet_id = "${azurerm_subnet.test.id}" + public_ip_address_id = "${azurerm_public_ip.test.id}" + } +} +`, rInt, location, rString, rInt, rString) +} + +func testAccAzureRMBastionHost_complete(rInt int, rString string, location string) string { + return fmt.Sprintf(` +resource "azurerm_resource_group" "test" { + name = "acctestRG-bastion-%d" + location = "%s" +} + +resource "azurerm_virtual_network" "test" { + name = "acctestVNet%s" + address_space = ["192.168.1.0/24"] + location = "${azurerm_resource_group.test.location}" + resource_group_name = "${azurerm_resource_group.test.name}" +} + +resource "azurerm_subnet" "test" { + name = "AzureBastionSubnet" + resource_group_name = "${azurerm_resource_group.test.name}" + virtual_network_name = "${azurerm_virtual_network.test.name}" + address_prefix = "192.168.1.224/27" +} + +resource "azurerm_public_ip" "test" { + name = "acctestBastionPIP%d" + location = "${azurerm_resource_group.test.location}" + resource_group_name = "${azurerm_resource_group.test.name}" + allocation_method = "Static" + sku = "Standard" +} + +resource "azurerm_bastion_host" "test" { + name = "acctestBastion%s" + location = "${azurerm_resource_group.test.location}" + resource_group_name = "${azurerm_resource_group.test.name}" + + ip_configuration { + name = "configuration" + subnet_id = "${azurerm_subnet.test.id}" + public_ip_address_id = "${azurerm_public_ip.test.id}" + } + + tags = { + environment = "production" + } +} +`, rInt, location, rString, rInt, rString) +} + +func testAccAzureRMBastionHost_requiresImport(rInt int, rString string, location string) string { + template := testAccAzureRMBastionHost_basic(rInt, rString, location) + return fmt.Sprintf(` +%s +resource "azurerm_bastion_host" "import" { + name = "${azurerm_bastion_host.test.name}" + resource_group_name = "${azurerm_bastion_host.test.resource_group_name}" + location = "${azurerm_bastion_host.test.location}" + + ip_configuration { + name = "configuration" + subnet_id = "${azurerm_subnet.test.id}" + public_ip_address_id = "${azurerm_public_ip.test.id}" + } +} +`, template) +} + +func testCheckAzureRMBastionHostExists(resourceName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + client := testAccProvider.Meta().(*ArmClient).Network.BastionHostsClient + ctx := testAccProvider.Meta().(*ArmClient).StopContext + + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("Not found: %q", resourceName) + } + + name := rs.Primary.Attributes["name"] + resourceGroup := rs.Primary.Attributes["resource_group_name"] + + resp, err := client.Get(ctx, resourceGroup, name) + if err != nil { + if utils.ResponseWasNotFound(resp.Response) { + return fmt.Errorf("Bad: Azure Bastion Host %q does not exist", rs.Primary.ID) + } + return fmt.Errorf("Bad: Get on Azure Bastion Host Client: %+v", err) + } + + return nil + } +} + +func testCheckAzureRMBastionHostDestroy(s *terraform.State) error { + client := testAccProvider.Meta().(*ArmClient).Network.BastionHostsClient + ctx := testAccProvider.Meta().(*ArmClient).StopContext + + for _, rs := range s.RootModule().Resources { + if rs.Type != "azurerm_bastion_host" { + continue + } + + name := rs.Primary.Attributes["name"] + resourceGroup := rs.Primary.Attributes["resource_group_name"] + + resp, err := client.Get(ctx, resourceGroup, name) + if err != nil { + if !utils.ResponseWasNotFound(resp.Response) { + return err + } + } + + return nil + } + + return nil +} diff --git a/go.mod b/go.mod index 027e3a200da5..285de5567f00 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/terraform-providers/terraform-provider-azurerm require ( github.com/Azure/azure-sdk-for-go v33.2.0+incompatible github.com/Azure/go-autorest/autorest v0.9.0 + github.com/Azure/go-autorest/autorest/azure/auth v0.3.0 github.com/Azure/go-autorest/autorest/date v0.2.0 github.com/btubbs/datetime v0.1.0 github.com/davecgh/go-spew v1.1.1 diff --git a/go.sum b/go.sum index 727955c22d1a..b132b355680b 100644 --- a/go.sum +++ b/go.sum @@ -442,6 +442,7 @@ golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20181217174547-8f45f776aaf1/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= diff --git a/website/azurerm.erb b/website/azurerm.erb index a15f20223823..e6c8738ba1c4 100644 --- a/website/azurerm.erb +++ b/website/azurerm.erb @@ -1433,6 +1433,10 @@ azurerm_connection_monitor +