Skip to content

Commit

Permalink
New datasource azurerm_storage_table_entities (#24973)
Browse files Browse the repository at this point in the history
* New datasource `azurerm_storage_table_entities`

Allows retrieving multiple storage table entities based on a filter
query.

```
✗ TF_ACC=1 go test -v ./internal/services/storage -timeout=1000m -run='TestAccDataSourceStorageTableEntities_basic'
=== RUN   TestAccDataSourceStorageTableEntities_basic
=== PAUSE TestAccDataSourceStorageTableEntities_basic
=== CONT  TestAccDataSourceStorageTableEntities_basic
--- PASS: TestAccDataSourceStorageTableEntities_basic (146.35s)
PASS
ok  	github.com/hashicorp/terraform-provider-azurerm/internal/services/storage	149.796s
```

* Fix schema semantics

* make generate

* Fix express_route_circuit_peering datasource

* terrafmt
  • Loading branch information
favoretti authored Feb 22, 2024
1 parent ec71e1c commit d68b999
Show file tree
Hide file tree
Showing 7 changed files with 417 additions and 3 deletions.
2 changes: 1 addition & 1 deletion .github/labeler-issue-triage.yml
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -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)
}
Loading

0 comments on commit d68b999

Please sign in to comment.