Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New datasource azurerm_storage_table_entities #24973

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/labeler-issue-triage.yml
Expand Up @@ -328,7 +328,7 @@ service/sql:
- '### (|New or )Affected Resource\(s\)\/Data Source\(s\)((.|\n)*)azurerm_sql_((.|\n)*)###'

service/storage:
- '### (|New or )Affected Resource\(s\)\/Data Source\(s\)((.|\n)*)azurerm_(storage_account\W+|storage_account_blob_container_sas\W+|storage_account_customer_managed_key\W+|storage_account_local_user\W+|storage_account_network_rules\W+|storage_account_sas\W+|storage_blob\W+|storage_blob_inventory_policy\W+|storage_container\W+|storage_containers\W+|storage_data_lake_gen2_filesystem\W+|storage_data_lake_gen2_path\W+|storage_encryption_scope\W+|storage_management_policy\W+|storage_object_replication\W+|storage_queue\W+|storage_share\W+|storage_share_directory\W+|storage_share_file\W+|storage_sync\W+|storage_sync_cloud_endpoint\W+|storage_sync_group\W+|storage_table\W+|storage_table_entity\W+)((.|\n)*)###'
- '### (|New or )Affected Resource\(s\)\/Data Source\(s\)((.|\n)*)azurerm_(storage_account\W+|storage_account_blob_container_sas\W+|storage_account_customer_managed_key\W+|storage_account_local_user\W+|storage_account_network_rules\W+|storage_account_sas\W+|storage_blob\W+|storage_blob_inventory_policy\W+|storage_container\W+|storage_containers\W+|storage_data_lake_gen2_filesystem\W+|storage_data_lake_gen2_path\W+|storage_encryption_scope\W+|storage_management_policy\W+|storage_object_replication\W+|storage_queue\W+|storage_share\W+|storage_share_directory\W+|storage_share_file\W+|storage_sync\W+|storage_sync_cloud_endpoint\W+|storage_sync_group\W+|storage_table\W+|storage_table_entities\W+|storage_table_entity\W+)((.|\n)*)###'

service/storagemover:
- '### (|New or )Affected Resource\(s\)\/Data Source\(s\)((.|\n)*)azurerm_storage_mover((.|\n)*)###'
Expand Down
Expand Up @@ -115,8 +115,7 @@ func dataSourceExpressRouteCircuitPeeringRead(d *pluginsdk.ResourceData, meta in
resp, err := client.Get(ctx, id.ResourceGroup, id.ExpressRouteCircuitName, id.PeeringName)
if err != nil {
if utils.ResponseWasNotFound(resp.Response) {
d.SetId("")
return nil
return fmt.Errorf("%s was not found", id)
}
return fmt.Errorf("retrieving %s: %+v", *id, err)
}
Expand Down
50 changes: 50 additions & 0 deletions internal/services/storage/parse/storage_table_entities.go
@@ -0,0 +1,50 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package parse

import (
"crypto/sha1"
"encoding/hex"
"fmt"
"strings"

"github.com/hashicorp/go-azure-helpers/resourcemanager/resourceids"
"github.com/hashicorp/terraform-provider-azurerm/utils"
)

// TODO: tests for this
var _ resourceids.Id = StorageTableEntitiesId{}

type StorageTableEntitiesId struct {
AccountName string
DomainSuffix string
TableName string
Filter string
}

func (id StorageTableEntitiesId) String() string {
components := []string{
fmt.Sprintf("Account Name %q", id.AccountName),
fmt.Sprintf("Domain Suffix %q", id.DomainSuffix),
fmt.Sprintf("TableName %q", id.TableName),
fmt.Sprintf("Filter %q", id.Filter),
}
return fmt.Sprintf("Storage Table %s", strings.Join(components, " / "))
}

func (id StorageTableEntitiesId) ID() string {
return fmt.Sprintf("https://%s.table.%s/%s(%s)", id.AccountName, id.DomainSuffix, id.TableName, id.Filter)
}

func NewStorageTableEntitiesId(accountName, domainSuffix, tablename, filter string) StorageTableEntitiesId {
s := utils.Base64EncodeIfNot(filter)
sha := sha1.Sum([]byte(s))
filterHash := hex.EncodeToString(sha[:])
return StorageTableEntitiesId{
AccountName: accountName,
DomainSuffix: domainSuffix,
TableName: tablename,
Filter: filterHash,
}
}
1 change: 1 addition & 0 deletions internal/services/storage/registration.go
Expand Up @@ -73,6 +73,7 @@ func (r Registration) SupportedResources() map[string]*pluginsdk.Resource {

func (r Registration) DataSources() []sdk.DataSource {
return []sdk.DataSource{
storageTableEntitiesDataSource{},
storageContainersDataSource{},
}
}
Expand Down
213 changes: 213 additions & 0 deletions internal/services/storage/storage_table_entities_data_source.go
@@ -0,0 +1,213 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package storage

import (
"context"
"fmt"
"log"
"strings"
"time"

"github.com/hashicorp/terraform-provider-azurerm/internal/sdk"
"github.com/hashicorp/terraform-provider-azurerm/internal/services/storage/parse"
"github.com/hashicorp/terraform-provider-azurerm/internal/services/storage/validate"
"github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk"
"github.com/hashicorp/terraform-provider-azurerm/internal/tf/validation"
"github.com/tombuildsstuff/giovanni/storage/2020-08-04/table/entities"
)

type storageTableEntitiesDataSource struct{}

var _ sdk.DataSource = storageTableEntitiesDataSource{}

type TableEntitiesDataSourceModel struct {
TableName string `tfschema:"table_name"`
StorageAccountName string `tfschema:"storage_account_name"`
Filter string `tfschema:"filter"`
Items []TableEntitiyDataSourceModel `tfschema:"items"`
}

type TableEntitiyDataSourceModel struct {
PartitionKey string `tfschema:"partition_key"`
RowKey string `tfschema:"row_key"`
Properties map[string]interface{} `tfschema:"properties"`
}

func (k storageTableEntitiesDataSource) Arguments() map[string]*pluginsdk.Schema {
return map[string]*pluginsdk.Schema{
"table_name": {
Type: pluginsdk.TypeString,
Required: true,
ValidateFunc: validate.StorageTableName,
},

"storage_account_name": {
Type: pluginsdk.TypeString,
Required: true,
ValidateFunc: validate.StorageAccountName,
},

"filter": {
Type: pluginsdk.TypeString,
Required: true,
ValidateFunc: validation.StringIsNotEmpty,
},
}
}

func (k storageTableEntitiesDataSource) Attributes() map[string]*pluginsdk.Schema {
return map[string]*pluginsdk.Schema{
"items": {
Type: pluginsdk.TypeList,
Computed: true,
Elem: &pluginsdk.Resource{
Schema: map[string]*pluginsdk.Schema{
"partition_key": {
Type: pluginsdk.TypeString,
Computed: true,
},

"row_key": {
Type: pluginsdk.TypeString,
Computed: true,
},

"properties": {
Type: pluginsdk.TypeMap,
Computed: true,
Elem: &pluginsdk.Schema{
Type: pluginsdk.TypeString,
},
},
},
},
},
}
}

func (k storageTableEntitiesDataSource) ModelObject() interface{} {
return &TableEntitiesDataSourceModel{}
}

func (k storageTableEntitiesDataSource) ResourceType() string {
return "azurerm_storage_table_entities"
}

func (k storageTableEntitiesDataSource) Read() sdk.ResourceFunc {
return sdk.ResourceFunc{
Timeout: 5 * time.Minute,
Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error {
var model TableEntitiesDataSourceModel
if err := metadata.Decode(&model); err != nil {
return err
}

storageClient := metadata.Client.Storage

account, err := storageClient.FindAccount(ctx, model.StorageAccountName)
if err != nil {
return fmt.Errorf("retrieving Account %q for Table %q: %s", model.StorageAccountName, model.TableName, err)
}
if account == nil {
return fmt.Errorf("the parent Storage Account %s was not found", model.StorageAccountName)
}

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

input := entities.QueryEntitiesInput{
Filter: &model.Filter,
MetaDataLevel: entities.MinimalMetaData,
}

id := parse.NewStorageTableEntitiesId(model.StorageAccountName, storageClient.Environment.StorageEndpointSuffix, model.TableName, model.Filter)

result, err := client.Query(ctx, model.StorageAccountName, model.TableName, input)
if err != nil {
return fmt.Errorf("retrieving Entities (Filter %q) (Table %q / Storage Account %q / Resource Group %q): %s", model.Filter, model.TableName, model.StorageAccountName, account.ResourceGroup, err)
}

var flattenedEntities []TableEntitiyDataSourceModel
for _, entity := range result.Entities {
flattenedEntity := flattenEntityWithMetadata(entity)
flattenedEntities = append(flattenedEntities, flattenedEntity)
}
model.Items = flattenedEntities
metadata.SetID(id)

return metadata.Encode(&model)
},
}
}

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

result := TableEntitiyDataSourceModel{}

for k, v := range entity {
properties := map[string]interface{}{}
if k == "PartitionKey" {
result.PartitionKey = v.(string)
continue
}

if k == "RowKey" {
result.RowKey = v.(string)
continue
}
// skip ODATA annotation returned with fullmetadata
if strings.HasPrefix(k, "odata.") || strings.HasSuffix(k, "@odata.type") {
continue
}
if dtype, ok := entity[k+"@odata.type"]; ok {
switch dtype {
case "Edm.Boolean":
properties[k] = fmt.Sprint(v)
case "Edm.Double":
properties[k] = fmt.Sprintf("%f", v)
case "Edm.Int32", "Edm.Int64":
// `v` returned as string for int 64
properties[k] = fmt.Sprint(v)
case "Edm.String":
properties[k] = v
default:
log.Printf("[WARN] key %q with unexpected @odata.type %q", k, dtype)
continue
}

properties[k+"@odata.type"] = dtype
result.Properties = properties
} else {
// special handling for property types that do not require the annotation to be present
// https://docs.microsoft.com/en-us/rest/api/storageservices/payload-format-for-table-service-operations#property-types-in-a-json-feed
switch c := v.(type) {
case bool:
properties[k] = fmt.Sprint(v)
properties[k+"@odata.type"] = "Edm.Boolean"
case float64:
f64 := v.(float64)
if v == float64(int64(f64)) {
properties[k] = fmt.Sprintf("%d", int64(f64))
properties[k+"@odata.type"] = "Edm.Int32"
} else {
// fmt.Sprintf("%f", v) will return `123.123000` for `123.123`, have to use fmt.Sprint
properties[k] = fmt.Sprint(v)
properties[k+"@odata.type"] = "Edm.Double"
}
case string:
properties[k] = v
default:
log.Printf("[WARN] key %q with unexpected type %T", k, c)
}
result.Properties = properties
}
}

return result
}
@@ -0,0 +1,98 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package storage_test

import (
"fmt"
"testing"

"github.com/hashicorp/terraform-provider-azurerm/internal/acceptance"
"github.com/hashicorp/terraform-provider-azurerm/internal/acceptance/check"
)

type StorageTableEntitiesDataSource struct{}

func TestAccDataSourceStorageTableEntities_basic(t *testing.T) {
data := acceptance.BuildTestData(t, "data.azurerm_storage_table_entities", "test")

data.DataSourceTest(t, []acceptance.TestStep{
{
Config: StorageTableEntitiesDataSource{}.basicWithDataSource(data),
Check: acceptance.ComposeTestCheckFunc(
check.That(data.ResourceName).Key("items.#").HasValue("2"),
),
},
})
}

func (d StorageTableEntitiesDataSource) basic(data acceptance.TestData) string {
return fmt.Sprintf(`
provider "azurerm" {
features {}
}

resource "azurerm_resource_group" "test" {
name = "tableentitydstest-%s"
location = "%s"
}

resource "azurerm_storage_account" "test" {
name = "acctesttedsc%s"
resource_group_name = "${azurerm_resource_group.test.name}"

location = "${azurerm_resource_group.test.location}"
account_tier = "Standard"
account_replication_type = "LRS"

allow_nested_items_to_be_public = false
}

resource "azurerm_storage_table" "test" {
name = "tabletesttedsc%s"
storage_account_name = azurerm_storage_account.test.name
}

resource "azurerm_storage_table_entity" "test" {
storage_account_name = azurerm_storage_account.test.name
table_name = azurerm_storage_table.test.name

partition_key = "testpartition"
row_key = "testrow"

entity = {
testkey = "testval"
}
}

resource "azurerm_storage_table_entity" "test2" {
storage_account_name = azurerm_storage_account.test.name
table_name = azurerm_storage_table.test.name

partition_key = "testpartition"
row_key = "testrow2"

entity = {
testkey = "testval2"
}
}
`, data.RandomString, data.Locations.Primary, data.RandomString, data.RandomString)
}

func (d StorageTableEntitiesDataSource) basicWithDataSource(data acceptance.TestData) string {
config := d.basic(data)
return fmt.Sprintf(`
%s

data "azurerm_storage_table_entities" "test" {
table_name = azurerm_storage_table_entity.test.table_name
storage_account_name = azurerm_storage_table_entity.test.storage_account_name
filter = "PartitionKey eq 'testpartition'"

depends_on = [
azurerm_storage_table_entity.test,
azurerm_storage_table_entity.test2,
]
}
`, config)
}