diff --git a/builtin/bins/provider-azurerm/main.go b/builtin/bins/provider-azurerm/main.go new file mode 100644 index 000000000..f81707338 --- /dev/null +++ b/builtin/bins/provider-azurerm/main.go @@ -0,0 +1,12 @@ +package main + +import ( + "github.com/hashicorp/terraform/builtin/providers/azurerm" + "github.com/hashicorp/terraform/plugin" +) + +func main() { + plugin.Serve(&plugin.ServeOpts{ + ProviderFunc: azurerm.Provider, + }) +} diff --git a/builtin/providers/azurerm/config.go b/builtin/providers/azurerm/config.go new file mode 100644 index 000000000..12911512b --- /dev/null +++ b/builtin/providers/azurerm/config.go @@ -0,0 +1,189 @@ +package azurerm + +import ( + "log" + "net/http" + + "github.com/Azure/azure-sdk-for-go/Godeps/_workspace/src/github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/azure-sdk-for-go/arm/compute" + "github.com/Azure/azure-sdk-for-go/arm/network" + "github.com/Azure/azure-sdk-for-go/arm/resources" + "github.com/Azure/azure-sdk-for-go/arm/scheduler" + "github.com/Azure/azure-sdk-for-go/arm/storage" + "github.com/Azure/go-autorest/autorest" +) + +// ArmClient contains the handles to all the specific Azure Resource Manager +// resource classes' respective clients. +type ArmClient struct { + availSetClient compute.AvailabilitySetsClient + usageOpsClient compute.UsageOperationsClient + vmExtensionImageClient compute.VirtualMachineExtensionImagesClient + vmExtensionClient compute.VirtualMachineExtensionsClient + vmImageClient compute.VirtualMachineImagesClient + vmClient compute.VirtualMachinesClient + + appGatewayClient network.ApplicationGatewaysClient + ifaceClient network.InterfacesClient + loadBalancerClient network.LoadBalancersClient + localNetConnClient network.LocalNetworkGatewaysClient + publicIPClient network.PublicIPAddressesClient + secGroupClient network.SecurityGroupsClient + secRuleClient network.SecurityRulesClient + subnetClient network.SubnetsClient + netUsageClient network.UsagesClient + vnetGatewayConnectionsClient network.VirtualNetworkGatewayConnectionsClient + vnetGatewayClient network.VirtualNetworkGatewaysClient + vnetClient network.VirtualNetworksClient + + resourceGroupClient resources.GroupsClient + tagsClient resources.TagsClient + + jobsClient scheduler.JobsClient + jobsCollectionsClient scheduler.JobCollectionsClient + + storageServiceClient storage.AccountsClient + storageUsageClient storage.UsageOperationsClient +} + +func withRequestLogging() autorest.SendDecorator { + return func(s autorest.Sender) autorest.Sender { + return autorest.SenderFunc(func(r *http.Request) (*http.Response, error) { + log.Printf("[DEBUG] Sending Azure RM Request %s to %s\n", r.Method, r.URL) + resp, err := s.Do(r) + log.Printf("[DEBUG] Received Azure RM Request status code %s for %s\n", resp.Status, r.URL) + return resp, err + }) + } +} + +// getArmClient is a helper method which returns a fully instantiated +// *ArmClient based on the Config's current settings. +func (c *Config) getArmClient() (*ArmClient, error) { + spt, err := azure.NewServicePrincipalToken(c.ClientID, c.ClientSecret, c.TenantID, azure.AzureResourceManagerScope) + if err != nil { + return nil, err + } + + // client declarations: + client := ArmClient{} + + // NOTE: these declarations should be left separate for clarity should the + // clients be wished to be configured with custom Responders/PollingModess etc... + asc := compute.NewAvailabilitySetsClient(c.SubscriptionID) + asc.Authorizer = spt + asc.Sender = autorest.CreateSender(withRequestLogging()) + client.availSetClient = asc + + uoc := compute.NewUsageOperationsClient(c.SubscriptionID) + uoc.Authorizer = spt + uoc.Sender = autorest.CreateSender(withRequestLogging()) + client.usageOpsClient = uoc + + vmeic := compute.NewVirtualMachineExtensionImagesClient(c.SubscriptionID) + vmeic.Authorizer = spt + vmeic.Sender = autorest.CreateSender(withRequestLogging()) + client.vmExtensionImageClient = vmeic + + vmec := compute.NewVirtualMachineExtensionsClient(c.SubscriptionID) + vmec.Authorizer = spt + vmec.Sender = autorest.CreateSender(withRequestLogging()) + client.vmExtensionClient = vmec + + vmic := compute.NewVirtualMachineImagesClient(c.SubscriptionID) + vmic.Authorizer = spt + vmic.Sender = autorest.CreateSender(withRequestLogging()) + client.vmImageClient = vmic + + vmc := compute.NewVirtualMachinesClient(c.SubscriptionID) + vmc.Authorizer = spt + vmc.Sender = autorest.CreateSender(withRequestLogging()) + client.vmClient = vmc + + agc := network.NewApplicationGatewaysClient(c.SubscriptionID) + agc.Authorizer = spt + agc.Sender = autorest.CreateSender(withRequestLogging()) + client.appGatewayClient = agc + + ifc := network.NewInterfacesClient(c.SubscriptionID) + ifc.Authorizer = spt + ifc.Sender = autorest.CreateSender(withRequestLogging()) + client.ifaceClient = ifc + + lbc := network.NewLoadBalancersClient(c.SubscriptionID) + lbc.Authorizer = spt + lbc.Sender = autorest.CreateSender(withRequestLogging()) + client.loadBalancerClient = lbc + + lgc := network.NewLocalNetworkGatewaysClient(c.SubscriptionID) + lgc.Authorizer = spt + lgc.Sender = autorest.CreateSender(withRequestLogging()) + client.localNetConnClient = lgc + + pipc := network.NewPublicIPAddressesClient(c.SubscriptionID) + pipc.Authorizer = spt + pipc.Sender = autorest.CreateSender(withRequestLogging()) + client.publicIPClient = pipc + + sgc := network.NewSecurityGroupsClient(c.SubscriptionID) + sgc.Authorizer = spt + sgc.Sender = autorest.CreateSender(withRequestLogging()) + client.secGroupClient = sgc + + src := network.NewSecurityRulesClient(c.SubscriptionID) + src.Authorizer = spt + src.Sender = autorest.CreateSender(withRequestLogging()) + client.secRuleClient = src + + snc := network.NewSubnetsClient(c.SubscriptionID) + snc.Authorizer = spt + snc.Sender = autorest.CreateSender(withRequestLogging()) + client.subnetClient = snc + + vgcc := network.NewVirtualNetworkGatewayConnectionsClient(c.SubscriptionID) + vgcc.Authorizer = spt + vgcc.Sender = autorest.CreateSender(withRequestLogging()) + client.vnetGatewayConnectionsClient = vgcc + + vgc := network.NewVirtualNetworkGatewaysClient(c.SubscriptionID) + vgc.Authorizer = spt + vgc.Sender = autorest.CreateSender(withRequestLogging()) + client.vnetGatewayClient = vgc + + vnc := network.NewVirtualNetworksClient(c.SubscriptionID) + vnc.Authorizer = spt + vnc.Sender = autorest.CreateSender(withRequestLogging()) + client.vnetClient = vnc + + rgc := resources.NewGroupsClient(c.SubscriptionID) + rgc.Authorizer = spt + rgc.Sender = autorest.CreateSender(withRequestLogging()) + client.resourceGroupClient = rgc + + tc := resources.NewTagsClient(c.SubscriptionID) + tc.Authorizer = spt + tc.Sender = autorest.CreateSender(withRequestLogging()) + client.tagsClient = tc + + jc := scheduler.NewJobsClient(c.SubscriptionID) + jc.Authorizer = spt + jc.Sender = autorest.CreateSender(withRequestLogging()) + client.jobsClient = jc + + jcc := scheduler.NewJobCollectionsClient(c.SubscriptionID) + jcc.Authorizer = spt + jcc.Sender = autorest.CreateSender(withRequestLogging()) + client.jobsCollectionsClient = jcc + + ssc := storage.NewAccountsClient(c.SubscriptionID) + ssc.Authorizer = spt + ssc.Sender = autorest.CreateSender(withRequestLogging()) + client.storageServiceClient = ssc + + suc := storage.NewUsageOperationsClient(c.SubscriptionID) + suc.Authorizer = spt + suc.Sender = autorest.CreateSender(withRequestLogging()) + client.storageUsageClient = suc + + return &client, nil +} diff --git a/builtin/providers/azurerm/provider.go b/builtin/providers/azurerm/provider.go new file mode 100644 index 000000000..53c5c97a6 --- /dev/null +++ b/builtin/providers/azurerm/provider.go @@ -0,0 +1,78 @@ +package azurerm + +import ( + "strings" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/terraform" +) + +// Provider returns a terraform.ResourceProvider. +func Provider() terraform.ResourceProvider { + return &schema.Provider{ + Schema: map[string]*schema.Schema{ + "subscription_id": &schema.Schema{ + Type: schema.TypeString, + Required: true, + DefaultFunc: schema.EnvDefaultFunc("ARM_SUBSCRIPTION_ID", ""), + }, + + "client_id": &schema.Schema{ + Type: schema.TypeString, + Required: true, + DefaultFunc: schema.EnvDefaultFunc("ARM_CLIENT_ID", ""), + }, + + "client_secret": &schema.Schema{ + Type: schema.TypeString, + Required: true, + DefaultFunc: schema.EnvDefaultFunc("ARM_CLIENT_SECRET", ""), + }, + + "tenant_id": &schema.Schema{ + Type: schema.TypeString, + Required: true, + DefaultFunc: schema.EnvDefaultFunc("ARM_TENANT_ID", ""), + }, + }, + + ResourcesMap: map[string]*schema.Resource{ + "azurerm_resource_group": resourceArmResourceGroup(), + "azurerm_virtual_network": resourceArmVirtualNetwork(), + }, + + ConfigureFunc: providerConfigure, + } +} + +// Config is the configuration structure used to instantiate a +// new Azure management client. +type Config struct { + ManagementURL string + + SubscriptionID string + ClientID string + ClientSecret string + TenantID string +} + +func providerConfigure(d *schema.ResourceData) (interface{}, error) { + config := Config{ + SubscriptionID: d.Get("subscription_id").(string), + ClientID: d.Get("client_id").(string), + ClientSecret: d.Get("client_secret").(string), + TenantID: d.Get("tenant_id").(string), + } + + client, err := config.getArmClient() + if err != nil { + return nil, err + } + + return client, nil +} + +func azureRMNormalizeLocation(location interface{}) string { + input := location.(string) + return strings.Replace(strings.ToLower(input), " ", "", -1) +} diff --git a/builtin/providers/azurerm/provider_test.go b/builtin/providers/azurerm/provider_test.go new file mode 100644 index 000000000..a26249f58 --- /dev/null +++ b/builtin/providers/azurerm/provider_test.go @@ -0,0 +1,40 @@ +package azurerm + +import ( + "os" + "testing" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/terraform" +) + +var testAccProviders map[string]terraform.ResourceProvider +var testAccProvider *schema.Provider + +func init() { + testAccProvider = Provider().(*schema.Provider) + testAccProviders = map[string]terraform.ResourceProvider{ + "azurerm": testAccProvider, + } +} + +func TestProvider(t *testing.T) { + if err := Provider().(*schema.Provider).InternalValidate(); err != nil { + t.Fatalf("err: %s", err) + } +} + +func TestProvider_impl(t *testing.T) { + var _ terraform.ResourceProvider = Provider() +} + +func testAccPreCheck(t *testing.T) { + subscriptionID := os.Getenv("ARM_SUBSCRIPTION_ID") + clientID := os.Getenv("ARM_CLIENT_ID") + clientSecret := os.Getenv("ARM_CLIENT_SECRET") + tenantID := os.Getenv("ARM_TENANT_ID") + + if subscriptionID == "" || clientID == "" || clientSecret == "" || tenantID == "" { + t.Fatal("ARM_SUBSCRIPTION_ID, ARM_CLIENT_ID, ARM_CLIENT_SECRET and ARM_TENANT_ID must be set for acceptance tests") + } +} diff --git a/builtin/providers/azurerm/resource_arm_resource_group.go b/builtin/providers/azurerm/resource_arm_resource_group.go new file mode 100644 index 000000000..a4304c8d4 --- /dev/null +++ b/builtin/providers/azurerm/resource_arm_resource_group.go @@ -0,0 +1,162 @@ +package azurerm + +import ( + "fmt" + "log" + "net/http" + "regexp" + "strings" + "time" + + "github.com/Azure/azure-sdk-for-go/arm/resources" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceArmResourceGroup() *schema.Resource { + return &schema.Resource{ + Create: resourceArmResourceGroupCreate, + Read: resourceArmResourceGroupRead, + Exists: resourceArmResourceGroupExists, + Delete: resourceArmResourceGroupDelete, + + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validateArmResourceGroupName, + }, + "location": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + StateFunc: azureRMNormalizeLocation, + }, + }, + } +} + +func validateArmResourceGroupName(v interface{}, k string) (ws []string, es []error) { + value := v.(string) + + if len(value) > 80 { + es = append(es, fmt.Errorf("%q may not exceed 80 characters in length", k)) + } + + if strings.HasSuffix(value, ".") { + es = append(es, fmt.Errorf("%q may not end with a period", k)) + } + + if matched := regexp.MustCompile(`[\(\)\.a-zA-Z0-9_-]`).Match([]byte(value)); !matched { + es = append(es, fmt.Errorf("%q may only contain alphanumeric characters, dash, underscores, parentheses and periods", k)) + } + + return +} + +func resourceArmResourceGroupCreate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*ArmClient) + resGroupClient := client.resourceGroupClient + + name := d.Get("name").(string) + location := d.Get("location").(string) + + rg := resources.ResourceGroup{ + Name: &name, + Location: &location, + } + + resp, err := resGroupClient.CreateOrUpdate(name, rg) + if err != nil { + return fmt.Errorf("Error issuing Azure ARM create request for resource group '%s': %s", name, err) + } + + d.SetId(*resp.ID) + + log.Printf("[DEBUG] Waiting for Resource Group (%s) to become available", name) + stateConf := &resource.StateChangeConf{ + Pending: []string{"Accepted"}, + Target: "Succeeded", + Refresh: resourceGroupStateRefreshFunc(client, name), + Timeout: 10 * time.Minute, + } + if _, err := stateConf.WaitForState(); err != nil { + return fmt.Errorf("Error waiting for Resource Group (%s) to become available: %s", name, err) + } + + return resourceArmResourceGroupRead(d, meta) +} + +func resourceArmResourceGroupRead(d *schema.ResourceData, meta interface{}) error { + resGroupClient := meta.(*ArmClient).resourceGroupClient + + id, err := parseAzureResourceID(d.Id()) + if err != nil { + return err + } + name := id.ResourceGroup + + res, err := resGroupClient.Get(name) + if err != nil { + if res.StatusCode == http.StatusNotFound { + d.SetId("") + return nil + } + return fmt.Errorf("Error issuing read request to Azure ARM for resource group '%s': %s", name, err) + } + + d.Set("name", res.Name) + d.Set("location", res.Location) + + return nil +} + +func resourceArmResourceGroupExists(d *schema.ResourceData, meta interface{}) (bool, error) { + resGroupClient := meta.(*ArmClient).resourceGroupClient + + id, err := parseAzureResourceID(d.Id()) + if err != nil { + return false, err + } + name := id.ResourceGroup + + resp, err := resGroupClient.CheckExistence(name) + if err != nil { + if resp.StatusCode != 200 { + return false, err + } + + return true, nil + } + + return true, nil +} + +func resourceArmResourceGroupDelete(d *schema.ResourceData, meta interface{}) error { + resGroupClient := meta.(*ArmClient).resourceGroupClient + + id, err := parseAzureResourceID(d.Id()) + if err != nil { + return err + } + name := id.ResourceGroup + + _, err = resGroupClient.Delete(name) + if err != nil { + return err + } + + return nil +} + +func resourceGroupStateRefreshFunc(client *ArmClient, id string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + res, err := client.resourceGroupClient.Get(id) + if err != nil { + return nil, "", fmt.Errorf("Error issuing read request in resourceGroupStateRefreshFunc to Azure ARM for resource group '%s': %s", id, err) + } + + return res, *res.Properties.ProvisioningState, nil + } +} diff --git a/builtin/providers/azurerm/resource_arm_resource_group_test.go b/builtin/providers/azurerm/resource_arm_resource_group_test.go new file mode 100644 index 000000000..2f7f80ab8 --- /dev/null +++ b/builtin/providers/azurerm/resource_arm_resource_group_test.go @@ -0,0 +1,82 @@ +package azurerm + +import ( + "fmt" + "testing" + + "github.com/Azure/azure-sdk-for-go/core/http" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccAzureRMResourceGroup_basic(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMResourceGroupDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAzureRMResourceGroup_basic, + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMResourceGroupExists("azurerm_resource_group.test"), + ), + }, + }, + }) +} + +func testCheckAzureRMResourceGroupExists(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["name"] + + // Ensure resource group exists in API + conn := testAccProvider.Meta().(*ArmClient).resourceGroupClient + + resp, err := conn.Get(resourceGroup) + if err != nil { + return fmt.Errorf("Bad: Get on resourceGroupClient: %s", err) + } + + if resp.StatusCode == http.StatusNotFound { + return fmt.Errorf("Bad: Virtual Network %q (resource group: %q) does not exist", name, resourceGroup) + } + + return nil + } +} + +func testCheckAzureRMResourceGroupDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*ArmClient).resourceGroupClient + + for _, rs := range s.RootModule().Resources { + if rs.Type != "azurerm_resource_group" { + continue + } + + resourceGroup := rs.Primary.ID + + resp, err := conn.Get(resourceGroup) + if err != nil { + return nil + } + + if resp.StatusCode != http.StatusNotFound { + return fmt.Errorf("Resource Group still exists:\n%#v", resp.Properties) + } + } + + return nil +} + +var testAccAzureRMResourceGroup_basic = ` +resource "azurerm_resource_group" "test" { + name = "acceptanceTestResourceGroup1_basic" + location = "West US" +} +` diff --git a/builtin/providers/azurerm/resource_arm_virtual_network.go b/builtin/providers/azurerm/resource_arm_virtual_network.go new file mode 100644 index 000000000..305af5a76 --- /dev/null +++ b/builtin/providers/azurerm/resource_arm_virtual_network.go @@ -0,0 +1,250 @@ +package azurerm + +import ( + "fmt" + "log" + "net/http" + "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 resourceArmVirtualNetwork() *schema.Resource { + return &schema.Resource{ + Create: resourceArmVirtualNetworkCreate, + Read: resourceArmVirtualNetworkRead, + Update: resourceArmVirtualNetworkCreate, + Delete: resourceArmVirtualNetworkDelete, + + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "address_space": &schema.Schema{ + Type: schema.TypeList, + Required: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + + "dns_servers": &schema.Schema{ + Type: schema.TypeList, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + + "subnet": &schema.Schema{ + Type: schema.TypeSet, + Required: 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, + }, + "security_group": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + }, + }, + Set: resourceAzureSubnetHash, + }, + + "location": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + StateFunc: azureRMNormalizeLocation, + }, + + "resource_group_name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + }, + } +} + +func resourceArmVirtualNetworkCreate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*ArmClient) + vnetClient := client.vnetClient + + log.Printf("[INFO] preparing arguments for Azure ARM virtual network creation.") + + name := d.Get("name").(string) + location := d.Get("location").(string) + resGroup := d.Get("resource_group_name").(string) + + vnet := network.VirtualNetwork{ + Name: &name, + Location: &location, + Properties: getVirtualNetworkProperties(d), + } + + resp, err := vnetClient.CreateOrUpdate(resGroup, name, vnet) + if err != nil { + return err + } + + d.SetId(*resp.ID) + + log.Printf("[DEBUG] Waiting for Virtual Network (%s) to become available", name) + stateConf := &resource.StateChangeConf{ + Pending: []string{"Accepted", "Updating"}, + Target: "Succeeded", + Refresh: virtualNetworkStateRefreshFunc(client, resGroup, name), + Timeout: 10 * time.Minute, + } + if _, err := stateConf.WaitForState(); err != nil { + return fmt.Errorf("Error waiting for Virtual Network (%s) to become available: %s", name, err) + } + + return resourceArmVirtualNetworkRead(d, meta) +} + +func resourceArmVirtualNetworkRead(d *schema.ResourceData, meta interface{}) error { + vnetClient := meta.(*ArmClient).vnetClient + + id, err := parseAzureResourceID(d.Id()) + if err != nil { + return err + } + resGroup := id.ResourceGroup + name := id.Path["virtualNetworks"] + + resp, err := vnetClient.Get(resGroup, name) + if resp.StatusCode == http.StatusNotFound { + d.SetId("") + return nil + } + if err != nil { + return fmt.Errorf("Error making Read request on Azure virtual network %s: %s", name, err) + } + vnet := *resp.Properties + + // update appropriate values + d.Set("address_space", vnet.AddressSpace.AddressPrefixes) + + subnets := &schema.Set{ + F: resourceAzureSubnetHash, + } + + for _, subnet := range *vnet.Subnets { + s := map[string]interface{}{} + + s["name"] = *subnet.Name + s["address_prefix"] = *subnet.Properties.AddressPrefix + if subnet.Properties.NetworkSecurityGroup != nil { + s["security_group"] = *subnet.Properties.NetworkSecurityGroup.ID + } + + subnets.Add(s) + } + d.Set("subnet", subnets) + + dnses := []string{} + for _, dns := range *vnet.DhcpOptions.DNSServers { + dnses = append(dnses, dns) + } + d.Set("dns_servers", dnses) + + return nil +} + +func resourceArmVirtualNetworkDelete(d *schema.ResourceData, meta interface{}) error { + vnetClient := meta.(*ArmClient).vnetClient + + id, err := parseAzureResourceID(d.Id()) + if err != nil { + return err + } + resGroup := id.ResourceGroup + name := id.Path["virtualNetworks"] + + _, err = vnetClient.Delete(resGroup, name) + + return err +} + +func getVirtualNetworkProperties(d *schema.ResourceData) *network.VirtualNetworkPropertiesFormat { + // first; get address space prefixes: + prefixes := []string{} + for _, prefix := range d.Get("address_space").([]interface{}) { + prefixes = append(prefixes, prefix.(string)) + } + + // then; the dns servers: + dnses := []string{} + for _, dns := range d.Get("dns_servers").([]interface{}) { + dnses = append(dnses, dns.(string)) + } + + // then; the subnets: + subnets := []network.Subnet{} + if subs := d.Get("subnet").(*schema.Set); subs.Len() > 0 { + for _, subnet := range subs.List() { + subnet := subnet.(map[string]interface{}) + + name := subnet["name"].(string) + prefix := subnet["address_prefix"].(string) + secGroup := subnet["security_group"].(string) + + var subnetObj network.Subnet + subnetObj.Name = &name + subnetObj.Properties = &network.SubnetPropertiesFormat{} + subnetObj.Properties.AddressPrefix = &prefix + + if secGroup != "" { + subnetObj.Properties.NetworkSecurityGroup = &network.SubResource{ + ID: &secGroup, + } + } + + subnets = append(subnets, subnetObj) + } + } + + // finally; return the struct: + return &network.VirtualNetworkPropertiesFormat{ + AddressSpace: &network.AddressSpace{ + AddressPrefixes: &prefixes, + }, + DhcpOptions: &network.DhcpOptions{ + DNSServers: &dnses, + }, + Subnets: &subnets, + } +} + +func resourceAzureSubnetHash(v interface{}) int { + m := v.(map[string]interface{}) + subnet := m["name"].(string) + m["address_prefix"].(string) + if securityGroup, present := m["security_group"]; present { + subnet = subnet + securityGroup.(string) + } + return hashcode.String(subnet) +} + +func virtualNetworkStateRefreshFunc(client *ArmClient, resourceGroupName string, networkName string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + res, err := client.vnetClient.Get(resourceGroupName, networkName) + if err != nil { + return nil, "", fmt.Errorf("Error issuing read request in virtualNetworkStateRefreshFunc to Azure ARM for virtual network '%s' (RG: '%s'): %s", networkName, resourceGroupName, err) + } + + return res, *res.Properties.ProvisioningState, nil + } +} diff --git a/builtin/providers/azurerm/resource_arm_virtual_network_test.go b/builtin/providers/azurerm/resource_arm_virtual_network_test.go new file mode 100644 index 000000000..41be2fa7d --- /dev/null +++ b/builtin/providers/azurerm/resource_arm_virtual_network_test.go @@ -0,0 +1,100 @@ +package azurerm + +import ( + "fmt" + "testing" + + "github.com/Azure/azure-sdk-for-go/core/http" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccAzureRMVirtualNetwork_basic(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMVirtualNetworkDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAzureRMVirtualNetwork_basic, + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMVirtualNetworkExists("azurerm_virtual_network.test"), + ), + }, + }, + }) +} + +func testCheckAzureRMVirtualNetworkExists(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) + } + + virtualNetworkName := 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 virtual network: %s", virtualNetworkName) + } + + // Ensure resource group/virtual network combination exists in API + conn := testAccProvider.Meta().(*ArmClient).vnetClient + + resp, err := conn.Get(resourceGroup, virtualNetworkName) + if err != nil { + return fmt.Errorf("Bad: Get on vnetClient: %s", err) + } + + if resp.StatusCode == http.StatusNotFound { + return fmt.Errorf("Bad: Virtual Network %q (resource group: %q) does not exist", name, resourceGroup) + } + + return nil + } +} + +func testCheckAzureRMVirtualNetworkDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*ArmClient).vnetClient + + for _, rs := range s.RootModule().Resources { + if rs.Type != "azurerm_virtual_network" { + 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("Virtual Network sitll exists:\n%#v", resp.Properties) + } + } + + return nil +} + +var testAccAzureRMVirtualNetwork_basic = ` +resource "azurerm_resource_group" "test" { + name = "acceptanceTestResourceGroup1" + location = "West US" +} + +resource "azurerm_virtual_network" "test" { + name = "acceptanceTestVirtualNetwork1" + address_space = ["10.0.0.0/16"] + location = "West US" + resource_group_name = "${azurerm_resource_group.test.name}" + + subnet { + name = "subnet1" + address_prefix = "10.0.1.0/24" + } +} +` diff --git a/builtin/providers/azurerm/resourceid.go b/builtin/providers/azurerm/resourceid.go new file mode 100644 index 000000000..fd3e0718c --- /dev/null +++ b/builtin/providers/azurerm/resourceid.go @@ -0,0 +1,82 @@ +package azurerm + +import ( + "fmt" + "net/url" + "strings" +) + +// ResourceID represents a parsed long-form Azure Resource Manager ID +// with the Subscription ID, Resource Group and the Provider as top- +// level fields, and other key-value pairs available via a map in the +// Path field. +type ResourceID struct { + SubscriptionID string + ResourceGroup string + Provider string + Path map[string]string +} + +// parseAzureResourceID converts a long-form Azure Resource Manager ID +// into a ResourceID. We make assumptions about the structure of URLs, +// which is obviously not good, but the best thing available given the +// SDK. +func parseAzureResourceID(id string) (*ResourceID, error) { + idURL, err := url.ParseRequestURI(id) + if err != nil { + return nil, fmt.Errorf("Cannot parse Azure Id: %s", err) + } + + path := idURL.Path + + path = strings.TrimSpace(path) + if strings.HasPrefix(path, "/") { + path = path[1:] + } + + if strings.HasSuffix(path, "/") { + path = path[:len(path)-1] + } + + components := strings.Split(path, "/") + + // We should have an even number of key-value pairs. + if len(components)%2 != 0 { + return nil, fmt.Errorf("The number of path segments is not divisible by 2 in %q", path) + } + + // Put the constituent key-value pairs into a map + componentMap := make(map[string]string, len(components)/2) + for current := 0; current < len(components); current += 2 { + key := components[current] + value := components[current+1] + + componentMap[key] = value + } + + // Build up a ResourceID from the map + idObj := &ResourceID{} + idObj.Path = componentMap + + if subscription, ok := componentMap["subscriptions"]; ok { + idObj.SubscriptionID = subscription + delete(componentMap, "subscriptions") + } else { + return nil, fmt.Errorf("No subscription ID found in: %q", path) + } + + if resourceGroup, ok := componentMap["resourceGroups"]; ok { + idObj.ResourceGroup = resourceGroup + delete(componentMap, "resourceGroups") + } else { + return nil, fmt.Errorf("No resource group name found in: %q", path) + } + + // It is OK not to have a provider in the case of a resource group + if provider, ok := componentMap["providers"]; ok { + idObj.Provider = provider + delete(componentMap, "providers") + } + + return idObj, nil +} diff --git a/builtin/providers/azurerm/resourceid_test.go b/builtin/providers/azurerm/resourceid_test.go new file mode 100644 index 000000000..15caad800 --- /dev/null +++ b/builtin/providers/azurerm/resourceid_test.go @@ -0,0 +1,107 @@ +package azurerm + +import ( + "reflect" + "testing" +) + +func TestParseAzureResourceID(t *testing.T) { + testCases := []struct { + id string + expectedResourceID *ResourceID + expectError bool + }{ + { + "random", + nil, + true, + }, + { + "/subscriptions/6d74bdd2-9f84-11e5-9bd9-7831c1c4c038", + nil, + true, + }, + { + "subscriptions/6d74bdd2-9f84-11e5-9bd9-7831c1c4c038", + nil, + true, + }, + { + "/subscriptions/6d74bdd2-9f84-11e5-9bd9-7831c1c4c038/resourceGroups/testGroup1", + &ResourceID{ + SubscriptionID: "6d74bdd2-9f84-11e5-9bd9-7831c1c4c038", + ResourceGroup: "testGroup1", + Provider: "", + Path: map[string]string{}, + }, + false, + }, + { + "/subscriptions/6d74bdd2-9f84-11e5-9bd9-7831c1c4c038/resourceGroups/testGroup1/providers/Microsoft.Network", + &ResourceID{ + SubscriptionID: "6d74bdd2-9f84-11e5-9bd9-7831c1c4c038", + ResourceGroup: "testGroup1", + Provider: "Microsoft.Network", + Path: map[string]string{}, + }, + false, + }, + { + // Missing leading / + "subscriptions/6d74bdd2-9f84-11e5-9bd9-7831c1c4c038/resourceGroups/testGroup1/providers/Microsoft.Network/virtualNetworks/virtualNetwork1/", + nil, + true, + }, + { + "/subscriptions/6d74bdd2-9f84-11e5-9bd9-7831c1c4c038/resourceGroups/testGroup1/providers/Microsoft.Network/virtualNetworks/virtualNetwork1", + &ResourceID{ + SubscriptionID: "6d74bdd2-9f84-11e5-9bd9-7831c1c4c038", + ResourceGroup: "testGroup1", + Provider: "Microsoft.Network", + Path: map[string]string{ + "virtualNetworks": "virtualNetwork1", + }, + }, + false, + }, + { + "/subscriptions/6d74bdd2-9f84-11e5-9bd9-7831c1c4c038/resourceGroups/testGroup1/providers/Microsoft.Network/virtualNetworks/virtualNetwork1?api-version=2006-01-02-preview", + &ResourceID{ + SubscriptionID: "6d74bdd2-9f84-11e5-9bd9-7831c1c4c038", + ResourceGroup: "testGroup1", + Provider: "Microsoft.Network", + Path: map[string]string{ + "virtualNetworks": "virtualNetwork1", + }, + }, + false, + }, + { + "/subscriptions/6d74bdd2-9f84-11e5-9bd9-7831c1c4c038/resourceGroups/testGroup1/providers/Microsoft.Network/virtualNetworks/virtualNetwork1/subnets/publicInstances1?api-version=2006-01-02-preview", + &ResourceID{ + SubscriptionID: "6d74bdd2-9f84-11e5-9bd9-7831c1c4c038", + ResourceGroup: "testGroup1", + Provider: "Microsoft.Network", + Path: map[string]string{ + "virtualNetworks": "virtualNetwork1", + "subnets": "publicInstances1", + }, + }, + false, + }, + } + + for _, test := range testCases { + parsed, err := parseAzureResourceID(test.id) + if test.expectError && err != nil { + continue + } + if err != nil { + t.Fatalf("Unexpected error: %s", err) + } + + if !reflect.DeepEqual(test.expectedResourceID, parsed) { + t.Fatalf("Unexpected resource ID:\nExpected: %+v\nGot: %+v\n", test.expectedResourceID, parsed) + } + } +} diff --git a/website/Vagrantfile b/website/Vagrantfile index 4bfc410e2..6507bea16 100644 --- a/website/Vagrantfile +++ b/website/Vagrantfile @@ -28,7 +28,7 @@ bundle exec middleman server & SCRIPT Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| - config.vm.box = "chef/ubuntu-12.04" + config.vm.box = "bento/ubuntu-12.04" config.vm.network "private_network", ip: "33.33.30.10" config.vm.provision "shell", inline: $script, privileged: false config.vm.synced_folder ".", "/vagrant", type: "rsync" diff --git a/website/source/assets/stylesheets/_docs.scss b/website/source/assets/stylesheets/_docs.scss index 5f2b07021..49e9e164f 100755 --- a/website/source/assets/stylesheets/_docs.scss +++ b/website/source/assets/stylesheets/_docs.scss @@ -10,6 +10,7 @@ body.layout-atlas, body.layout-aws, body.layout-azure, body.layout-chef, +body.layout-azurerm, body.layout-cloudflare, body.layout-cloudstack, body.layout-consul, diff --git a/website/source/docs/providers/azurerm/index.html.markdown b/website/source/docs/providers/azurerm/index.html.markdown new file mode 100644 index 000000000..0655a4843 --- /dev/null +++ b/website/source/docs/providers/azurerm/index.html.markdown @@ -0,0 +1,80 @@ +--- +layout: "azurerm" +page_title: "Provider: Azure Resource Manager" +sidebar_current: "docs-azurerm-index" +description: |- + The Azure Resource Manager provider is used to interact with the many resources supported by Azure, via the ARM API. This supercedes the Azure provider, which interacts with Azure using the Service Management API. The provider needs to be configured with a credentials file, or credentials needed to generate OAuth tokens for the ARM API. +--- + +# Azure Resource Manager Provider + +The Azure Resource Manager provider is used to interact with the many resources +supported by Azure, via the ARM API. This supercedes the Azure provider, which +interacts with Azure using the Service Management API. The provider needs to be +configured with the credentials needed to generate OAuth tokens for the ARM API. + +Use the navigation to the left to read about the available resources. + +## Example Usage + +``` +# Configure the Azure Resource Manager Provider +provider "azurerm" { + subscription_id = "..." + client_id = "..." + client_secret = "..." + tenant_id = "..." +} + +# Create a resource group +resource "azurerm_resource_group" "production" { + name = "production" + location = "West US" +} + +# Create a virtual network in the web_servers resource group +resource "azurerm_virtual_network" "network" { + name = "productionNetwork" + address_space = ["10.0.0.0/16"] + location = "West US" + resource_group_name = "${azurerm_resource_group.production.name}" + + subnet { + name = "subnet1" + address_prefix = "10.0.1.0/24" + } + + subnet { + name = "subnet2" + address_prefix = "10.0.2.0/24" + } + + subnet { + name = "subnet3" + address_prefix = "10.0.3.0/24" + } +} + +``` + +## Argument Reference + +The following arguments are supported: + +* `subscription_id` - (Optional) The subscription ID to use. It can also + be sourced from the `ARM_SUBSCRIPTION_ID` environment variable. + +* `client_id` - (Optional) The client ID to use. It can also be sourced from + the `ARM_CLIENT_ID` environment variable. + +* `client_secret` - (Optional) The client secret to use. It can also be sourced from + the `ARM_CLIENT_SECRET` environment variable. + +* `tenant_id` - (Optional) The tenant ID to use. It can also be sourced from the + `ARM_TENANT_ID` environment variable. + +## Testing: + +Credentials must be provided via the `ARM_SUBSCRIPTION_ID`, `ARM_CLIENT_ID`, +`ARM_CLIENT_SECRET` and `ARM_TENANT_ID` environment variables in order to run +acceptance tests. diff --git a/website/source/docs/providers/azurerm/r/resource_group.html.markdown b/website/source/docs/providers/azurerm/r/resource_group.html.markdown new file mode 100644 index 000000000..06d6dbb22 --- /dev/null +++ b/website/source/docs/providers/azurerm/r/resource_group.html.markdown @@ -0,0 +1,36 @@ +--- +layout: "azurerm" +page_title: "Azure Resource Manager: azurerm_resource_group" +sidebar_current: "docs-azurerm-resource-resource-group" +description: |- + Creates a new resource group on Azure. +--- + +# azurerm\_resource\_group + +Creates a new resource group on Azure. + +## Example Usage + +``` +resource "azurerm_resource_group" "test" { + name = "testResourceGroup1" + location = "West US" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) The name of the resource group. Must be unique on your + Azure subscription. + +* `location` - (Required) The location where the resource group should be created. + For a list of all Azure locations, please consult [this link](http://azure.microsoft.com/en-us/regions/). + +## Attributes Reference + +The following attributes are exported: + +* `id` - The resource group ID. diff --git a/website/source/docs/providers/azurerm/r/virtual_network.html.markdown b/website/source/docs/providers/azurerm/r/virtual_network.html.markdown new file mode 100644 index 000000000..164ffce4b --- /dev/null +++ b/website/source/docs/providers/azurerm/r/virtual_network.html.markdown @@ -0,0 +1,76 @@ +--- +layout: "azurerm" +page_title: "Azure Resource Manager: azure_virtual_network" +sidebar_current: "docs-azurerm-resource-virtual-network" +description: |- + Creates a new virtual network including any configured subnets. Each subnet can optionally be configured with a security group to be associated with the subnet. +--- + +# azurerm\_virtual\_network + +Creates a new virtual network including any configured subnets. Each subnet can +optionally be configured with a security group to be associated with the subnet. + +## Example Usage + +``` +resource "azurerm_virtual_network" "test" { + name = "virtualNetwork1" + resource_group_name = "${azurerm_resource_group.test.name}" + address_space = ["10.0.0.0/16"] + location = "West US" + + subnet { + name = "subnet1" + address_prefix = "10.0.1.0/24" + } + + subnet { + name = "subnet2" + address_prefix = "10.0.2.0/24" + } + + subnet { + name = "subnet3" + address_prefix = "10.0.3.0/24" + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) The name of the virtual network. Changing this forces a + new resource to be created. + +* `resource_group_name` - (Required) The name of the resource group in which to + create the virtual network. + +* `address_space` - (Required) The address space that is used the virtual + network. You can supply more than one address space. Changing this forces + a new resource to be created. + +* `location` - (Required) The location/region where the virtual network is + created. Changing this forces a new resource to be created. + +* `dns_servers` - (Optional) List of names of DNS servers previously registered + on Azure. + +* `subnet` - (Required) Can be specified multiple times to define multiple + subnets. Each `subnet` block supports fields documented below. + +The `subnet` block supports: + +* `name` - (Required) The name of the subnet. + +* `address_prefix` - (Required) The address prefix to use for the subnet. + +* `security_group` - (Optional) The Network Security Group to associate with + the subnet. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The virtual NetworkConfiguration ID. diff --git a/website/source/layouts/azurerm.erb b/website/source/layouts/azurerm.erb new file mode 100644 index 000000000..f52a1bce2 --- /dev/null +++ b/website/source/layouts/azurerm.erb @@ -0,0 +1,30 @@ +<% wrap_layout :inner do %> + <% content_for :sidebar do %> + + <% end %> + + <%= yield %> +<% end %> diff --git a/website/source/layouts/docs.erb b/website/source/layouts/docs.erb index 3deb5be98..e61f01b87 100644 --- a/website/source/layouts/docs.erb +++ b/website/source/layouts/docs.erb @@ -130,7 +130,11 @@ > - Azure + Azure (Service Management) + + + > + Azure (Resource Manager) >