diff --git a/builtin/providers/azurerm/config.go b/builtin/providers/azurerm/config.go index c766f1fac..20f6f073b 100644 --- a/builtin/providers/azurerm/config.go +++ b/builtin/providers/azurerm/config.go @@ -37,6 +37,7 @@ type ArmClient struct { vnetGatewayConnectionsClient network.VirtualNetworkGatewayConnectionsClient vnetGatewayClient network.VirtualNetworkGatewaysClient vnetClient network.VirtualNetworksClient + routeTablesClient network.RouteTablesClient providers resources.ProvidersClient resourceGroupClient resources.GroupsClient @@ -186,6 +187,12 @@ func (c *Config) getArmClient() (*ArmClient, error) { vnc.Sender = autorest.CreateSender(withRequestLogging()) client.vnetClient = vnc + rtc := network.NewRouteTablesClient(c.SubscriptionID) + setUserAgent(&rtc.Client) + rtc.Authorizer = spt + rtc.Sender = autorest.CreateSender(withRequestLogging()) + client.routeTablesClient = rtc + rgc := resources.NewGroupsClient(c.SubscriptionID) setUserAgent(&rgc.Client) rgc.Authorizer = spt diff --git a/builtin/providers/azurerm/provider.go b/builtin/providers/azurerm/provider.go index e2dea9974..bcf93684c 100644 --- a/builtin/providers/azurerm/provider.go +++ b/builtin/providers/azurerm/provider.go @@ -49,6 +49,7 @@ func Provider() terraform.ResourceProvider { "azurerm_public_ip": resourceArmPublicIp(), "azurerm_subnet": resourceArmSubnet(), "azurerm_network_interface": resourceArmNetworkInterface(), + "azurerm_route_table": resourceArmRouteTable(), }, ConfigureFunc: providerConfigure, } diff --git a/builtin/providers/azurerm/resource_arm_route_table.go b/builtin/providers/azurerm/resource_arm_route_table.go new file mode 100644 index 000000000..daedbafff --- /dev/null +++ b/builtin/providers/azurerm/resource_arm_route_table.go @@ -0,0 +1,252 @@ +package azurerm + +import ( + "bytes" + "fmt" + "log" + "net/http" + "strings" + "time" + + "github.com/Azure/azure-sdk-for-go/arm/network" + "github.com/hashicorp/terraform/helper/hashcode" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceArmRouteTable() *schema.Resource { + return &schema.Resource{ + Create: resourceArmRouteTableCreate, + Read: resourceArmRouteTableRead, + Update: resourceArmRouteTableCreate, + Delete: resourceArmRouteTableDelete, + + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "location": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + StateFunc: azureRMNormalizeLocation, + }, + + "resource_group_name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "route": &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + + "address_prefix": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + + "next_hop_type": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ValidateFunc: validateRouteTableNextHopType, + }, + + "next_hop_in_ip_address": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + }, + }, + Set: resourceArmRouteTableRouteHash, + }, + + "subnets": &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + }, + }, + } +} + +func resourceArmRouteTableCreate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*ArmClient) + routeTablesClient := client.routeTablesClient + + log.Printf("[INFO] preparing arguments for Azure ARM Route Table creation.") + + name := d.Get("name").(string) + location := d.Get("location").(string) + resGroup := d.Get("resource_group_name").(string) + + routeSet := network.RouteTable{ + Name: &name, + Location: &location, + } + + if _, ok := d.GetOk("route"); ok { + properties := network.RouteTablePropertiesFormat{} + routes, routeErr := expandAzureRmRouteTableRoutes(d) + if routeErr != nil { + return fmt.Errorf("Error Building list of Route Table Routes: %s", routeErr) + } + if len(routes) > 0 { + routeSet.Properties = &properties + } + + } + + resp, err := routeTablesClient.CreateOrUpdate(resGroup, name, routeSet) + if err != nil { + return err + } + + d.SetId(*resp.ID) + + log.Printf("[DEBUG] Waiting for Route Table (%s) to become available", name) + stateConf := &resource.StateChangeConf{ + Pending: []string{"Accepted", "Updating"}, + Target: "Succeeded", + Refresh: routeTableStateRefreshFunc(client, resGroup, name), + Timeout: 10 * time.Minute, + } + if _, err := stateConf.WaitForState(); err != nil { + return fmt.Errorf("Error waiting forRoute Table (%s) to become available: %s", name, err) + } + + return resourceArmRouteTableRead(d, meta) +} + +func resourceArmRouteTableRead(d *schema.ResourceData, meta interface{}) error { + routeTablesClient := meta.(*ArmClient).routeTablesClient + + id, err := parseAzureResourceID(d.Id()) + if err != nil { + return err + } + resGroup := id.ResourceGroup + name := id.Path["routeTables"] + + resp, err := routeTablesClient.Get(resGroup, name, "") + if resp.StatusCode == http.StatusNotFound { + d.SetId("") + return nil + } + if err != nil { + return fmt.Errorf("Error making Read request on Azure Route Table %s: %s", name, err) + } + + if resp.Properties.Subnets != nil { + if len(*resp.Properties.Subnets) > 0 { + subnets := make([]string, 0, len(*resp.Properties.Subnets)) + for _, subnet := range *resp.Properties.Subnets { + id := subnet.ID + subnets = append(subnets, *id) + } + + if err := d.Set("subnets", subnets); err != nil { + return err + } + } + } + + return nil +} + +func resourceArmRouteTableDelete(d *schema.ResourceData, meta interface{}) error { + routeTablesClient := meta.(*ArmClient).routeTablesClient + + id, err := parseAzureResourceID(d.Id()) + if err != nil { + return err + } + resGroup := id.ResourceGroup + name := id.Path["routeTables"] + + _, err = routeTablesClient.Delete(resGroup, name) + + return err +} + +func expandAzureRmRouteTableRoutes(d *schema.ResourceData) ([]network.Route, error) { + configs := d.Get("route").(*schema.Set).List() + routes := make([]network.Route, 0, len(configs)) + + for _, configRaw := range configs { + data := configRaw.(map[string]interface{}) + + address_prefix := data["address_prefix"].(string) + next_hop_type := data["next_hop_type"].(string) + + properties := network.RoutePropertiesFormat{ + AddressPrefix: &address_prefix, + NextHopType: network.RouteNextHopType(next_hop_type), + } + + if v := data["next_hop_in_ip_address"].(string); v != "" { + properties.NextHopIPAddress = &v + } + + name := data["name"].(string) + route := network.Route{ + Name: &name, + Properties: &properties, + } + + routes = append(routes, route) + } + + return routes, nil +} + +func routeTableStateRefreshFunc(client *ArmClient, resourceGroupName string, routeTableName string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + res, err := client.routeTablesClient.Get(resourceGroupName, routeTableName, "") + if err != nil { + return nil, "", fmt.Errorf("Error issuing read request in routeTableStateRefreshFunc to Azure ARM for route table '%s' (RG: '%s'): %s", routeTableName, resourceGroupName, err) + } + + return res, *res.Properties.ProvisioningState, nil + } +} + +func resourceArmRouteTableRouteHash(v interface{}) int { + var buf bytes.Buffer + m := v.(map[string]interface{}) + buf.WriteString(fmt.Sprintf("%s-", m["name"].(string))) + buf.WriteString(fmt.Sprintf("%s-", m["address_prefix"].(string))) + buf.WriteString(fmt.Sprintf("%s-", m["next_hop_type"].(string))) + + return hashcode.String(buf.String()) +} + +func validateRouteTableNextHopType(v interface{}, k string) (ws []string, errors []error) { + value := strings.ToLower(v.(string)) + hopTypes := map[string]bool{ + "virtualnetworkgateway": true, + "vnetlocal": true, + "internet": true, + "virtualappliance": true, + "null": true, + } + + if !hopTypes[value] { + errors = append(errors, fmt.Errorf("Route Table NextHopType Protocol can only be VirtualNetworkGateway, VnetLocal, Internet or VirtualAppliance")) + } + return +} diff --git a/builtin/providers/azurerm/resource_arm_route_table_test.go b/builtin/providers/azurerm/resource_arm_route_table_test.go new file mode 100644 index 000000000..8df04e3b2 --- /dev/null +++ b/builtin/providers/azurerm/resource_arm_route_table_test.go @@ -0,0 +1,201 @@ +package azurerm + +import ( + "fmt" + "net/http" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestResourceAzureRMRouteTableNextHopType_validation(t *testing.T) { + cases := []struct { + Value string + ErrCount int + }{ + { + Value: "Random", + ErrCount: 1, + }, + { + Value: "VirtualNetworkGateway", + ErrCount: 0, + }, + { + Value: "VNETLocal", + ErrCount: 0, + }, + { + Value: "Internet", + ErrCount: 0, + }, + { + Value: "VirtualAppliance", + ErrCount: 0, + }, + { + Value: "Null", + ErrCount: 0, + }, + { + Value: "VIRTUALNETWORKGATEWAY", + ErrCount: 0, + }, + { + Value: "virtualnetworkgateway", + ErrCount: 0, + }, + } + + for _, tc := range cases { + _, errors := validateRouteTableNextHopType(tc.Value, "azurerm_route_table") + + if len(errors) != tc.ErrCount { + t.Fatalf("Expected the Azure RM Route Table nextHopType to trigger a validation error") + } + } +} + +func TestAccAzureRMRouteTable_basic(t *testing.T) { + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMRouteTableDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAzureRMRouteTable_basic, + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMRouteTableExists("azurerm_route_table.test"), + ), + }, + }, + }) +} + +func TestAccAzureRMRouteTable_multipleRoutes(t *testing.T) { + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMRouteTableDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAzureRMRouteTable_basic, + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMRouteTableExists("azurerm_route_table.test"), + resource.TestCheckResourceAttr( + "azurerm_route_table.test", "route.#", "1"), + ), + }, + + resource.TestStep{ + Config: testAccAzureRMRouteTable_multipleRoutes, + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMRouteTableExists("azurerm_route_table.test"), + resource.TestCheckResourceAttr( + "azurerm_route_table.test", "route.#", "2"), + ), + }, + }, + }) +} + +func testCheckAzureRMRouteTableExists(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) + } + + name := rs.Primary.Attributes["name"] + resourceGroup, hasResourceGroup := rs.Primary.Attributes["resource_group_name"] + if !hasResourceGroup { + return fmt.Errorf("Bad: no resource group found in state for route table: %s", name) + } + + conn := testAccProvider.Meta().(*ArmClient).routeTablesClient + + resp, err := conn.Get(resourceGroup, name, "") + if err != nil { + return fmt.Errorf("Bad: Get on routeTablesClient: %s", err) + } + + if resp.StatusCode == http.StatusNotFound { + return fmt.Errorf("Bad: Route Table %q (resource group: %q) does not exist", name, resourceGroup) + } + + return nil + } +} + +func testCheckAzureRMRouteTableDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*ArmClient).routeTablesClient + + for _, rs := range s.RootModule().Resources { + if rs.Type != "azurerm_route_table" { + continue + } + + name := rs.Primary.Attributes["name"] + resourceGroup := rs.Primary.Attributes["resource_group_name"] + + resp, err := conn.Get(resourceGroup, name, "") + + if err != nil { + return nil + } + + if resp.StatusCode != http.StatusNotFound { + return fmt.Errorf("Route Table still exists:\n%#v", resp.Properties) + } + } + + return nil +} + +var testAccAzureRMRouteTable_basic = ` +resource "azurerm_resource_group" "test" { + name = "acceptanceTestResourceGroup1" + location = "West US" +} + +resource "azurerm_route_table" "test" { + name = "acceptanceTestSecurityGroup1" + location = "West US" + resource_group_name = "${azurerm_resource_group.test.name}" + + route { + name = "route1" + address_prefix = "*" + next_hop_type = "internet" + } +} +` + +var testAccAzureRMRouteTable_multipleRoutes = ` +resource "azurerm_resource_group" "test" { + name = "acceptanceTestResourceGroup1" + location = "West US" +} + +resource "azurerm_route_table" "test" { + name = "acceptanceTestSecurityGroup1" + location = "West US" + resource_group_name = "${azurerm_resource_group.test.name}" + + route { + name = "route1" + address_prefix = "*" + next_hop_type = "internet" + } + + route { + name = "route2" + address_prefix = "*" + next_hop_type = "virtualappliance" + } +} +` diff --git a/website/source/docs/providers/azurerm/r/route_table.html.markdown b/website/source/docs/providers/azurerm/r/route_table.html.markdown new file mode 100644 index 000000000..b75df1346 --- /dev/null +++ b/website/source/docs/providers/azurerm/r/route_table.html.markdown @@ -0,0 +1,65 @@ +--- +layout: "azurerm" +page_title: "Azure Resource Manager: azurerm_route_table" +sidebar_current: "docs-azurerm-resource-network-route-table" +description: |- + Creates a new Route Table Resource +--- + +# azurerm\_route\_table + +Creates a new Route Table Resource + +## Example Usage + +``` +resource "azurerm_resource_group" "test" { + name = "acceptanceTestResourceGroup1" + location = "West US" +} + +resource "azurerm_route_table" "test" { + name = "acceptanceTestSecurityGroup1" + location = "West US" + resource_group_name = "${azurerm_resource_group.test.name}" + + route { + name = "route1" + address_prefix = "*" + next_hop_type = "internet" + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) The name of the route table. Changing this forces a + new resource to be created. + +* `resource_group_name` - (Required) The name of the resource group in which to + create the route table. + +* `location` - (Required) Specifies the supported Azure location where the resource exists. Changing this forces a new resource to be created. + +* `route` - (Optional) Can be specified multiple times to define multiple + routes. Each `route` block supports fields documented below. + +The `route` block supports: + +* `name` - (Required) The name of the route. + +* `address_prefix` - (Required) The destination CIDR to which the route applies, such as 10.1.0.0/16 + +* `next_hop_type` - (Required) The type of Azure hop the packet should be sent to. + Possible values are VirtualNetworkGateway, VnetLocal, Internet, VirtualAppliance and None + +* `next_hop_in_ip_address` - (Optional) Contains the IP address packets should be forwarded to. Next hop values are only allowed in routes where the next hop type is VirtualAppliance. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The Route Table ID. +* `subnets` - The collection of Subnets associated with this route table. diff --git a/website/source/layouts/azurerm.erb b/website/source/layouts/azurerm.erb index 155e8058a..8542fe625 100644 --- a/website/source/layouts/azurerm.erb +++ b/website/source/layouts/azurerm.erb @@ -47,6 +47,10 @@ azurerm_network_interface + > + azurerm_route_table + +