Skip to content

Commit

Permalink
provider/azurerm: Add azurerm_storage_account
Browse files Browse the repository at this point in the history
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
  • Loading branch information
jen20 committed Jan 21, 2016
1 parent 90b6512 commit f45190b
Show file tree
Hide file tree
Showing 4 changed files with 479 additions and 3 deletions.
21 changes: 19 additions & 2 deletions builtin/providers/azurerm/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -57,7 +58,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)
Expand All @@ -69,6 +70,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 != "" {
Expand Down Expand Up @@ -240,7 +257,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)
Expand Down
3 changes: 2 additions & 1 deletion builtin/providers/azurerm/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ func Provider() terraform.ResourceProvider {
"azurerm_route_table": resourceArmRouteTable(),
"azurerm_route": resourceArmRoute(),
"azurerm_cdn_profile": resourceArmCdnProfile(),
"azurerm_storage_account": resourceArmStorageAccount(),
},
ConfigureFunc: providerConfigure,
}
Expand Down Expand Up @@ -98,7 +99,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)
Expand Down
292 changes: 292 additions & 0 deletions builtin/providers/azurerm/resource_arm_storage_account.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading

0 comments on commit f45190b

Please sign in to comment.