From 53c23511efe1150a5682e8d128370a57fddfd558 Mon Sep 17 00:00:00 2001 From: James Nugent Date: Sat, 9 Jan 2016 19:22:00 -0500 Subject: [PATCH 1/2] provider/azurerm: Add `azurerm_storage_account` This is an unusual resource (so far) in that it cannot be created in one call, and instead must be created and the modified to set some of the parameters. We use the pollIndefinitelyWhileNeeded function which will continue to poll Azure RM operation monitoring endpoints until an error is reported or the operation meets one of the given status codes. The function was originally part of this feature but was separated out in order to unblock other work. Currently there is no support for the "custom_domain" section of the storage account API. This was originally present and was later taken out of the scope of the storage account resource in order that the following workflow can be used: 1. Create storage account 2. Create DNS CNAME entry once the account name is known 3. Create custom domain mapping --- builtin/providers/azurerm/config.go | 21 +- builtin/providers/azurerm/provider.go | 3 +- .../azurerm/resource_arm_storage_account.go | 292 ++++++++++++++++++ .../resource_arm_storage_account_test.go | 166 ++++++++++ 4 files changed, 479 insertions(+), 3 deletions(-) create mode 100644 builtin/providers/azurerm/resource_arm_storage_account.go create mode 100644 builtin/providers/azurerm/resource_arm_storage_account_test.go diff --git a/builtin/providers/azurerm/config.go b/builtin/providers/azurerm/config.go index 118c9aeb8b..de4da13825 100644 --- a/builtin/providers/azurerm/config.go +++ b/builtin/providers/azurerm/config.go @@ -4,6 +4,7 @@ import ( "fmt" "log" "net/http" + "time" "github.com/Azure/azure-sdk-for-go/Godeps/_workspace/src/github.com/Azure/go-autorest/autorest" "github.com/Azure/azure-sdk-for-go/Godeps/_workspace/src/github.com/Azure/go-autorest/autorest/azure" @@ -58,7 +59,7 @@ type ArmClient struct { 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) + log.Printf("[DEBUG] Sending Azure RM Request %q to %q\n", r.Method, r.URL) resp, err := s.Do(r) if resp != nil { log.Printf("[DEBUG] Received Azure RM Request status code %s for %s\n", resp.Status, r.URL) @@ -70,6 +71,22 @@ func withRequestLogging() autorest.SendDecorator { } } +func withPollWatcher() autorest.SendDecorator { + return func(s autorest.Sender) autorest.Sender { + return autorest.SenderFunc(func(r *http.Request) (*http.Response, error) { + fmt.Printf("[DEBUG] Sending Azure RM Request %q to %q\n", r.Method, r.URL) + resp, err := s.Do(r) + fmt.Printf("[DEBUG] Received Azure RM Request status code %s for %s\n", resp.Status, r.URL) + if autorest.ResponseRequiresPolling(resp) { + fmt.Printf("[DEBUG] Azure RM request will poll %s after %d seconds\n", + autorest.GetPollingLocation(resp), + int(autorest.GetPollingDelay(resp, time.Duration(0))/time.Second)) + } + return resp, err + }) + } +} + func setUserAgent(client *autorest.Client) { var version string if terraform.VersionPrerelease != "" { @@ -241,7 +258,7 @@ func (c *Config) getArmClient() (*ArmClient, error) { ssc := storage.NewAccountsClient(c.SubscriptionID) setUserAgent(&ssc.Client) ssc.Authorizer = spt - ssc.Sender = autorest.CreateSender(withRequestLogging()) + ssc.Sender = autorest.CreateSender(withRequestLogging(), withPollWatcher()) client.storageServiceClient = ssc suc := storage.NewUsageOperationsClient(c.SubscriptionID) diff --git a/builtin/providers/azurerm/provider.go b/builtin/providers/azurerm/provider.go index d02c00a811..56911cfd6a 100644 --- a/builtin/providers/azurerm/provider.go +++ b/builtin/providers/azurerm/provider.go @@ -55,6 +55,7 @@ func Provider() terraform.ResourceProvider { "azurerm_route": resourceArmRoute(), "azurerm_cdn_profile": resourceArmCdnProfile(), "azurerm_cdn_endpoint": resourceArmCdnEndpoint(), + "azurerm_storage_account": resourceArmStorageAccount(), }, ConfigureFunc: providerConfigure, } @@ -99,7 +100,7 @@ func providerConfigure(d *schema.ResourceData) (interface{}, error) { func registerAzureResourceProvidersWithSubscription(config *Config, client *ArmClient) error { providerClient := client.providers - providers := []string{"Microsoft.Network", "Microsoft.Compute", "Microsoft.Cdn"} + providers := []string{"Microsoft.Network", "Microsoft.Compute", "Microsoft.Cdn", "Microsoft.Storage"} for _, v := range providers { res, err := providerClient.Register(v) diff --git a/builtin/providers/azurerm/resource_arm_storage_account.go b/builtin/providers/azurerm/resource_arm_storage_account.go new file mode 100644 index 0000000000..bd2a9cdf8b --- /dev/null +++ b/builtin/providers/azurerm/resource_arm_storage_account.go @@ -0,0 +1,292 @@ +package azurerm + +import ( + "fmt" + "net/http" + "regexp" + "strings" + + "github.com/Azure/azure-sdk-for-go/arm/storage" + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceArmStorageAccount() *schema.Resource { + return &schema.Resource{ + Create: resourceArmStorageAccountCreate, + Read: resourceArmStorageAccountRead, + Update: resourceArmStorageAccountUpdate, + Delete: resourceArmStorageAccountDelete, + + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validateArmStorageAccountName, + }, + + "resource_group_name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "location": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + StateFunc: azureRMNormalizeLocation, + }, + + "account_type": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ValidateFunc: validateArmStorageAccountType, + }, + + "primary_location": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + + "secondary_location": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + + "primary_blob_endpoint": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + + "secondary_blob_endpoint": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + + "primary_queue_endpoint": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + + "secondary_queue_endpoint": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + + "primary_table_endpoint": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + + "secondary_table_endpoint": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + + // NOTE: The API does not appear to expose a secondary file endpoint + "primary_file_endpoint": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + + "tags": tagsSchema(), + }, + } +} + +func resourceArmStorageAccountCreate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*ArmClient).storageServiceClient + + resourceGroupName := d.Get("resource_group_name").(string) + storageAccountName := d.Get("name").(string) + accountType := d.Get("account_type").(string) + location := d.Get("location").(string) + tags := d.Get("tags").(map[string]interface{}) + + opts := storage.AccountCreateParameters{ + Location: &location, + Properties: &storage.AccountPropertiesCreateParameters{ + AccountType: storage.AccountType(accountType), + }, + Tags: expandTags(tags), + } + + accResp, err := client.Create(resourceGroupName, storageAccountName, opts) + if err != nil { + return fmt.Errorf("Error creating Azure Storage Account '%s': %s", storageAccountName, err) + } + _, err = pollIndefinitelyAsNeeded(client.Client, accResp.Response.Response, http.StatusOK) + if err != nil { + return fmt.Errorf("Error creating Azure Storage Account %q: %s", storageAccountName, err) + } + + // The only way to get the ID back apparently is to read the resource again + account, err := client.GetProperties(resourceGroupName, storageAccountName) + if err != nil { + return fmt.Errorf("Error retrieving Azure Storage Account %q: %s", storageAccountName, err) + } + + d.SetId(*account.ID) + + return resourceArmStorageAccountRead(d, meta) +} + +// resourceArmStorageAccountUpdate is unusual in the ARM API where most resources have a combined +// and idempotent operation for CreateOrUpdate. In particular updating all of the parameters +// available requires a call to Update per parameter... +func resourceArmStorageAccountUpdate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*ArmClient).storageServiceClient + id, err := parseAzureResourceID(d.Id()) + if err != nil { + return err + } + storageAccountName := id.Path["storageAccounts"] + resourceGroupName := id.ResourceGroup + + d.Partial(true) + + if d.HasChange("account_type") { + accountType := d.Get("account_type").(string) + + opts := storage.AccountUpdateParameters{ + Properties: &storage.AccountPropertiesUpdateParameters{ + AccountType: storage.AccountType(accountType), + }, + } + accResp, err := client.Update(resourceGroupName, storageAccountName, opts) + if err != nil { + return fmt.Errorf("Error updating Azure Storage Account type %q: %s", storageAccountName, err) + } + _, err = pollIndefinitelyAsNeeded(client.Client, accResp.Response.Response, http.StatusOK) + if err != nil { + return fmt.Errorf("Error updating Azure Storage Account type %q: %s", storageAccountName, err) + } + + d.SetPartial("account_type") + } + + if d.HasChange("tags") { + tags := d.Get("tags").(map[string]interface{}) + + opts := storage.AccountUpdateParameters{ + Tags: expandTags(tags), + } + accResp, err := client.Update(resourceGroupName, storageAccountName, opts) + if err != nil { + return fmt.Errorf("Error updating Azure Storage Account tags %q: %s", storageAccountName, err) + } + _, err = pollIndefinitelyAsNeeded(client.Client, accResp.Response.Response, http.StatusOK) + if err != nil { + return fmt.Errorf("Error updating Azure Storage Account tags %q: %s", storageAccountName, err) + } + + d.SetPartial("tags") + } + + d.Partial(false) + return nil +} + +func resourceArmStorageAccountRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*ArmClient).storageServiceClient + + id, err := parseAzureResourceID(d.Id()) + if err != nil { + return err + } + name := id.Path["storageAccounts"] + resGroup := id.ResourceGroup + + resp, err := client.GetProperties(resGroup, name) + if err != nil { + if resp.StatusCode == http.StatusNoContent { + d.SetId("") + return nil + } + + return fmt.Errorf("Error reading the state of AzureRM Storage Account %q: %s", name, err) + } + + d.Set("location", resp.Location) + d.Set("account_type", resp.Properties.AccountType) + d.Set("primary_location", resp.Properties.PrimaryLocation) + d.Set("secondary_location", resp.Properties.SecondaryLocation) + + if resp.Properties.PrimaryEndpoints != nil { + d.Set("primary_blob_endpoint", resp.Properties.PrimaryEndpoints.Blob) + d.Set("primary_queue_endpoint", resp.Properties.PrimaryEndpoints.Queue) + d.Set("primary_table_endpoint", resp.Properties.PrimaryEndpoints.Table) + d.Set("primary_file_endpoint", resp.Properties.PrimaryEndpoints.File) + } + + if resp.Properties.SecondaryEndpoints != nil { + if resp.Properties.SecondaryEndpoints.Blob != nil { + d.Set("secondary_blob_endpoint", resp.Properties.SecondaryEndpoints.Blob) + } else { + d.Set("secondary_blob_endpoint", "") + } + if resp.Properties.SecondaryEndpoints.Queue != nil { + d.Set("secondary_queue_endpoint", resp.Properties.SecondaryEndpoints.Queue) + } else { + d.Set("secondary_queue_endpoint", "") + } + if resp.Properties.SecondaryEndpoints.Table != nil { + d.Set("secondary_table_endpoint", resp.Properties.SecondaryEndpoints.Table) + } else { + d.Set("secondary_table_endpoint", "") + } + } + + flattenAndSetTags(d, resp.Tags) + + return nil +} + +func resourceArmStorageAccountDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*ArmClient).storageServiceClient + + id, err := parseAzureResourceID(d.Id()) + if err != nil { + return err + } + name := id.Path["storageAccounts"] + resGroup := id.ResourceGroup + + accResp, err := client.Delete(resGroup, name) + if err != nil { + return fmt.Errorf("Error issuing AzureRM delete request for storage account %q: %s", name, err) + } + _, err = pollIndefinitelyAsNeeded(client.Client, accResp.Response, http.StatusNotFound) + if err != nil { + return fmt.Errorf("Error polling for AzureRM delete request for storage account %q: %s", name, err) + } + + return nil +} + +func validateArmStorageAccountName(v interface{}, k string) (ws []string, es []error) { + input := v.(string) + + if !regexp.MustCompile(`\A([a-z0-9]{3,24})\z`).MatchString(input) { + es = append(es, fmt.Errorf("name can only consist of lowercase letters and numbers, and must be between 3 and 24 characters long")) + } + + return +} + +func validateArmStorageAccountType(v interface{}, k string) (ws []string, es []error) { + validAccountTypes := []string{"standard_lrs", "standard_zrs", + "standard_grs", "standard_ragrs", "premium_lrs"} + + input := strings.ToLower(v.(string)) + + for _, valid := range validAccountTypes { + if valid == input { + return + } + } + + es = append(es, fmt.Errorf("Invalid storage account type %q", input)) + return +} diff --git a/builtin/providers/azurerm/resource_arm_storage_account_test.go b/builtin/providers/azurerm/resource_arm_storage_account_test.go new file mode 100644 index 0000000000..6cad5a1f50 --- /dev/null +++ b/builtin/providers/azurerm/resource_arm_storage_account_test.go @@ -0,0 +1,166 @@ +package azurerm + +import ( + "fmt" + "net/http" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestValidateArmStorageAccountType(t *testing.T) { + testCases := []struct { + input string + shouldError bool + }{ + {"standard_lrs", false}, + {"invalid", true}, + } + + for _, test := range testCases { + _, es := validateArmStorageAccountType(test.input, "account_type") + + if test.shouldError && len(es) == 0 { + t.Fatalf("Expected validating account_type %q to fail", test.input) + } + } +} + +func TestValidateArmStorageAccountName(t *testing.T) { + testCases := []struct { + input string + shouldError bool + }{ + {"ab", true}, + {"ABC", true}, + {"abc", false}, + {"123456789012345678901234", false}, + {"1234567890123456789012345", true}, + {"abc12345", false}, + } + + for _, test := range testCases { + _, es := validateArmStorageAccountName(test.input, "name") + + if test.shouldError && len(es) == 0 { + t.Fatalf("Expected validating name %q to fail", test.input) + } + } +} + +func TestAccAzureRMStorageAccount_basic(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMStorageAccountDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAzureRMStorageAccount_basic, + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMStorageAccountExists("azurerm_storage_account.testsa"), + resource.TestCheckResourceAttr("azurerm_storage_account.testsa", "account_type", "Standard_LRS"), + resource.TestCheckResourceAttr("azurerm_storage_account.testsa", "tags.#", "1"), + resource.TestCheckResourceAttr("azurerm_storage_account.testsa", "tags.environment", "production"), + ), + }, + + resource.TestStep{ + Config: testAccAzureRMStorageAccount_update, + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMStorageAccountExists("azurerm_storage_account.testsa"), + resource.TestCheckResourceAttr("azurerm_storage_account.testsa", "account_type", "Standard_GRS"), + resource.TestCheckResourceAttr("azurerm_storage_account.testsa", "tags.#", "1"), + resource.TestCheckResourceAttr("azurerm_storage_account.testsa", "tags.environment", "staging"), + ), + }, + }, + }) +} + +func testCheckAzureRMStorageAccountExists(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) + } + + storageAccount := rs.Primary.Attributes["name"] + resourceGroup := rs.Primary.Attributes["resource_group_name"] + + // Ensure resource group exists in API + conn := testAccProvider.Meta().(*ArmClient).storageServiceClient + + resp, err := conn.GetProperties(resourceGroup, storageAccount) + if err != nil { + return fmt.Errorf("Bad: Get on storageServiceClient: %s", err) + } + + if resp.StatusCode == http.StatusNotFound { + return fmt.Errorf("Bad: StorageAccount %q (resource group: %q) does not exist", name, resourceGroup) + } + + return nil + } +} + +func testCheckAzureRMStorageAccountDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*ArmClient).storageServiceClient + + for _, rs := range s.RootModule().Resources { + if rs.Type != "azurerm_storage_account" { + continue + } + + name := rs.Primary.Attributes["name"] + resourceGroup := rs.Primary.Attributes["resource_group_name"] + + resp, err := conn.GetProperties(resourceGroup, name) + if err != nil { + return nil + } + + if resp.StatusCode != http.StatusNotFound { + return fmt.Errorf("Storage Account still exists:\n%#v", resp.Properties) + } + } + + return nil +} + +var testAccAzureRMStorageAccount_basic = ` +resource "azurerm_resource_group" "testrg" { + name = "testAccAzureRMStorageAccountBasic" + location = "westus" +} + +resource "azurerm_storage_account" "testsa" { + name = "unlikely23exst2acct1435" + resource_group_name = "${azurerm_resource_group.testrg.name}" + + location = "westus" + account_type = "Standard_LRS" + + tags { + environment = "production" + } +}` + +var testAccAzureRMStorageAccount_update = ` +resource "azurerm_resource_group" "testrg" { + name = "testAccAzureRMStorageAccountBasic" + location = "westus" +} + +resource "azurerm_storage_account" "testsa" { + name = "unlikely23exst2acct1435" + resource_group_name = "${azurerm_resource_group.testrg.name}" + + location = "westus" + account_type = "Standard_GRS" + + tags { + environment = "staging" + } +}` From 8449cf9d98f51f24e40e1dcf5760d96bd9f3060d Mon Sep 17 00:00:00 2001 From: James Nugent Date: Wed, 20 Jan 2016 19:58:48 -0500 Subject: [PATCH 2/2] provider/azurerm: `azurerm_storage_account` docs --- .../azurerm/r/storage_account.html.markdown | 72 +++++++++++++++++++ website/source/layouts/azurerm.erb | 11 +++ 2 files changed, 83 insertions(+) create mode 100644 website/source/docs/providers/azurerm/r/storage_account.html.markdown diff --git a/website/source/docs/providers/azurerm/r/storage_account.html.markdown b/website/source/docs/providers/azurerm/r/storage_account.html.markdown new file mode 100644 index 0000000000..93d756144a --- /dev/null +++ b/website/source/docs/providers/azurerm/r/storage_account.html.markdown @@ -0,0 +1,72 @@ +--- +layout: "azurerm" +page_title: "Azure Resource Manager: azurerm_storage_account" +sidebar_current: "docs-azurerm-resource-storage-account" +description: |- + Create a Azure Storage Account. +--- + +# azurerm\_storage\_account + +Create an Azure Storage Account. + +## Example Usage + +``` +resource "azurerm_resource_group" "testrg" { + name = "resourceGroupName" + location = "westus" +} + +resource "azurerm_storage_account" "testsa" { + name = "storageaccountname" + resource_group_name = "${azurerm_resource_group.testrg.name}" + + location = "westus" + account_type = "Standard_GRS" + + tags { + environment = "staging" + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) Specifies the name of the storage account. Changing this forces a + new resource to be created. This must be unique across the entire Azure service, + not just within the resource group. + +* `resource_group_name` - (Required) The name of the resource group in which to + create the storage account. Changing this forces a new resource to be created. + +* `location` - (Required) Specifies the supported Azure location where the + resource exists. Changing this forces a new resource to be created. + +* `account_type` - (Required) Defines the type of storage account to be + created. Valid options are `Standard_LRS`, `Standard_ZRS`, `Standard_GRS`, + `Standard_RAGRS`, `Premium_LRS`. Changing this is sometimes valid - see the Azure + documentation for more information on which types of accounts can be converted + into other types. + +* `tags` - (Optional) A mapping of tags to assign to the resource. + +Note that although the Azure API supports setting custom domain names for +storage accounts, this is not currently supported. + +## Attributes Reference + +The following attributes are exported in addition to the arguments listed above: + +* `id` - The storage account Resource ID. +* `primary_location` - The primary location of the storage account. +* `secondary_location` - The secondary location of the storage account. +* `primary_blob_endpoint` - The endpoint URL for blob storage in the primary location. +* `secondary_blob_endpoint` - The endpoint URL for blob storage in the secondary location. +* `primary_queue_endpoint` - The endpoint URL for queue storage in the primary location. +* `secondary_queue_endpoint` - The endpoint URL for queue storage in the secondary location. +* `primary_table_endpoint` - The endpoint URL for table storage in the primary location. +* `secondary_table_endpoint` - The endpoint URL for table storage in the secondary location. +* `primary_file_endpoint` - The endpoint URL for file storage in the primary location. diff --git a/website/source/layouts/azurerm.erb b/website/source/layouts/azurerm.erb index f143c5917d..8bf97c4d4c 100644 --- a/website/source/layouts/azurerm.erb +++ b/website/source/layouts/azurerm.erb @@ -72,6 +72,17 @@ + + > + Storage Resources + + > Virtual Machine Resources