Skip to content

Commit

Permalink
New Resource: azurerm_storage_table_entity (#3831)
Browse files Browse the repository at this point in the history
  • Loading branch information
mbfrahry authored Jul 12, 2019
1 parent bcaffac commit 10eb89e
Show file tree
Hide file tree
Showing 22 changed files with 1,544 additions and 4 deletions.
87 changes: 87 additions & 0 deletions azurerm/internal/authorizers/authorizer_shared_key_lite_table.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package authorizers

import (
"net/http"
"strings"

"github.com/Azure/go-autorest/autorest"
)

// TODO: switch to using the version from github.com/Azure/go-autorest
// once https://github.com/Azure/go-autorest/pull/416 has been merged

// SharedKeyLiteTableAuthorizer implements an authorization for Shared Key Lite
// this can be used for interaction with Table Storage Endpoints
type SharedKeyLiteTableAuthorizer struct {
storageAccountName string
storageAccountKey string
}

// NewSharedKeyLiteAuthorizer crates a SharedKeyLiteAuthorizer using the given credentials
func NewSharedKeyLiteTableAuthorizer(accountName, accountKey string) *SharedKeyLiteTableAuthorizer {
return &SharedKeyLiteTableAuthorizer{
storageAccountName: accountName,
storageAccountKey: accountKey,
}
}

// WithAuthorization returns a PrepareDecorator that adds an HTTP Authorization header whose
// value is "SharedKeyLite " followed by the computed key.
// This can be used for the Blob, Queue, and File Services
//
// from: https://docs.microsoft.com/en-us/rest/api/storageservices/authorize-with-shared-key
// You may use Shared Key Lite authorization to authorize a request made against the
// 2009-09-19 version and later of the Blob and Queue services,
// and version 2014-02-14 and later of the File services.
func (skl *SharedKeyLiteTableAuthorizer) WithAuthorization() autorest.PrepareDecorator {
return func(p autorest.Preparer) autorest.Preparer {
return autorest.PreparerFunc(func(r *http.Request) (*http.Request, error) {
r, err := p.Prepare(r)
if err != nil {
return r, err
}

key, err := buildSharedKeyLiteTable(skl.storageAccountName, skl.storageAccountKey, r)
if err != nil {
return r, err
}

sharedKeyHeader := formatSharedKeyLiteAuthorizationHeader(skl.storageAccountName, *key)
return autorest.Prepare(r, autorest.WithHeader(HeaderAuthorization, sharedKeyHeader))
})
}
}

func buildSharedKeyLiteTable(accountName, storageAccountKey string, r *http.Request) (*string, error) {
// first ensure the relevant headers are configured
prepareHeadersForRequest(r)

sharedKey, err := computeSharedKeyLiteTable(r.URL.String(), accountName, r.Header)
if err != nil {
return nil, err
}

// we then need to HMAC that value
hmacdValue := hmacValue(storageAccountKey, *sharedKey)
return &hmacdValue, nil
}

// computeSharedKeyLite computes the Shared Key Lite required for Storage Authentication
// NOTE: this function assumes that the `x-ms-date` field is set
func computeSharedKeyLiteTable(url string, accountName string, headers http.Header) (*string, error) {
dateHeader := headers.Get("x-ms-date")
canonicalizedResource, err := buildCanonicalizedResource(url, accountName)
if err != nil {
return nil, err
}

canonicalizedString := buildCanonicalizedStringForSharedKeyLiteTable(*canonicalizedResource, dateHeader)
return &canonicalizedString, nil
}

func buildCanonicalizedStringForSharedKeyLiteTable(canonicalizedResource, dateHeader string) string {
return strings.Join([]string{
dateHeader,
canonicalizedResource,
}, "\n")
}
13 changes: 13 additions & 0 deletions azurerm/internal/services/storage/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/authorizers"
"github.com/tombuildsstuff/giovanni/storage/2018-11-09/file/directories"
"github.com/tombuildsstuff/giovanni/storage/2018-11-09/file/shares"
"github.com/tombuildsstuff/giovanni/storage/2018-11-09/table/entities"
)

type Client struct {
Expand Down Expand Up @@ -81,6 +82,18 @@ func (client Client) FileSharesClient(ctx context.Context, resourceGroup, accoun
return &directoriesClient, nil
}

func (client Client) TableEntityClient(ctx context.Context, resourceGroup, accountName string) (*entities.Client, error) {
accountKey, err := client.findAccountKey(ctx, resourceGroup, accountName)
if err != nil {
return nil, fmt.Errorf("Error retrieving Account Key: %s", err)
}

storageAuth := authorizers.NewSharedKeyLiteTableAuthorizer(accountName, *accountKey)
entitiesClient := entities.New()
entitiesClient.Client.Authorizer = storageAuth
return &entitiesClient, nil
}

func (client Client) findAccountKey(ctx context.Context, resourceGroup, accountName string) (*string, error) {
props, err := client.accountsClient.ListKeys(ctx, resourceGroup, accountName)
if err != nil {
Expand Down
1 change: 1 addition & 0 deletions azurerm/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,7 @@ func Provider() terraform.ResourceProvider {
"azurerm_storage_share": resourceArmStorageShare(),
"azurerm_storage_share_directory": resourceArmStorageShareDirectory(),
"azurerm_storage_table": resourceArmStorageTable(),
"azurerm_storage_table_entity": resourceArmStorageTableEntity(),
"azurerm_stream_analytics_job": resourceArmStreamAnalyticsJob(),
"azurerm_stream_analytics_function_javascript_udf": resourceArmStreamAnalyticsFunctionUDF(),
"azurerm_stream_analytics_output_blob": resourceArmStreamAnalyticsOutputBlob(),
Expand Down
204 changes: 204 additions & 0 deletions azurerm/resource_arm_storage_table_entity.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
package azurerm

import (
"fmt"
"log"

"github.com/hashicorp/terraform/helper/schema"
"github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/tf"
"github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/validate"
"github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils"
"github.com/tombuildsstuff/giovanni/storage/2018-11-09/table/entities"
)

func resourceArmStorageTableEntity() *schema.Resource {
return &schema.Resource{
Create: resourceArmStorageTableEntityCreateUpdate,
Read: resourceArmStorageTableEntityRead,
Update: resourceArmStorageTableEntityCreateUpdate,
Delete: resourceArmStorageTableEntityDelete,
Importer: &schema.ResourceImporter{
State: schema.ImportStatePassthrough,
},

Schema: map[string]*schema.Schema{
"table_name": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
ValidateFunc: validateArmStorageTableName,
},
"storage_account_name": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
ValidateFunc: validate.NoEmptyStrings,
},
"partition_key": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
ValidateFunc: validate.NoEmptyStrings,
},
"row_key": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
ValidateFunc: validate.NoEmptyStrings,
},
"entity": {
Type: schema.TypeMap,
Required: true,
Elem: &schema.Schema{
Type: schema.TypeString,
},
},
},
}
}

func resourceArmStorageTableEntityCreateUpdate(d *schema.ResourceData, meta interface{}) error {
ctx := meta.(*ArmClient).StopContext
storageClient := meta.(*ArmClient).storage

accountName := d.Get("storage_account_name").(string)
tableName := d.Get("table_name").(string)
partitionKey := d.Get("partition_key").(string)
rowKey := d.Get("row_key").(string)
entity := d.Get("entity").(map[string]interface{})

resourceGroup, err := storageClient.FindResourceGroup(ctx, accountName)
if err != nil {
return fmt.Errorf("Error locating Resource Group: %s", err)
}

client, err := storageClient.TableEntityClient(ctx, *resourceGroup, accountName)
if err != nil {
return fmt.Errorf("Error building Entity Client: %s", err)
}

if requireResourcesToBeImported {
input := entities.GetEntityInput{
PartitionKey: partitionKey,
RowKey: rowKey,
MetaDataLevel: entities.NoMetaData,
}
existing, err := client.Get(ctx, accountName, tableName, input)
if err != nil {
if !utils.ResponseWasNotFound(existing.Response) {
return fmt.Errorf("Error checking for presence of existing Entity (Partition Key %q / Row Key %q) (Table %q / Storage Account %q / Resource Group %q): %s", partitionKey, rowKey, tableName, accountName, *resourceGroup, err)
}
}

if !utils.ResponseWasNotFound(existing.Response) {
id := client.GetResourceID(accountName, tableName, partitionKey, rowKey)
return tf.ImportAsExistsError("azurerm_storage_table_entity", id)
}
}

input := entities.InsertOrMergeEntityInput{
PartitionKey: partitionKey,
RowKey: rowKey,
Entity: entity,
}

if _, err := client.InsertOrMerge(ctx, accountName, tableName, input); err != nil {
return fmt.Errorf("Error creating Entity (Partition Key %q / Row Key %q) (Table %q / Storage Account %q / Resource Group %q): %+v", partitionKey, rowKey, tableName, accountName, *resourceGroup, err)
}

resourceID := client.GetResourceID(accountName, tableName, partitionKey, rowKey)
d.SetId(resourceID)

return resourceArmStorageTableEntityRead(d, meta)
}

func resourceArmStorageTableEntityRead(d *schema.ResourceData, meta interface{}) error {
ctx := meta.(*ArmClient).StopContext
storageClient := meta.(*ArmClient).storage

id, err := entities.ParseResourceID(d.Id())
if err != nil {
return err
}

resourceGroup, err := storageClient.FindResourceGroup(ctx, id.AccountName)
if err != nil {
return fmt.Errorf("Error locating Resource Group for Storage Account %q: %s", id.AccountName, err)
}
if resourceGroup == nil {
log.Printf("[DEBUG] Unable to locate Resource Group for Storage Account %q - assuming removed & removing from state", id.AccountName)
d.SetId("")
return nil
}

client, err := storageClient.TableEntityClient(ctx, *resourceGroup, id.AccountName)
if err != nil {
return fmt.Errorf("Error building Table Entity Client for Storage Account %q (Resource Group %q): %s", id.AccountName, *resourceGroup, err)
}

input := entities.GetEntityInput{
PartitionKey: id.PartitionKey,
RowKey: id.RowKey,
MetaDataLevel: entities.NoMetaData,
}

result, err := client.Get(ctx, id.AccountName, id.TableName, input)
if err != nil {
return fmt.Errorf("Error retrieving Entity (Partition Key %q / Row Key %q) (Table %q / Storage Account %q / Resource Group %q): %s", id.PartitionKey, id.RowKey, id.TableName, id.AccountName, *resourceGroup, err)
}

d.Set("storage_account_name", id.AccountName)
d.Set("table_name", id.TableName)
d.Set("partition_key", id.PartitionKey)
d.Set("row_key", id.RowKey)
if err := d.Set("entity", flattenEntity(result.Entity)); err != nil {
return fmt.Errorf("Error setting `entity` for Entity (Partition Key %q / Row Key %q) (Table %q / Storage Account %q / Resource Group %q): %s", id.PartitionKey, id.RowKey, id.TableName, id.AccountName, *resourceGroup, err)
}

return nil
}

func resourceArmStorageTableEntityDelete(d *schema.ResourceData, meta interface{}) error {
ctx := meta.(*ArmClient).StopContext
storageClient := meta.(*ArmClient).storage

id, err := entities.ParseResourceID(d.Id())
if err != nil {
return err
}

resourceGroup, err := storageClient.FindResourceGroup(ctx, id.AccountName)
if err != nil {
return fmt.Errorf("Error locating Resource Group for Storage Account %q: %s", id.AccountName, err)
}
if resourceGroup == nil {
log.Printf("[DEBUG] Unable to locate Resource Group for Storage Account %q - assuming removed already", id.AccountName)
d.SetId("")
return nil
}

client, err := storageClient.TableEntityClient(ctx, *resourceGroup, id.AccountName)
if err != nil {
return fmt.Errorf("Error building Entity Client for Storage Account %q (Resource Group %q): %s", id.AccountName, *resourceGroup, err)
}

input := entities.DeleteEntityInput{
PartitionKey: id.PartitionKey,
RowKey: id.RowKey,
}

if _, err := client.Delete(ctx, id.AccountName, id.TableName, input); err != nil {
return fmt.Errorf("Error deleting Entity (Partition Key %q / Row Key %q) (Table %q / Storage Account %q / Resource Group %q): %s", id.PartitionKey, id.RowKey, id.TableName, id.AccountName, *resourceGroup, err)
}

return nil
}

// The api returns extra information that we already have. We'll remove it here before setting it in state.
func flattenEntity(entity map[string]interface{}) map[string]interface{} {
delete(entity, "PartitionKey")
delete(entity, "RowKey")
delete(entity, "Timestamp")

return entity
}
Loading

0 comments on commit 10eb89e

Please sign in to comment.