diff --git a/.github/labeler-issue-triage.yml b/.github/labeler-issue-triage.yml index d55c98783de4..93aa7ae0a448 100644 --- a/.github/labeler-issue-triage.yml +++ b/.github/labeler-issue-triage.yml @@ -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)*)###' diff --git a/internal/services/network/express_route_circuit_peering_data_source.go b/internal/services/network/express_route_circuit_peering_data_source.go index 3688b568fbd2..0cfc9f715c74 100644 --- a/internal/services/network/express_route_circuit_peering_data_source.go +++ b/internal/services/network/express_route_circuit_peering_data_source.go @@ -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) } diff --git a/internal/services/storage/parse/storage_table_entities.go b/internal/services/storage/parse/storage_table_entities.go new file mode 100644 index 000000000000..4c18c883d341 --- /dev/null +++ b/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, + } +} diff --git a/internal/services/storage/registration.go b/internal/services/storage/registration.go index 3742836ecc0b..176faafc272e 100644 --- a/internal/services/storage/registration.go +++ b/internal/services/storage/registration.go @@ -73,6 +73,7 @@ func (r Registration) SupportedResources() map[string]*pluginsdk.Resource { func (r Registration) DataSources() []sdk.DataSource { return []sdk.DataSource{ + storageTableEntitiesDataSource{}, storageContainersDataSource{}, } } diff --git a/internal/services/storage/storage_table_entities_data_source.go b/internal/services/storage/storage_table_entities_data_source.go new file mode 100644 index 000000000000..c862ba51aa75 --- /dev/null +++ b/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 +} diff --git a/internal/services/storage/storage_table_entities_data_source_test.go b/internal/services/storage/storage_table_entities_data_source_test.go new file mode 100644 index 000000000000..425f6864672e --- /dev/null +++ b/internal/services/storage/storage_table_entities_data_source_test.go @@ -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) +} diff --git a/website/docs/d/storage_table_entities.html.markdown b/website/docs/d/storage_table_entities.html.markdown new file mode 100644 index 000000000000..cc58bf95530b --- /dev/null +++ b/website/docs/d/storage_table_entities.html.markdown @@ -0,0 +1,53 @@ +--- +subcategory: "Storage" +layout: "azurerm" +page_title: "Azure Resource Manager: azurerm_storage_table_entities" +description: |- + Gets all existing entities from Storage Tablethat match a filter. +--- + +# Data Source: azurerm_storage_table_entity + +Use this data source to access information about an existing Storage Table Entity. + +## Example Usage + +```hcl +data "azurerm_storage_table_entities" "example" { + table_name = "example-table-name" + storage_account_name = "example-storage-account-name" + filter = "PartitionKey eq 'example'" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `table_name` - The name of the Table. + +* `storage_account_name` - The name of the Storage Account where the Table exists. + +* `filter` - The filter used to retrieve the entities. + +## Attributes Reference + +* `id` - The ID of the storage table entity. + +* `items` - A list of `items` blocks as defined below. + +--- + +Each element in `items` block exports the following: + +* `partition_key` - Partition Key of the Entity. + +* `row_key` - Row Key of the Entity. + +* `properties` - A map of any additional properties in key-value format. + +## Timeouts + +The `timeouts` block allows you to specify [timeouts](https://www.terraform.io/language/resources/syntax#operation-timeouts) for certain actions: + +* `read` - (Defaults to 5 minutes) Used when retrieving the Storage Table Entity.