From c279adfc55e4126756529789ff648a1620d4ab49 Mon Sep 17 00:00:00 2001 From: Nashwan Azhari Date: Tue, 8 Dec 2015 17:30:12 -0500 Subject: [PATCH 1/3] provider/azurerm: Initial commit. This commit brings some of the work over from #3808, but rearchitects to use a separate provider for Azure Resource Manager. This is in line with the decisions made by the Azure Powershell Cmdlets, and is important for usability since the sets of required fields change between the ASM and ARM APIs. Currently `azurerm_resource_group` and `azurerm_virtual_network` are implemented, more resources will follow. --- builtin/bins/provider-azurerm/main.go | 12 + builtin/bins/provider-azurerm/main_test.go | 1 + builtin/providers/azurerm/config.go | 227 +++++++++++++++ builtin/providers/azurerm/provider.go | 108 +++++++ builtin/providers/azurerm/provider_test.go | 195 +++++++++++++ .../azurerm/resourceArmResourceGroup.go | 140 +++++++++ .../azurerm/resourceArmVirtualNetwork.go | 270 ++++++++++++++++++ 7 files changed, 953 insertions(+) create mode 100644 builtin/bins/provider-azurerm/main.go create mode 100644 builtin/bins/provider-azurerm/main_test.go create mode 100644 builtin/providers/azurerm/config.go create mode 100644 builtin/providers/azurerm/provider.go create mode 100644 builtin/providers/azurerm/provider_test.go create mode 100644 builtin/providers/azurerm/resourceArmResourceGroup.go create mode 100644 builtin/providers/azurerm/resourceArmVirtualNetwork.go 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/bins/provider-azurerm/main_test.go b/builtin/bins/provider-azurerm/main_test.go new file mode 100644 index 000000000..06ab7d0f9 --- /dev/null +++ b/builtin/bins/provider-azurerm/main_test.go @@ -0,0 +1 @@ +package main diff --git a/builtin/providers/azurerm/config.go b/builtin/providers/azurerm/config.go new file mode 100644 index 000000000..669e4631d --- /dev/null +++ b/builtin/providers/azurerm/config.go @@ -0,0 +1,227 @@ +package azurerm + +import ( + "encoding/json" + "fmt" + + "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/hashicorp/terraform/helper/pathorcontents" +) + +// 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 +} + +// getArmClient is a helper method which returns a fully instantiated +// *ArmClient based on the Config's current settings. +func (c *Config) getArmClient() (*ArmClient, error) { + // first; check that all the necessary credentials were provided: + if !c._armCredentialsProvided() { + return nil, fmt.Errorf("Not all ARM-required fields have been provided.") + } + + 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 + client.availSetClient = asc + + uoc := compute.NewUsageOperationsClient(c.SubscriptionID) + uoc.Authorizer = spt + client.usageOpsClient = uoc + + vmeic := compute.NewVirtualMachineExtensionImagesClient(c.SubscriptionID) + vmeic.Authorizer = spt + client.vmExtensionImageClient = vmeic + + vmec := compute.NewVirtualMachineExtensionsClient(c.SubscriptionID) + vmec.Authorizer = spt + client.vmExtensionClient = vmec + + vmic := compute.NewVirtualMachineImagesClient(c.SubscriptionID) + vmic.Authorizer = spt + client.vmImageClient = vmic + + vmc := compute.NewVirtualMachinesClient(c.SubscriptionID) + vmc.Authorizer = spt + client.vmClient = vmc + + agc := network.NewApplicationGatewaysClient(c.SubscriptionID) + agc.Authorizer = spt + client.appGatewayClient = agc + + ifc := network.NewInterfacesClient(c.SubscriptionID) + ifc.Authorizer = spt + client.ifaceClient = ifc + + lbc := network.NewLoadBalancersClient(c.SubscriptionID) + lbc.Authorizer = spt + client.loadBalancerClient = lbc + + lgc := network.NewLocalNetworkGatewaysClient(c.SubscriptionID) + lgc.Authorizer = spt + client.localNetConnClient = lgc + + pipc := network.NewPublicIPAddressesClient(c.SubscriptionID) + pipc.Authorizer = spt + client.publicIPClient = pipc + + sgc := network.NewSecurityGroupsClient(c.SubscriptionID) + sgc.Authorizer = spt + client.secGroupClient = sgc + + src := network.NewSecurityRulesClient(c.SubscriptionID) + src.Authorizer = spt + client.secRuleClient = src + + snc := network.NewSubnetsClient(c.SubscriptionID) + snc.Authorizer = spt + client.subnetClient = snc + + vgcc := network.NewVirtualNetworkGatewayConnectionsClient(c.SubscriptionID) + vgcc.Authorizer = spt + client.vnetGatewayConnectionsClient = vgcc + + vgc := network.NewVirtualNetworkGatewaysClient(c.SubscriptionID) + vgc.Authorizer = spt + client.vnetGatewayClient = vgc + + vnc := network.NewVirtualNetworksClient(c.SubscriptionID) + vnc.Authorizer = spt + client.vnetClient = vnc + + rgc := resources.NewGroupsClient(c.SubscriptionID) + rgc.Authorizer = spt + client.resourceGroupClient = rgc + + tc := resources.NewTagsClient(c.SubscriptionID) + tc.Authorizer = spt + client.tagsClient = tc + + jc := scheduler.NewJobsClient(c.SubscriptionID) + jc.Authorizer = spt + client.jobsClient = jc + + jcc := scheduler.NewJobCollectionsClient(c.SubscriptionID) + jcc.Authorizer = spt + client.jobsCollectionsClient = jcc + + ssc := storage.NewAccountsClient(c.SubscriptionID) + ssc.Authorizer = spt + client.storageServiceClient = ssc + + suc := storage.NewUsageOperationsClient(c.SubscriptionID) + suc.Authorizer = spt + client.storageUsageClient = suc + + return &client, nil +} + +// armCredentialsProvided is a helper method which indicates whether or not the +// credentials required for authenticating against the ARM APIs were provided. +func (c *Config) armCredentialsProvided() bool { + return c.ArmConfig != "" || c._armCredentialsProvided() +} +func (c *Config) _armCredentialsProvided() bool { + return !(c.SubscriptionID == "" || c.ClientID == "" || c.ClientSecret == "" || c.TenantID == "") +} + +// readArmSettings is a helper method which; given the contents of the ARM +// credentials file, loads all the data into the Config. +func (c *Config) readArmSettings(contents string) error { + data := &armConfigData{} + err := json.Unmarshal([]byte(contents), data) + + c.SubscriptionID = data.SubscriptionID + c.ClientID = data.ClientID + c.ClientSecret = data.ClientSecret + c.TenantID = data.TenantID + + return err +} + +// configFileContentsWarning represents the warning message returned when the +// path to the 'arm_config_file' is provided instead of its sourced contents. +var configFileContentsWarning = ` +The path to the 'arm_config_file' was provided instead of its contents. +Support for accepting filepaths instead of their contents will be removed +in the near future. Do please consider switching over to using +'${file("/path/to/config.arm")}' instead. +`[1:] + +// validateArmConfigFile is a helper function which verifies that +// the provided ARM configuration file is valid. +func validateArmConfigFile(v interface{}, _ string) (ws []string, es []error) { + value := v.(string) + if value == "" { + return nil, nil + } + + pathOrContents, wasPath, err := pathorcontents.Read(v.(string)) + if err != nil { + es = append(es, fmt.Errorf("Error reading 'arm_config_file': %s", err)) + } + + if wasPath { + ws = append(ws, configFileContentsWarning) + } + + data := armConfigData{} + err = json.Unmarshal([]byte(pathOrContents), &data) + if err != nil { + es = append(es, fmt.Errorf("Error unmarshalling the provided 'arm_config_file': %s", err)) + } + + return +} + +// armConfigData is a private struct which represents the expected layout of +// an ARM configuration file. It is used for unmarshalling purposes. +type armConfigData struct { + ClientID string `json:"clientID"` + ClientSecret string `json:"clientSecret"` + SubscriptionID string `json:"subscriptionID"` + TenantID string `json:"tenantID"` +} diff --git a/builtin/providers/azurerm/provider.go b/builtin/providers/azurerm/provider.go new file mode 100644 index 000000000..8b559c0b1 --- /dev/null +++ b/builtin/providers/azurerm/provider.go @@ -0,0 +1,108 @@ +package azurerm + +import ( + "fmt" + "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{ + "arm_config_file": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Default: "", + DefaultFunc: schema.EnvDefaultFunc("ARM_CONFIG_FILE", nil), + ValidateFunc: validateArmConfigFile, + }, + + "subscription_id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + DefaultFunc: schema.EnvDefaultFunc("AZURE_SUBSCRIPTION_ID", ""), + }, + + "client_id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + DefaultFunc: schema.EnvDefaultFunc("ARM_CLIENT_ID", ""), + }, + + "client_secret": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + DefaultFunc: schema.EnvDefaultFunc("ARM_CLIENT_SECRET", ""), + }, + + "tenant_id": &schema.Schema{ + Type: schema.TypeString, + Optional: 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 + + ArmConfig string + + SubscriptionID string + ClientID string + ClientSecret string + TenantID string +} + +const noConfigError = `Credentials must be provided either via arm_config_file, or via +subscription_id, client_id, client_secret and tenant_id. Please see +the provider documentation for more information on how to obtain these +credentials.` + +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), + } + + // check if credentials file is provided: + armConfig := d.Get("arm_config_file").(string) + if armConfig != "" { + // then, load the settings from that: + if err := config.readArmSettings(armConfig); err != nil { + return nil, err + } + } + + // then; check whether the ARM credentials were provided: + if !config.armCredentialsProvided() { + return nil, fmt.Errorf(noConfigError) + } + + 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..333d8fa3e --- /dev/null +++ b/builtin/providers/azurerm/provider_test.go @@ -0,0 +1,195 @@ +package azurerm + +import ( + "io" + "io/ioutil" + "math/rand" + "os" + "strings" + "testing" + "time" + + "github.com/hashicorp/terraform/config" + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/terraform" + "github.com/mitchellh/go-homedir" +) + +var testAccProviders map[string]terraform.ResourceProvider +var testAccProvider *schema.Provider + +const ( + testAccSecurityGroupName = "terraform-security-group" + testAccHostedServiceName = "terraform-testing-service" +) + +// testAccStorageServiceName is used as the name for the Storage Service +// created in all storage-related tests. +// It is much more convenient to provide a Storage Service which +// has been created beforehand as the creation of one takes a lot +// and would greatly impede the multitude of tests which rely on one. +// NOTE: the storage container should be located in `West US`. +var testAccStorageServiceName = os.Getenv("AZURE_STORAGE") + +const testAccStorageContainerName = "terraform-testing-container" + +func init() { + testAccProvider = Provider().(*schema.Provider) + testAccProviders = map[string]terraform.ResourceProvider{ + "azure": 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) { + if v := os.Getenv("AZURE_PUBLISH_SETTINGS"); v == "" { + subscriptionID := os.Getenv("AZURE_SUBSCRIPTION_ID") + certificate := os.Getenv("AZURE_CERTIFICATE") + + if subscriptionID == "" || certificate == "" { + t.Fatal("either AZURE_PUBLISH_SETTINGS, or AZURE_SUBSCRIPTION_ID " + + "and AZURE_CERTIFICATE must be set for acceptance tests") + } + } + + if v := os.Getenv("AZURE_STORAGE"); v == "" { + t.Fatal("AZURE_STORAGE must be set for acceptance tests") + } +} + +func TestAzure_validateSettingsFile(t *testing.T) { + f, err := ioutil.TempFile("", "tf-test") + if err != nil { + t.Fatalf("Error creating temporary file in TestAzure_validateSettingsFile: %s", err) + } + defer os.Remove(f.Name()) + + fx, err := ioutil.TempFile("", "tf-test-xml") + if err != nil { + t.Fatalf("Error creating temporary file with XML in TestAzure_validateSettingsFile: %s", err) + } + defer os.Remove(fx.Name()) + _, err = io.WriteString(fx, "") + if err != nil { + t.Fatalf("Error writing XML File: %s", err) + } + fx.Close() + + home, err := homedir.Dir() + if err != nil { + t.Fatalf("Error fetching homedir: %s", err) + } + fh, err := ioutil.TempFile(home, "tf-test-home") + if err != nil { + t.Fatalf("Error creating homedir-based temporary file: %s", err) + } + defer os.Remove(fh.Name()) + _, err = io.WriteString(fh, "") + if err != nil { + t.Fatalf("Error writing XML File: %s", err) + } + fh.Close() + + r := strings.NewReplacer(home, "~") + homePath := r.Replace(fh.Name()) + + cases := []struct { + Input string // String of XML or a path to an XML file + W int // expected count of warnings + E int // expected count of errors + }{ + {"test", 0, 1}, + {f.Name(), 1, 1}, + {fx.Name(), 1, 0}, + {homePath, 1, 0}, + {"", 0, 0}, + } + + for _, tc := range cases { + w, e := validateSettingsFile(tc.Input, "") + + if len(w) != tc.W { + t.Errorf("Error in TestAzureValidateSettingsFile: input: %s , warnings: %v, errors: %v", tc.Input, w, e) + } + if len(e) != tc.E { + t.Errorf("Error in TestAzureValidateSettingsFile: input: %s , warnings: %v, errors: %v", tc.Input, w, e) + } + } +} + +func TestAzure_providerConfigure(t *testing.T) { + home, err := homedir.Dir() + if err != nil { + t.Fatalf("Error fetching homedir: %s", err) + } + fh, err := ioutil.TempFile(home, "tf-test-home") + if err != nil { + t.Fatalf("Error creating homedir-based temporary file: %s", err) + } + defer os.Remove(fh.Name()) + + _, err = io.WriteString(fh, testAzurePublishSettingsStr) + if err != nil { + t.Fatalf("err: %s", err) + } + fh.Close() + + r := strings.NewReplacer(home, "~") + homePath := r.Replace(fh.Name()) + + cases := []struct { + SettingsFile string // String of XML or a path to an XML file + NilMeta bool // whether meta is expected to be nil + }{ + {testAzurePublishSettingsStr, false}, + {homePath, false}, + } + + for _, tc := range cases { + rp := Provider() + raw := map[string]interface{}{ + "settings_file": tc.SettingsFile, + } + + rawConfig, err := config.NewRawConfig(raw) + if err != nil { + t.Fatalf("err: %s", err) + } + + err = rp.Configure(terraform.NewResourceConfig(rawConfig)) + meta := rp.(*schema.Provider).Meta() + if (meta == nil) != tc.NilMeta { + t.Fatalf("expected NilMeta: %t, got meta: %#v, settings_file: %q", + tc.NilMeta, meta, tc.SettingsFile) + } + } +} + +func genRandInt() int { + return rand.New(rand.NewSource(time.Now().UnixNano())).Int() % 100000 +} + +// testAzurePublishSettingsStr is a revoked publishsettings file +const testAzurePublishSettingsStr = ` + + + + + + +` diff --git a/builtin/providers/azurerm/resourceArmResourceGroup.go b/builtin/providers/azurerm/resourceArmResourceGroup.go new file mode 100644 index 000000000..2e321aab6 --- /dev/null +++ b/builtin/providers/azurerm/resourceArmResourceGroup.go @@ -0,0 +1,140 @@ +package azurerm + +import ( + "fmt" + "log" + "time" + + "github.com/Azure/azure-sdk-for-go/arm/resources" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/helper/schema" +) + +// resourceArmResourceGroup returns the *schema.Resource +// associated to resource group resources on ARM. +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, + //TODO(jen20) - implement validation func: {resource-group-name} must uniquely identify the resource group within the subscription. It must be no longer than 80 characters long. It can only contain alphanumeric characters, dash, underscore, opening parenthesis, closing parenthesis or period. The name cannot end with a period. + }, + "location": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + StateFunc: azureRMNormalizeLocation, + }, + }, + } +} + +// resourceArmResourceGroupCreate goes ahead and creates the specified ARM resource group. +func resourceArmResourceGroupCreate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*ArmClient) + resGroupClient := client.resourceGroupClient + + name := d.Get("name").(string) + location := d.Get("location").(string) + + log.Printf("[INFO] Issuing Azure ARM creation request for resource group '%s'.", name) + + rg := resources.ResourceGroup{ + Name: &name, + Location: &location, + } + + _, 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(*rg.Name) + + // Wait for the resource group to become available + // TODO(jen20): Is there any need for this? + log.Printf("[DEBUG] Waiting for Resource Group (%s) to become available", d.Id()) + stateConf := &resource.StateChangeConf{ + Pending: []string{"Accepted"}, + Target: "Succeeded", + Refresh: resourceGroupStateRefreshFunc(client, d.Id()), + Timeout: 10 * time.Minute, + } + if _, err := stateConf.WaitForState(); err != nil { + return fmt.Errorf("Error waiting for Resource Group (%s) to become available: %s", d.Id(), err) + } + + return resourceArmResourceGroupRead(d, meta) +} + +// resourceArmResourceGroupRead goes ahead and reads the state of the corresponding ARM resource group. +func resourceArmResourceGroupRead(d *schema.ResourceData, meta interface{}) error { + resGroupClient := meta.(*ArmClient).resourceGroupClient + + name := d.Id() + log.Printf("[INFO] Issuing read request to Azure ARM for resource group '%s'.", name) + + res, err := resGroupClient.Get(name) + if err != 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 +} + +// resourceArmResourceGroupExists goes ahead and checks for the existence of the correspoding ARM resource group. +func resourceArmResourceGroupExists(d *schema.ResourceData, meta interface{}) (bool, error) { + resGroupClient := meta.(*ArmClient).resourceGroupClient + + name := d.Id() + + resp, err := resGroupClient.CheckExistence(name) + if err != nil { + // TODO(aznashwan): implement some error switching helpers in the SDK + // to avoid HTTP error checks such as the below: + if resp.StatusCode != 200 { + return false, err + } + + return true, nil + } + + return true, nil +} + +// resourceArmResourceGroupDelete deletes the specified ARM resource group. +func resourceArmResourceGroupDelete(d *schema.ResourceData, meta interface{}) error { + resGroupClient := meta.(*ArmClient).resourceGroupClient + + name := d.Id() + + _, err := resGroupClient.Delete(name) + if err != nil { + return err + } + + return nil +} + +// resourceGroupStateRefreshFunc returns a resource.StateRefreshFunc that is used to watch +// a resource group. +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/resourceArmVirtualNetwork.go b/builtin/providers/azurerm/resourceArmVirtualNetwork.go new file mode 100644 index 000000000..c4471b37b --- /dev/null +++ b/builtin/providers/azurerm/resourceArmVirtualNetwork.go @@ -0,0 +1,270 @@ +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: resourceArmVirtualNetworkUpdate, + 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_names": &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, + }, + }, + } +} + +// resourceArmVirtualNetworkCreate creates the specified ARM virtual network. +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), + } + + log.Printf("[INFO] Sending virtual network create request to ARM.") + _, err := vnetClient.CreateOrUpdate(resGroup, name, vnet) + if err != nil { + return err + } + + // if res.Response.StatusCode != http.StatusAccepted { + // return fmt.Errorf("Creation request was denies: code: %d", res.Response.StatusCode) + // } + + d.SetId(name) + d.Set("resGroup", resGroup) + + // Wait for the resource group to become available + // TODO(jen20): Is there any need for this? + log.Printf("[DEBUG] Waiting for Virtual Network (%s) to become available", d.Id()) + 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", d.Id(), err) + } + + return resourceArmVirtualNetworkRead(d, meta) +} + +// resourceArmVirtualNetworkRead goes ahead and reads the state of the corresponding ARM virtual network. +func resourceArmVirtualNetworkRead(d *schema.ResourceData, meta interface{}) error { + vnetClient := meta.(*ArmClient).vnetClient + + name := d.Get("name").(string) + resGroup := d.Get("resource_group_name").(string) + + log.Printf("[INFO] Sending virtual network read request to ARM.") + + resp, err := vnetClient.Get(resGroup, name) + if resp.StatusCode == http.StatusNotFound { + // it means the virtual network has been deleted in the meantime; + // so we must go ahead and remove it here: + 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 all the appropriate values: + d.Set("address_space", vnet.AddressSpace.AddressPrefixes) + + // read state of subnets: + subnets := &schema.Set{ + F: resourceAzureSubnetHash, + } + + for _, subnet := range *vnet.Subnets { + s := map[string]interface{}{} + + s["name"] = *subnet.Name + s["address_prefix"] = *subnet.Properties.AddressPrefix + // NOTE(aznashwan): ID's necessary? + if subnet.Properties.NetworkSecurityGroup != nil { + s["security_group"] = *subnet.Properties.NetworkSecurityGroup.ID + } + + subnets.Add(s) + } + d.Set("subnet", subnets) + + // now; dns servers: + dnses := []string{} + for _, dns := range *vnet.DhcpOptions.DNSServers { + dnses = append(dnses, dns) + } + d.Set("dns_servers_names", dnses) + + return nil +} + +// resourceArmVirtualNetworkUpdate goes ahead and updates the corresponding ARM virtual network. +func resourceArmVirtualNetworkUpdate(d *schema.ResourceData, meta interface{}) error { + // considering Create's idempotency, Update is simply a proxy for it... + // Update has been left as a separate function here for utmost clarity: + return resourceArmVirtualNetworkCreate(d, meta) +} + +// resourceArmVirtualNetworkDelete deletes the specified ARM virtual network. +func resourceArmVirtualNetworkDelete(d *schema.ResourceData, meta interface{}) error { + vnetClient := meta.(*ArmClient).vnetClient + + name := d.Get("name").(string) + resGroup := d.Get("resource_group_name").(string) + _, err := vnetClient.Delete(resGroup, name) + + return err +} + +// getVirtualNetworkProperties is a helper function which returns the +// VirtualNetworkPropertiesFormat of the network resource. +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_names").([]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) +} + +// virtualNetworkStateRefreshFunc returns a resource.StateRefreshFunc that is used to watch +// a virtual network. +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 + } +} From 63bc8e98526cbed553437ed76833e4a4b78ad816 Mon Sep 17 00:00:00 2001 From: Nashwan Azhari Date: Tue, 8 Dec 2015 18:50:48 -0500 Subject: [PATCH 2/3] provider/azurerm: Tidy up minor issues This commit cleans up some of the work on the Azure ARM provider following review by @phinze. Specifically: - Unnecessary ASM-targeted tests are removed - Validation is added to the `resource_group` resource - `dns_servers_names` -> `dns_servers` as per the API documentation - AZURE_SUBSCRIPTION_ID environment variable is renamed to be ARM_SUBSCRIPTION_ID in order to match the other environment variables --- builtin/providers/azurerm/config.go | 15 +- builtin/providers/azurerm/provider.go | 2 +- builtin/providers/azurerm/provider_test.go | 151 ++---------------- .../azurerm/resourceArmResourceGroup.go | 30 +++- .../azurerm/resourceArmVirtualNetwork.go | 7 +- 5 files changed, 40 insertions(+), 165 deletions(-) diff --git a/builtin/providers/azurerm/config.go b/builtin/providers/azurerm/config.go index 669e4631d..4e6a75096 100644 --- a/builtin/providers/azurerm/config.go +++ b/builtin/providers/azurerm/config.go @@ -182,15 +182,6 @@ func (c *Config) readArmSettings(contents string) error { return err } -// configFileContentsWarning represents the warning message returned when the -// path to the 'arm_config_file' is provided instead of its sourced contents. -var configFileContentsWarning = ` -The path to the 'arm_config_file' was provided instead of its contents. -Support for accepting filepaths instead of their contents will be removed -in the near future. Do please consider switching over to using -'${file("/path/to/config.arm")}' instead. -`[1:] - // validateArmConfigFile is a helper function which verifies that // the provided ARM configuration file is valid. func validateArmConfigFile(v interface{}, _ string) (ws []string, es []error) { @@ -199,15 +190,11 @@ func validateArmConfigFile(v interface{}, _ string) (ws []string, es []error) { return nil, nil } - pathOrContents, wasPath, err := pathorcontents.Read(v.(string)) + pathOrContents, _, err := pathorcontents.Read(v.(string)) if err != nil { es = append(es, fmt.Errorf("Error reading 'arm_config_file': %s", err)) } - if wasPath { - ws = append(ws, configFileContentsWarning) - } - data := armConfigData{} err = json.Unmarshal([]byte(pathOrContents), &data) if err != nil { diff --git a/builtin/providers/azurerm/provider.go b/builtin/providers/azurerm/provider.go index 8b559c0b1..b9b86ad60 100644 --- a/builtin/providers/azurerm/provider.go +++ b/builtin/providers/azurerm/provider.go @@ -23,7 +23,7 @@ func Provider() terraform.ResourceProvider { "subscription_id": &schema.Schema{ Type: schema.TypeString, Optional: true, - DefaultFunc: schema.EnvDefaultFunc("AZURE_SUBSCRIPTION_ID", ""), + DefaultFunc: schema.EnvDefaultFunc("ARM_SUBSCRIPTION_ID", ""), }, "client_id": &schema.Schema{ diff --git a/builtin/providers/azurerm/provider_test.go b/builtin/providers/azurerm/provider_test.go index 333d8fa3e..ff15860fb 100644 --- a/builtin/providers/azurerm/provider_test.go +++ b/builtin/providers/azurerm/provider_test.go @@ -1,18 +1,11 @@ package azurerm import ( - "io" - "io/ioutil" - "math/rand" "os" - "strings" "testing" - "time" - "github.com/hashicorp/terraform/config" "github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/terraform" - "github.com/mitchellh/go-homedir" ) var testAccProviders map[string]terraform.ResourceProvider @@ -36,7 +29,7 @@ const testAccStorageContainerName = "terraform-testing-container" func init() { testAccProvider = Provider().(*schema.Provider) testAccProviders = map[string]terraform.ResourceProvider{ - "azure": testAccProvider, + "azurerm": testAccProvider, } } @@ -51,13 +44,15 @@ func TestProvider_impl(t *testing.T) { } func testAccPreCheck(t *testing.T) { - if v := os.Getenv("AZURE_PUBLISH_SETTINGS"); v == "" { - subscriptionID := os.Getenv("AZURE_SUBSCRIPTION_ID") - certificate := os.Getenv("AZURE_CERTIFICATE") + if v := os.Getenv("ARM_CREDENTIALS_FILE"); v == "" { + 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 == "" || certificate == "" { - t.Fatal("either AZURE_PUBLISH_SETTINGS, or AZURE_SUBSCRIPTION_ID " + - "and AZURE_CERTIFICATE must be set for acceptance tests") + if subscriptionID == "" || clientID == "" || clientSecret == "" || tenantID == "" { + t.Fatal("Either ARM_CREDENTIALS_FILE or ARM_SUBSCRIPTION_ID, ARM_CLIENT_ID, " + + "ARM_CLIENT_SECRET and ARM_TENANT_ID must be set for acceptance tests") } } @@ -65,131 +60,3 @@ func testAccPreCheck(t *testing.T) { t.Fatal("AZURE_STORAGE must be set for acceptance tests") } } - -func TestAzure_validateSettingsFile(t *testing.T) { - f, err := ioutil.TempFile("", "tf-test") - if err != nil { - t.Fatalf("Error creating temporary file in TestAzure_validateSettingsFile: %s", err) - } - defer os.Remove(f.Name()) - - fx, err := ioutil.TempFile("", "tf-test-xml") - if err != nil { - t.Fatalf("Error creating temporary file with XML in TestAzure_validateSettingsFile: %s", err) - } - defer os.Remove(fx.Name()) - _, err = io.WriteString(fx, "") - if err != nil { - t.Fatalf("Error writing XML File: %s", err) - } - fx.Close() - - home, err := homedir.Dir() - if err != nil { - t.Fatalf("Error fetching homedir: %s", err) - } - fh, err := ioutil.TempFile(home, "tf-test-home") - if err != nil { - t.Fatalf("Error creating homedir-based temporary file: %s", err) - } - defer os.Remove(fh.Name()) - _, err = io.WriteString(fh, "") - if err != nil { - t.Fatalf("Error writing XML File: %s", err) - } - fh.Close() - - r := strings.NewReplacer(home, "~") - homePath := r.Replace(fh.Name()) - - cases := []struct { - Input string // String of XML or a path to an XML file - W int // expected count of warnings - E int // expected count of errors - }{ - {"test", 0, 1}, - {f.Name(), 1, 1}, - {fx.Name(), 1, 0}, - {homePath, 1, 0}, - {"", 0, 0}, - } - - for _, tc := range cases { - w, e := validateSettingsFile(tc.Input, "") - - if len(w) != tc.W { - t.Errorf("Error in TestAzureValidateSettingsFile: input: %s , warnings: %v, errors: %v", tc.Input, w, e) - } - if len(e) != tc.E { - t.Errorf("Error in TestAzureValidateSettingsFile: input: %s , warnings: %v, errors: %v", tc.Input, w, e) - } - } -} - -func TestAzure_providerConfigure(t *testing.T) { - home, err := homedir.Dir() - if err != nil { - t.Fatalf("Error fetching homedir: %s", err) - } - fh, err := ioutil.TempFile(home, "tf-test-home") - if err != nil { - t.Fatalf("Error creating homedir-based temporary file: %s", err) - } - defer os.Remove(fh.Name()) - - _, err = io.WriteString(fh, testAzurePublishSettingsStr) - if err != nil { - t.Fatalf("err: %s", err) - } - fh.Close() - - r := strings.NewReplacer(home, "~") - homePath := r.Replace(fh.Name()) - - cases := []struct { - SettingsFile string // String of XML or a path to an XML file - NilMeta bool // whether meta is expected to be nil - }{ - {testAzurePublishSettingsStr, false}, - {homePath, false}, - } - - for _, tc := range cases { - rp := Provider() - raw := map[string]interface{}{ - "settings_file": tc.SettingsFile, - } - - rawConfig, err := config.NewRawConfig(raw) - if err != nil { - t.Fatalf("err: %s", err) - } - - err = rp.Configure(terraform.NewResourceConfig(rawConfig)) - meta := rp.(*schema.Provider).Meta() - if (meta == nil) != tc.NilMeta { - t.Fatalf("expected NilMeta: %t, got meta: %#v, settings_file: %q", - tc.NilMeta, meta, tc.SettingsFile) - } - } -} - -func genRandInt() int { - return rand.New(rand.NewSource(time.Now().UnixNano())).Int() % 100000 -} - -// testAzurePublishSettingsStr is a revoked publishsettings file -const testAzurePublishSettingsStr = ` - - - - - - -` diff --git a/builtin/providers/azurerm/resourceArmResourceGroup.go b/builtin/providers/azurerm/resourceArmResourceGroup.go index 2e321aab6..3155b6872 100644 --- a/builtin/providers/azurerm/resourceArmResourceGroup.go +++ b/builtin/providers/azurerm/resourceArmResourceGroup.go @@ -3,6 +3,8 @@ package azurerm import ( "fmt" "log" + "regexp" + "strings" "time" "github.com/Azure/azure-sdk-for-go/arm/resources" @@ -21,10 +23,10 @@ func resourceArmResourceGroup() *schema.Resource { Schema: map[string]*schema.Schema{ "name": &schema.Schema{ - Type: schema.TypeString, - Required: true, - ForceNew: true, - //TODO(jen20) - implement validation func: {resource-group-name} must uniquely identify the resource group within the subscription. It must be no longer than 80 characters long. It can only contain alphanumeric characters, dash, underscore, opening parenthesis, closing parenthesis or period. The name cannot end with a period. + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validateArmResourceGroupName, }, "location": &schema.Schema{ Type: schema.TypeString, @@ -36,6 +38,26 @@ func resourceArmResourceGroup() *schema.Resource { } } +// validateArmResourceGroupName validates inputs to the name argument against the requirements +// documented in the ARM REST API guide: http://bit.ly/1NEXclG +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 +} + // resourceArmResourceGroupCreate goes ahead and creates the specified ARM resource group. func resourceArmResourceGroupCreate(d *schema.ResourceData, meta interface{}) error { client := meta.(*ArmClient) diff --git a/builtin/providers/azurerm/resourceArmVirtualNetwork.go b/builtin/providers/azurerm/resourceArmVirtualNetwork.go index c4471b37b..d7ce438bf 100644 --- a/builtin/providers/azurerm/resourceArmVirtualNetwork.go +++ b/builtin/providers/azurerm/resourceArmVirtualNetwork.go @@ -32,7 +32,7 @@ func resourceArmVirtualNetwork() *schema.Resource { Elem: &schema.Schema{Type: schema.TypeString}, }, - "dns_servers_names": &schema.Schema{ + "dns_servers": &schema.Schema{ Type: schema.TypeList, Optional: true, Elem: &schema.Schema{ @@ -158,7 +158,6 @@ func resourceArmVirtualNetworkRead(d *schema.ResourceData, meta interface{}) err s["name"] = *subnet.Name s["address_prefix"] = *subnet.Properties.AddressPrefix - // NOTE(aznashwan): ID's necessary? if subnet.Properties.NetworkSecurityGroup != nil { s["security_group"] = *subnet.Properties.NetworkSecurityGroup.ID } @@ -172,7 +171,7 @@ func resourceArmVirtualNetworkRead(d *schema.ResourceData, meta interface{}) err for _, dns := range *vnet.DhcpOptions.DNSServers { dnses = append(dnses, dns) } - d.Set("dns_servers_names", dnses) + d.Set("dns_servers", dnses) return nil } @@ -206,7 +205,7 @@ func getVirtualNetworkProperties(d *schema.ResourceData) *network.VirtualNetwork // then; the dns servers: dnses := []string{} - for _, dns := range d.Get("dns_servers_names").([]interface{}) { + for _, dns := range d.Get("dns_servers").([]interface{}) { dnses = append(dnses, dns.(string)) } From 805c4896bda01b5e099aa1cbaa2d3b968cd5c123 Mon Sep 17 00:00:00 2001 From: James Nugent Date: Tue, 8 Dec 2015 20:25:05 -0500 Subject: [PATCH 3/3] provider/azurerm: Clean up work for base provider - Add documentation for resources - Rename files to match standard patterns - Add acceptance tests for resource groups - Add acceptance tests for vnets - Remove ARM_CREDENTIALS file - as discussed this does not appear to be an Azure standard, and there is scope for confusion with the azureProfile.json file which the CLI generates. If a standard emerges we can reconsider this. - Validate credentials in the schema - Remove storage testing artefacts - Use ARM IDs as Terraform IDs - Use autorest hooks for logging --- builtin/bins/provider-azurerm/main_test.go | 1 - builtin/providers/azurerm/config.go | 99 ++++++---------- builtin/providers/azurerm/provider.go | 38 +------ builtin/providers/azurerm/provider_test.go | 34 +----- ...roup.go => resource_arm_resource_group.go} | 58 +++++----- .../resource_arm_resource_group_test.go | 82 ++++++++++++++ ...ork.go => resource_arm_virtual_network.go} | 59 ++++------ .../resource_arm_virtual_network_test.go | 100 ++++++++++++++++ builtin/providers/azurerm/resourceid.go | 82 ++++++++++++++ builtin/providers/azurerm/resourceid_test.go | 107 ++++++++++++++++++ website/Vagrantfile | 2 +- website/source/assets/stylesheets/_docs.scss | 1 + .../providers/azurerm/index.html.markdown | 80 +++++++++++++ .../azurerm/r/resource_group.html.markdown | 36 ++++++ .../azurerm/r/virtual_network.html.markdown | 76 +++++++++++++ website/source/layouts/azurerm.erb | 30 +++++ website/source/layouts/docs.erb | 6 +- 17 files changed, 696 insertions(+), 195 deletions(-) delete mode 100644 builtin/bins/provider-azurerm/main_test.go rename builtin/providers/azurerm/{resourceArmResourceGroup.go => resource_arm_resource_group.go} (67%) create mode 100644 builtin/providers/azurerm/resource_arm_resource_group_test.go rename builtin/providers/azurerm/{resourceArmVirtualNetwork.go => resource_arm_virtual_network.go} (75%) create mode 100644 builtin/providers/azurerm/resource_arm_virtual_network_test.go create mode 100644 builtin/providers/azurerm/resourceid.go create mode 100644 builtin/providers/azurerm/resourceid_test.go create mode 100644 website/source/docs/providers/azurerm/index.html.markdown create mode 100644 website/source/docs/providers/azurerm/r/resource_group.html.markdown create mode 100644 website/source/docs/providers/azurerm/r/virtual_network.html.markdown create mode 100644 website/source/layouts/azurerm.erb diff --git a/builtin/bins/provider-azurerm/main_test.go b/builtin/bins/provider-azurerm/main_test.go deleted file mode 100644 index 06ab7d0f9..000000000 --- a/builtin/bins/provider-azurerm/main_test.go +++ /dev/null @@ -1 +0,0 @@ -package main diff --git a/builtin/providers/azurerm/config.go b/builtin/providers/azurerm/config.go index 4e6a75096..12911512b 100644 --- a/builtin/providers/azurerm/config.go +++ b/builtin/providers/azurerm/config.go @@ -1,8 +1,8 @@ package azurerm import ( - "encoding/json" - "fmt" + "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" @@ -10,7 +10,7 @@ import ( "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/hashicorp/terraform/helper/pathorcontents" + "github.com/Azure/go-autorest/autorest" ) // ArmClient contains the handles to all the specific Azure Resource Manager @@ -46,14 +46,20 @@ type ArmClient struct { 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) { - // first; check that all the necessary credentials were provided: - if !c._armCredentialsProvided() { - return nil, fmt.Errorf("Not all ARM-required fields have been provided.") - } - spt, err := azure.NewServicePrincipalToken(c.ClientID, c.ClientSecret, c.TenantID, azure.AzureResourceManagerScope) if err != nil { return nil, err @@ -66,149 +72,118 @@ func (c *Config) getArmClient() (*ArmClient, error) { // 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 } - -// armCredentialsProvided is a helper method which indicates whether or not the -// credentials required for authenticating against the ARM APIs were provided. -func (c *Config) armCredentialsProvided() bool { - return c.ArmConfig != "" || c._armCredentialsProvided() -} -func (c *Config) _armCredentialsProvided() bool { - return !(c.SubscriptionID == "" || c.ClientID == "" || c.ClientSecret == "" || c.TenantID == "") -} - -// readArmSettings is a helper method which; given the contents of the ARM -// credentials file, loads all the data into the Config. -func (c *Config) readArmSettings(contents string) error { - data := &armConfigData{} - err := json.Unmarshal([]byte(contents), data) - - c.SubscriptionID = data.SubscriptionID - c.ClientID = data.ClientID - c.ClientSecret = data.ClientSecret - c.TenantID = data.TenantID - - return err -} - -// validateArmConfigFile is a helper function which verifies that -// the provided ARM configuration file is valid. -func validateArmConfigFile(v interface{}, _ string) (ws []string, es []error) { - value := v.(string) - if value == "" { - return nil, nil - } - - pathOrContents, _, err := pathorcontents.Read(v.(string)) - if err != nil { - es = append(es, fmt.Errorf("Error reading 'arm_config_file': %s", err)) - } - - data := armConfigData{} - err = json.Unmarshal([]byte(pathOrContents), &data) - if err != nil { - es = append(es, fmt.Errorf("Error unmarshalling the provided 'arm_config_file': %s", err)) - } - - return -} - -// armConfigData is a private struct which represents the expected layout of -// an ARM configuration file. It is used for unmarshalling purposes. -type armConfigData struct { - ClientID string `json:"clientID"` - ClientSecret string `json:"clientSecret"` - SubscriptionID string `json:"subscriptionID"` - TenantID string `json:"tenantID"` -} diff --git a/builtin/providers/azurerm/provider.go b/builtin/providers/azurerm/provider.go index b9b86ad60..53c5c97a6 100644 --- a/builtin/providers/azurerm/provider.go +++ b/builtin/providers/azurerm/provider.go @@ -1,7 +1,6 @@ package azurerm import ( - "fmt" "strings" "github.com/hashicorp/terraform/helper/schema" @@ -12,35 +11,27 @@ import ( func Provider() terraform.ResourceProvider { return &schema.Provider{ Schema: map[string]*schema.Schema{ - "arm_config_file": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - Default: "", - DefaultFunc: schema.EnvDefaultFunc("ARM_CONFIG_FILE", nil), - ValidateFunc: validateArmConfigFile, - }, - "subscription_id": &schema.Schema{ Type: schema.TypeString, - Optional: true, + Required: true, DefaultFunc: schema.EnvDefaultFunc("ARM_SUBSCRIPTION_ID", ""), }, "client_id": &schema.Schema{ Type: schema.TypeString, - Optional: true, + Required: true, DefaultFunc: schema.EnvDefaultFunc("ARM_CLIENT_ID", ""), }, "client_secret": &schema.Schema{ Type: schema.TypeString, - Optional: true, + Required: true, DefaultFunc: schema.EnvDefaultFunc("ARM_CLIENT_SECRET", ""), }, "tenant_id": &schema.Schema{ Type: schema.TypeString, - Optional: true, + Required: true, DefaultFunc: schema.EnvDefaultFunc("ARM_TENANT_ID", ""), }, }, @@ -59,19 +50,12 @@ func Provider() terraform.ResourceProvider { type Config struct { ManagementURL string - ArmConfig string - SubscriptionID string ClientID string ClientSecret string TenantID string } -const noConfigError = `Credentials must be provided either via arm_config_file, or via -subscription_id, client_id, client_secret and tenant_id. Please see -the provider documentation for more information on how to obtain these -credentials.` - func providerConfigure(d *schema.ResourceData) (interface{}, error) { config := Config{ SubscriptionID: d.Get("subscription_id").(string), @@ -80,20 +64,6 @@ func providerConfigure(d *schema.ResourceData) (interface{}, error) { TenantID: d.Get("tenant_id").(string), } - // check if credentials file is provided: - armConfig := d.Get("arm_config_file").(string) - if armConfig != "" { - // then, load the settings from that: - if err := config.readArmSettings(armConfig); err != nil { - return nil, err - } - } - - // then; check whether the ARM credentials were provided: - if !config.armCredentialsProvided() { - return nil, fmt.Errorf(noConfigError) - } - client, err := config.getArmClient() if err != nil { return nil, err diff --git a/builtin/providers/azurerm/provider_test.go b/builtin/providers/azurerm/provider_test.go index ff15860fb..a26249f58 100644 --- a/builtin/providers/azurerm/provider_test.go +++ b/builtin/providers/azurerm/provider_test.go @@ -11,21 +11,6 @@ import ( var testAccProviders map[string]terraform.ResourceProvider var testAccProvider *schema.Provider -const ( - testAccSecurityGroupName = "terraform-security-group" - testAccHostedServiceName = "terraform-testing-service" -) - -// testAccStorageServiceName is used as the name for the Storage Service -// created in all storage-related tests. -// It is much more convenient to provide a Storage Service which -// has been created beforehand as the creation of one takes a lot -// and would greatly impede the multitude of tests which rely on one. -// NOTE: the storage container should be located in `West US`. -var testAccStorageServiceName = os.Getenv("AZURE_STORAGE") - -const testAccStorageContainerName = "terraform-testing-container" - func init() { testAccProvider = Provider().(*schema.Provider) testAccProviders = map[string]terraform.ResourceProvider{ @@ -44,19 +29,12 @@ func TestProvider_impl(t *testing.T) { } func testAccPreCheck(t *testing.T) { - if v := os.Getenv("ARM_CREDENTIALS_FILE"); v == "" { - subscriptionID := os.Getenv("ARM_SUBSCRIPTION_ID") - clientID := os.Getenv("ARM_CLIENT_ID") - clientSecret := os.Getenv("ARM_CLIENT_SECRET") - tenantID := os.Getenv("ARM_TENANT_ID") + 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("Either ARM_CREDENTIALS_FILE or ARM_SUBSCRIPTION_ID, ARM_CLIENT_ID, " + - "ARM_CLIENT_SECRET and ARM_TENANT_ID must be set for acceptance tests") - } - } - - if v := os.Getenv("AZURE_STORAGE"); v == "" { - t.Fatal("AZURE_STORAGE must be set for acceptance tests") + 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/resourceArmResourceGroup.go b/builtin/providers/azurerm/resource_arm_resource_group.go similarity index 67% rename from builtin/providers/azurerm/resourceArmResourceGroup.go rename to builtin/providers/azurerm/resource_arm_resource_group.go index 3155b6872..a4304c8d4 100644 --- a/builtin/providers/azurerm/resourceArmResourceGroup.go +++ b/builtin/providers/azurerm/resource_arm_resource_group.go @@ -3,6 +3,7 @@ package azurerm import ( "fmt" "log" + "net/http" "regexp" "strings" "time" @@ -12,8 +13,6 @@ import ( "github.com/hashicorp/terraform/helper/schema" ) -// resourceArmResourceGroup returns the *schema.Resource -// associated to resource group resources on ARM. func resourceArmResourceGroup() *schema.Resource { return &schema.Resource{ Create: resourceArmResourceGroupCreate, @@ -38,8 +37,6 @@ func resourceArmResourceGroup() *schema.Resource { } } -// validateArmResourceGroupName validates inputs to the name argument against the requirements -// documented in the ARM REST API guide: http://bit.ly/1NEXclG func validateArmResourceGroupName(v interface{}, k string) (ws []string, es []error) { value := v.(string) @@ -51,14 +48,13 @@ func validateArmResourceGroupName(v interface{}, k string) (ws []string, es []er 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 { + 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 } -// resourceArmResourceGroupCreate goes ahead and creates the specified ARM resource group. func resourceArmResourceGroupCreate(d *schema.ResourceData, meta interface{}) error { client := meta.(*ArmClient) resGroupClient := client.resourceGroupClient @@ -66,64 +62,67 @@ func resourceArmResourceGroupCreate(d *schema.ResourceData, meta interface{}) er name := d.Get("name").(string) location := d.Get("location").(string) - log.Printf("[INFO] Issuing Azure ARM creation request for resource group '%s'.", name) - rg := resources.ResourceGroup{ Name: &name, Location: &location, } - _, err := resGroupClient.CreateOrUpdate(name, rg) + 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(*rg.Name) + d.SetId(*resp.ID) - // Wait for the resource group to become available - // TODO(jen20): Is there any need for this? - log.Printf("[DEBUG] Waiting for Resource Group (%s) to become available", d.Id()) + log.Printf("[DEBUG] Waiting for Resource Group (%s) to become available", name) stateConf := &resource.StateChangeConf{ Pending: []string{"Accepted"}, Target: "Succeeded", - Refresh: resourceGroupStateRefreshFunc(client, d.Id()), + 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", d.Id(), err) + return fmt.Errorf("Error waiting for Resource Group (%s) to become available: %s", name, err) } return resourceArmResourceGroupRead(d, meta) } -// resourceArmResourceGroupRead goes ahead and reads the state of the corresponding ARM resource group. func resourceArmResourceGroupRead(d *schema.ResourceData, meta interface{}) error { resGroupClient := meta.(*ArmClient).resourceGroupClient - name := d.Id() - log.Printf("[INFO] Issuing read request to Azure ARM for resource group '%s'.", name) + 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) + d.Set("name", res.Name) + d.Set("location", res.Location) return nil } -// resourceArmResourceGroupExists goes ahead and checks for the existence of the correspoding ARM resource group. func resourceArmResourceGroupExists(d *schema.ResourceData, meta interface{}) (bool, error) { resGroupClient := meta.(*ArmClient).resourceGroupClient - name := d.Id() + id, err := parseAzureResourceID(d.Id()) + if err != nil { + return false, err + } + name := id.ResourceGroup resp, err := resGroupClient.CheckExistence(name) if err != nil { - // TODO(aznashwan): implement some error switching helpers in the SDK - // to avoid HTTP error checks such as the below: if resp.StatusCode != 200 { return false, err } @@ -134,13 +133,16 @@ func resourceArmResourceGroupExists(d *schema.ResourceData, meta interface{}) (b return true, nil } -// resourceArmResourceGroupDelete deletes the specified ARM resource group. func resourceArmResourceGroupDelete(d *schema.ResourceData, meta interface{}) error { resGroupClient := meta.(*ArmClient).resourceGroupClient - name := d.Id() + id, err := parseAzureResourceID(d.Id()) + if err != nil { + return err + } + name := id.ResourceGroup - _, err := resGroupClient.Delete(name) + _, err = resGroupClient.Delete(name) if err != nil { return err } @@ -148,8 +150,6 @@ func resourceArmResourceGroupDelete(d *schema.ResourceData, meta interface{}) er return nil } -// resourceGroupStateRefreshFunc returns a resource.StateRefreshFunc that is used to watch -// a resource group. func resourceGroupStateRefreshFunc(client *ArmClient, id string) resource.StateRefreshFunc { return func() (interface{}, string, error) { res, err := client.resourceGroupClient.Get(id) 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/resourceArmVirtualNetwork.go b/builtin/providers/azurerm/resource_arm_virtual_network.go similarity index 75% rename from builtin/providers/azurerm/resourceArmVirtualNetwork.go rename to builtin/providers/azurerm/resource_arm_virtual_network.go index d7ce438bf..305af5a76 100644 --- a/builtin/providers/azurerm/resourceArmVirtualNetwork.go +++ b/builtin/providers/azurerm/resource_arm_virtual_network.go @@ -16,7 +16,7 @@ func resourceArmVirtualNetwork() *schema.Resource { return &schema.Resource{ Create: resourceArmVirtualNetworkCreate, Read: resourceArmVirtualNetworkRead, - Update: resourceArmVirtualNetworkUpdate, + Update: resourceArmVirtualNetworkCreate, Delete: resourceArmVirtualNetworkDelete, Schema: map[string]*schema.Schema{ @@ -78,7 +78,6 @@ func resourceArmVirtualNetwork() *schema.Resource { } } -// resourceArmVirtualNetworkCreate creates the specified ARM virtual network. func resourceArmVirtualNetworkCreate(d *schema.ResourceData, meta interface{}) error { client := meta.(*ArmClient) vnetClient := client.vnetClient @@ -95,22 +94,14 @@ func resourceArmVirtualNetworkCreate(d *schema.ResourceData, meta interface{}) e Properties: getVirtualNetworkProperties(d), } - log.Printf("[INFO] Sending virtual network create request to ARM.") - _, err := vnetClient.CreateOrUpdate(resGroup, name, vnet) + resp, err := vnetClient.CreateOrUpdate(resGroup, name, vnet) if err != nil { return err } - // if res.Response.StatusCode != http.StatusAccepted { - // return fmt.Errorf("Creation request was denies: code: %d", res.Response.StatusCode) - // } + d.SetId(*resp.ID) - d.SetId(name) - d.Set("resGroup", resGroup) - - // Wait for the resource group to become available - // TODO(jen20): Is there any need for this? - log.Printf("[DEBUG] Waiting for Virtual Network (%s) to become available", d.Id()) + log.Printf("[DEBUG] Waiting for Virtual Network (%s) to become available", name) stateConf := &resource.StateChangeConf{ Pending: []string{"Accepted", "Updating"}, Target: "Succeeded", @@ -118,25 +109,24 @@ func resourceArmVirtualNetworkCreate(d *schema.ResourceData, meta interface{}) e Timeout: 10 * time.Minute, } if _, err := stateConf.WaitForState(); err != nil { - return fmt.Errorf("Error waiting for Virtual Network (%s) to become available: %s", d.Id(), err) + return fmt.Errorf("Error waiting for Virtual Network (%s) to become available: %s", name, err) } return resourceArmVirtualNetworkRead(d, meta) } -// resourceArmVirtualNetworkRead goes ahead and reads the state of the corresponding ARM virtual network. func resourceArmVirtualNetworkRead(d *schema.ResourceData, meta interface{}) error { vnetClient := meta.(*ArmClient).vnetClient - name := d.Get("name").(string) - resGroup := d.Get("resource_group_name").(string) - - log.Printf("[INFO] Sending virtual network read request to ARM.") + 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 { - // it means the virtual network has been deleted in the meantime; - // so we must go ahead and remove it here: d.SetId("") return nil } @@ -145,10 +135,9 @@ func resourceArmVirtualNetworkRead(d *schema.ResourceData, meta interface{}) err } vnet := *resp.Properties - // update all the appropriate values: + // update appropriate values d.Set("address_space", vnet.AddressSpace.AddressPrefixes) - // read state of subnets: subnets := &schema.Set{ F: resourceAzureSubnetHash, } @@ -166,7 +155,6 @@ func resourceArmVirtualNetworkRead(d *schema.ResourceData, meta interface{}) err } d.Set("subnet", subnets) - // now; dns servers: dnses := []string{} for _, dns := range *vnet.DhcpOptions.DNSServers { dnses = append(dnses, dns) @@ -176,26 +164,21 @@ func resourceArmVirtualNetworkRead(d *schema.ResourceData, meta interface{}) err return nil } -// resourceArmVirtualNetworkUpdate goes ahead and updates the corresponding ARM virtual network. -func resourceArmVirtualNetworkUpdate(d *schema.ResourceData, meta interface{}) error { - // considering Create's idempotency, Update is simply a proxy for it... - // Update has been left as a separate function here for utmost clarity: - return resourceArmVirtualNetworkCreate(d, meta) -} - -// resourceArmVirtualNetworkDelete deletes the specified ARM virtual network. func resourceArmVirtualNetworkDelete(d *schema.ResourceData, meta interface{}) error { vnetClient := meta.(*ArmClient).vnetClient - name := d.Get("name").(string) - resGroup := d.Get("resource_group_name").(string) - _, err := vnetClient.Delete(resGroup, name) + id, err := parseAzureResourceID(d.Id()) + if err != nil { + return err + } + resGroup := id.ResourceGroup + name := id.Path["virtualNetworks"] + + _, err = vnetClient.Delete(resGroup, name) return err } -// getVirtualNetworkProperties is a helper function which returns the -// VirtualNetworkPropertiesFormat of the network resource. func getVirtualNetworkProperties(d *schema.ResourceData) *network.VirtualNetworkPropertiesFormat { // first; get address space prefixes: prefixes := []string{} @@ -255,8 +238,6 @@ func resourceAzureSubnetHash(v interface{}) int { return hashcode.String(subnet) } -// virtualNetworkStateRefreshFunc returns a resource.StateRefreshFunc that is used to watch -// a virtual network. func virtualNetworkStateRefreshFunc(client *ArmClient, resourceGroupName string, networkName string) resource.StateRefreshFunc { return func() (interface{}, string, error) { res, err := client.vnetClient.Get(resourceGroupName, networkName) 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) >