diff --git a/.github/labeler-issue-triage.yml b/.github/labeler-issue-triage.yml index ce59598b643d..276d2ffbfbdf 100644 --- a/.github/labeler-issue-triage.yml +++ b/.github/labeler-issue-triage.yml @@ -103,7 +103,7 @@ service/database-migration: - '### (|New or )Affected Resource\(s\)\/Data Source\(s\)((.|\n)*)azurerm_database_migration_((.|\n)*)###' service/databox-edge: - - '### (|New or )Affected Resource\(s\)\/Data Source\(s\)((.|\n)*)azurerm_databox_edge_((.|\n)*)###' + - '### (|New or )Affected Resource\(s\)\/Data Source\(s\)((.|\n)*)azurerm_databox_edge_device((.|\n)*)###' service/databricks: - '### (|New or )Affected Resource\(s\)\/Data Source\(s\)((.|\n)*)azurerm_databricks_((.|\n)*)###' @@ -121,7 +121,6 @@ service/digital-twins: - '### (|New or )Affected Resource\(s\)\/Data Source\(s\)((.|\n)*)azurerm_digital_twins_((.|\n)*)###' service/disks: - - '### (|New or )Affected Resource\(s\)\/Data Source\(s\)((.|\n)*)azurerm_disk_pool((.|\n)*)###' service/dns: - '### (|New or )Affected Resource\(s\)\/Data Source\(s\)((.|\n)*)azurerm_(dns_|private_dns_a_record\W+|private_dns_aaaa_record\W+|private_dns_cname_record\W+|private_dns_mx_record\W+|private_dns_ptr_record\W+|private_dns_soa_record\W+|private_dns_srv_record\W+|private_dns_txt_record\W+|private_dns_zone\W+|private_dns_zone_virtual_network_link\W+)((.|\n)*)###' @@ -154,7 +153,7 @@ service/frontdoor: - '### (|New or )Affected Resource\(s\)\/Data Source\(s\)((.|\n)*)azurerm_frontdoor((.|\n)*)###' service/graph: - - '### (|New or )Affected Resource\(s\)\/Data Source\(s\)((.|\n)*)azurerm_graph_((.|\n)*)###' + - '### (|New or )Affected Resource\(s\)\/Data Source\(s\)((.|\n)*)azurerm_graph_services_account((.|\n)*)###' service/hdinsight: - '### (|New or )Affected Resource\(s\)\/Data Source\(s\)((.|\n)*)azurerm_hdinsight_((.|\n)*)###' @@ -166,7 +165,7 @@ service/hsm: - '### (|New or )Affected Resource\(s\)\/Data Source\(s\)((.|\n)*)azurerm_dedicated_hardware_security_module((.|\n)*)###' service/hybrid-compute: - - '### (|New or )Affected Resource\(s\)\/Data Source\(s\)((.|\n)*)azurerm_(arc_machine\W+|arc_machine_extension\W+|arc_private_link_scope\W+|hybrid_compute_machine)((.|\n)*)###' + - '### (|New or )Affected Resource\(s\)\/Data Source\(s\)((.|\n)*)azurerm_(arc_machine\W+|arc_machine_extension\W+|arc_private_link_scope\W+)((.|\n)*)###' service/iot-central: - '### (|New or )Affected Resource\(s\)\/Data Source\(s\)((.|\n)*)azurerm_iotcentral_((.|\n)*)###' @@ -175,7 +174,6 @@ service/iot-hub: - '### (|New or )Affected Resource\(s\)\/Data Source\(s\)((.|\n)*)azurerm_iothub((.|\n)*)###' service/iot-time-series: - - '### (|New or )Affected Resource\(s\)\/Data Source\(s\)((.|\n)*)azurerm_iot_time_series_insights_((.|\n)*)###' service/key-vault: - '### (|New or )Affected Resource\(s\)\/Data Source\(s\)((.|\n)*)azurerm_(key_vault\W+|key_vault_access_policy\W+|key_vault_certificate\W+|key_vault_certificate_contacts\W+|key_vault_certificate_data\W+|key_vault_certificate_issuer\W+|key_vault_certificates\W+|key_vault_encrypted_value\W+|key_vault_key\W+|key_vault_managed_storage_account\W+|key_vault_managed_storage_account_sas_token_definition\W+|key_vault_secret\W+|key_vault_secrets\W+)((.|\n)*)###' @@ -184,7 +182,6 @@ service/kusto: - '### (|New or )Affected Resource\(s\)\/Data Source\(s\)((.|\n)*)azurerm_kusto_((.|\n)*)###' service/labservice: - - '### (|New or )Affected Resource\(s\)\/Data Source\(s\)((.|\n)*)azurerm_lab_service_((.|\n)*)###' service/lighthouse: - '### (|New or )Affected Resource\(s\)\/Data Source\(s\)((.|\n)*)azurerm_lighthouse_((.|\n)*)###' @@ -199,10 +196,9 @@ service/log-analytics: - '### (|New or )Affected Resource\(s\)\/Data Source\(s\)((.|\n)*)azurerm_log_analytics_((.|\n)*)###' service/logic: - - '### (|New or )Affected Resource\(s\)\/Data Source\(s\)((.|\n)*)azurerm_(integration_service_environment|logic_app_)((.|\n)*)###' + - '### (|New or )Affected Resource\(s\)\/Data Source\(s\)((.|\n)*)azurerm_logic_app_((.|\n)*)###' service/logz: - - '### (|New or )Affected Resource\(s\)\/Data Source\(s\)((.|\n)*)azurerm_logz_((.|\n)*)###' service/machine-learning: - '### (|New or )Affected Resource\(s\)\/Data Source\(s\)((.|\n)*)azurerm_machine_learning_((.|\n)*)###' @@ -226,7 +222,7 @@ service/maria-db: - '### (|New or )Affected Resource\(s\)\/Data Source\(s\)((.|\n)*)azurerm_mariadb_((.|\n)*)###' service/media: - - '### (|New or )Affected Resource\(s\)\/Data Source\(s\)((.|\n)*)azurerm_media_((.|\n)*)###' + - '### (|New or )Affected Resource\(s\)\/Data Source\(s\)((.|\n)*)azurerm_media_services_account_filter((.|\n)*)###' service/mixed-reality: - '### (|New or )Affected Resource\(s\)\/Data Source\(s\)((.|\n)*)azurerm_spatial_anchors_account((.|\n)*)###' @@ -244,7 +240,7 @@ service/mssqlmanagedinstance: - '### (|New or )Affected Resource\(s\)\/Data Source\(s\)((.|\n)*)azurerm_mssql_managed_((.|\n)*)###' service/mysql: - - '### (|New or )Affected Resource\(s\)\/Data Source\(s\)((.|\n)*)azurerm_mysql_((.|\n)*)###' + - '### (|New or )Affected Resource\(s\)\/Data Source\(s\)((.|\n)*)azurerm_mysql_flexible_((.|\n)*)###' service/netapp: - '### (|New or )Affected Resource\(s\)\/Data Source\(s\)((.|\n)*)azurerm_netapp_((.|\n)*)###' @@ -271,7 +267,7 @@ service/policy: - '### (|New or )Affected Resource\(s\)\/Data Source\(s\)((.|\n)*)azurerm_(management_group_policy_|policy_|resource_group_policy_assignment\W+|resource_group_policy_exemption\W+|resource_group_policy_remediation\W+|resource_policy_assignment\W+|resource_policy_exemption\W+|resource_policy_remediation\W+|subscription_policy_)((.|\n)*)###' service/portal: - - '### (|New or )Affected Resource\(s\)\/Data Source\(s\)((.|\n)*)azurerm_(dashboard\W+|portal_)((.|\n)*)###' + - '### (|New or )Affected Resource\(s\)\/Data Source\(s\)((.|\n)*)azurerm_portal_((.|\n)*)###' service/postgresql: - '### (|New or )Affected Resource\(s\)\/Data Source\(s\)((.|\n)*)azurerm_postgresql_((.|\n)*)###' @@ -328,10 +324,9 @@ service/spring: - '### (|New or )Affected Resource\(s\)\/Data Source\(s\)((.|\n)*)azurerm_(spring_cloud_accelerator\W+|spring_cloud_active_deployment\W+|spring_cloud_api_portal\W+|spring_cloud_api_portal_custom_domain\W+|spring_cloud_app\W+|spring_cloud_app_cosmosdb_association\W+|spring_cloud_app_dynamics_application_performance_monitoring\W+|spring_cloud_app_mysql_association\W+|spring_cloud_app_redis_association\W+|spring_cloud_application_insights_application_performance_monitoring\W+|spring_cloud_application_live_view\W+|spring_cloud_build_deployment\W+|spring_cloud_build_pack_binding\W+|spring_cloud_builder\W+|spring_cloud_certificate\W+|spring_cloud_configuration_service\W+|spring_cloud_container_deployment\W+|spring_cloud_custom_domain\W+|spring_cloud_customized_accelerator\W+|spring_cloud_dev_tool_portal\W+|spring_cloud_dynatrace_application_performance_monitoring\W+|spring_cloud_elastic_application_performance_monitoring\W+|spring_cloud_gateway\W+|spring_cloud_gateway_custom_domain\W+|spring_cloud_gateway_route_config\W+|spring_cloud_java_deployment\W+|spring_cloud_new_relic_application_performance_monitoring\W+|spring_cloud_service\W+|spring_cloud_storage\W+)((.|\n)*)###' 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_container_immutability_policy\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_sync_server_endpoint\W+|storage_table\W+|storage_table\W+|storage_table_entities\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_queue_properties\W+|storage_account_sas\W+|storage_account_static_website\W+|storage_blob\W+|storage_blob_inventory_policy\W+|storage_container\W+|storage_container_immutability_policy\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_sync_server_endpoint\W+|storage_table\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)*)###' @@ -352,7 +347,6 @@ service/traffic-manager: - '### (|New or )Affected Resource\(s\)\/Data Source\(s\)((.|\n)*)azurerm_traffic_manager_((.|\n)*)###' service/video-analyzer: - - '### (|New or )Affected Resource\(s\)\/Data Source\(s\)((.|\n)*)azurerm_video_analyzer((.|\n)*)###' service/virtual-desktops: - '### (|New or )Affected Resource\(s\)\/Data Source\(s\)((.|\n)*)azurerm_virtual_desktop_((.|\n)*)###' diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f733bb0cce2..f1de63719280 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,17 @@ +## 3.117.0 (November 7, 2024) + +SPECIAL NOTES: This 3.x release is a special, one-off, back-port of functionality for `azurerm_storage_account` to enable users to deploy this resource in environments which block / are restrictive of Data Plane access, thus preventing the resource being created and/or managed. This functionality is back-ported from the `v4.9.0` release. Users migrating from this release to the 4.x line, should upgrade directly to `v4.9.0` or later, as these features are not compatible with earlier releases of 4.x. + +FEATURES: + +* **New Resource:** `azurerm_storage_account_queue_properties` ([#27819](https://github.com/hashicorp/terraform-provider-azurerm/pull/27819)) +* **New Resource:** `azurerm_storage_account_static_website` ([#27819](https://github.com/hashicorp/terraform-provider-azurerm/pull/27819)) +* New Provider Feature - storage `data_plane_available` feature flag ([#27819](https://github.com/hashicorp/terraform-provider-azurerm/pull/27819)) + +ENHANCEMENTS: + +* `azurerm_storage_account` - can now be created and managed if Data Plane endpoints are blocked by a firewall ([#27819](https://github.com/hashicorp/terraform-provider-azurerm/pull/27819)) + ## 3.116.0 (August 16, 2024) DEPRECATIONS: diff --git a/go.mod b/go.mod index bcaf2c387c7e..636a1ccc2d54 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,7 @@ require ( github.com/google/uuid v1.6.0 github.com/hashicorp/go-azure-helpers v0.70.1 github.com/hashicorp/go-azure-sdk/resource-manager v0.20240731.1212841 - github.com/hashicorp/go-azure-sdk/sdk v0.20240731.1212841 + github.com/hashicorp/go-azure-sdk/sdk v0.20241025.1143247 // out of sync with rm due to drift of versions but required network fixes github.com/hashicorp/go-hclog v1.6.3 github.com/hashicorp/go-multierror v1.1.1 github.com/hashicorp/go-uuid v1.0.3 diff --git a/go.sum b/go.sum index 7dc7466ae565..9afc02e18604 100644 --- a/go.sum +++ b/go.sum @@ -95,8 +95,8 @@ github.com/hashicorp/go-azure-helpers v0.70.1 h1:7hlnRrZobMZxpOzdlNEsayzAayj/KRG github.com/hashicorp/go-azure-helpers v0.70.1/go.mod h1:BmbF4JDYXK5sEmFeU5hcn8Br21uElcqLfdQxjatwQKw= github.com/hashicorp/go-azure-sdk/resource-manager v0.20240731.1212841 h1:H7BkxZl0qitdWq7sEGzNqkn5/11YTamwq2XTI8/7Jq0= github.com/hashicorp/go-azure-sdk/resource-manager v0.20240731.1212841/go.mod h1:/4Ly9Gppp/Nu9AaWDfod6atYQ4n2OqN0ERpE2xZQz8A= -github.com/hashicorp/go-azure-sdk/sdk v0.20240731.1212841 h1:RzWuy96j/7q3Vi2aZoiIrokm8yotUNX1UGD3pbWi5Ck= -github.com/hashicorp/go-azure-sdk/sdk v0.20240731.1212841/go.mod h1:dMKF6bXrgGmy1d3pLzkmBpG2JIHgSAV2/OMSCEgyMwE= +github.com/hashicorp/go-azure-sdk/sdk v0.20241025.1143247 h1:NoYFgxtEsxHhE6TyJ6DRXqHLcxZ0cmrpGxNPio0lT84= +github.com/hashicorp/go-azure-sdk/sdk v0.20241025.1143247/go.mod h1:dMKF6bXrgGmy1d3pLzkmBpG2JIHgSAV2/OMSCEgyMwE= github.com/hashicorp/go-checkpoint v0.5.0 h1:MFYpPZCnQqQTE18jFwSII6eUQrD/oxMFp3mlgcqk5mU= github.com/hashicorp/go-checkpoint v0.5.0/go.mod h1:7nfLNL10NsxqO4iWuW6tWW0HjZuDrwkBuEQsVcpCOgg= github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= diff --git a/internal/clients/graph/client.go b/internal/clients/graph/client.go index 63131098ca5a..d70ee4597a66 100644 --- a/internal/clients/graph/client.go +++ b/internal/clients/graph/client.go @@ -41,7 +41,7 @@ type directoryObjectModel struct { } func graphClient(authorizer auth.Authorizer, environment environments.Environment) (*msgraph.Client, error) { - client, err := msgraph.NewMsGraphClient(environment.MicrosoftGraph, "Graph", msgraph.VersionOnePointZero) + client, err := msgraph.NewClient(environment.MicrosoftGraph, "Graph", msgraph.VersionOnePointZero) if err != nil { return nil, fmt.Errorf("building client: %+v", err) } diff --git a/internal/features/defaults.go b/internal/features/defaults.go index 12c24186eb3c..fbd2e8445a9e 100644 --- a/internal/features/defaults.go +++ b/internal/features/defaults.go @@ -62,6 +62,9 @@ func Default() UserFeatures { RollInstancesWhenRequired: true, ScaleToZeroOnDelete: true, }, + Storage: StorageFeatures{ + DataPlaneAvailable: true, + }, Subscription: SubscriptionFeatures{ PreventCancellationOnDestroy: false, }, diff --git a/internal/features/user_flags.go b/internal/features/user_flags.go index 4b367743ae89..181f423fcc15 100644 --- a/internal/features/user_flags.go +++ b/internal/features/user_flags.go @@ -16,6 +16,7 @@ type UserFeatures struct { ResourceGroup ResourceGroupFeatures RecoveryServicesVault RecoveryServicesVault ManagedDisk ManagedDiskFeatures + Storage StorageFeatures Subscription SubscriptionFeatures PostgresqlFlexibleServer PostgresqlFlexibleServerFeatures MachineLearning MachineLearningFeatures @@ -84,6 +85,10 @@ type AppConfigurationFeatures struct { RecoverSoftDeleted bool } +type StorageFeatures struct { + DataPlaneAvailable bool +} + type SubscriptionFeatures struct { PreventCancellationOnDestroy bool } diff --git a/internal/provider/features.go b/internal/provider/features.go index 38ef20645d16..425b12f2a440 100644 --- a/internal/provider/features.go +++ b/internal/provider/features.go @@ -309,6 +309,21 @@ func schemaFeatures(supportLegacyTestSuite bool) *pluginsdk.Schema { }, }, + "storage": { + Type: pluginsdk.TypeList, + Optional: true, + MaxItems: 1, + Elem: &pluginsdk.Resource{ + Schema: map[string]*schema.Schema{ + "data_plane_available": { + Type: pluginsdk.TypeBool, + Optional: true, + Default: true, + }, + }, + }, + }, + "subscription": { Type: pluginsdk.TypeList, Optional: true, @@ -581,6 +596,16 @@ func expandFeatures(input []interface{}) features.UserFeatures { } } + if raw, ok := val["storage"]; ok { + items := raw.([]interface{}) + if len(items) > 0 { + storageRaw := items[0].(map[string]interface{}) + if v, ok := storageRaw["data_plane_available"]; ok { + featuresMap.Storage.DataPlaneAvailable = v.(bool) + } + } + } + if raw, ok := val["subscription"]; ok { items := raw.([]interface{}) if len(items) > 0 { diff --git a/internal/provider/features_test.go b/internal/provider/features_test.go index 44414fd56278..c3753511b86f 100644 --- a/internal/provider/features_test.go +++ b/internal/provider/features_test.go @@ -75,6 +75,9 @@ func TestExpandFeatures(t *testing.T) { RecoveryServicesVault: features.RecoveryServicesVault{ RecoverSoftDeletedBackupProtectedVM: true, }, + Storage: features.StorageFeatures{ + DataPlaneAvailable: true, + }, Subscription: features.SubscriptionFeatures{ PreventCancellationOnDestroy: false, }, @@ -156,6 +159,11 @@ func TestExpandFeatures(t *testing.T) { "recover_soft_deleted_backup_protected_vm": true, }, }, + "storage": []interface{}{ + map[string]interface{}{ + "data_plane_available": true, + }, + }, "subscription": []interface{}{ map[string]interface{}{ "prevent_cancellation_on_destroy": true, @@ -235,6 +243,9 @@ func TestExpandFeatures(t *testing.T) { RecoveryServicesVault: features.RecoveryServicesVault{ RecoverSoftDeletedBackupProtectedVM: true, }, + Storage: features.StorageFeatures{ + DataPlaneAvailable: true, + }, Subscription: features.SubscriptionFeatures{ PreventCancellationOnDestroy: true, }, @@ -331,6 +342,11 @@ func TestExpandFeatures(t *testing.T) { "recover_soft_deleted_backup_protected_vm": false, }, }, + "storage": []interface{}{ + map[string]interface{}{ + "data_plane_available": false, + }, + }, "subscription": []interface{}{ map[string]interface{}{ "prevent_cancellation_on_destroy": false, @@ -410,6 +426,9 @@ func TestExpandFeatures(t *testing.T) { RecoveryServicesVault: features.RecoveryServicesVault{ RecoverSoftDeletedBackupProtectedVM: false, }, + Storage: features.StorageFeatures{ + DataPlaneAvailable: false, + }, Subscription: features.SubscriptionFeatures{ PreventCancellationOnDestroy: false, }, @@ -1431,6 +1450,54 @@ func TestExpandFeaturesManagedDisk(t *testing.T) { } } +func TestExpandFeaturesStorage(t *testing.T) { + testData := []struct { + Name string + Input []interface{} + EnvVars map[string]interface{} + Expected features.UserFeatures + }{ + { + Name: "Empty Block", + Input: []interface{}{ + map[string]interface{}{ + "storage": []interface{}{}, + }, + }, + Expected: features.UserFeatures{ + Storage: features.StorageFeatures{ + DataPlaneAvailable: true, + }, + }, + }, + { + Name: "Storage Data Plane on Create is Disabled", + Input: []interface{}{ + map[string]interface{}{ + "storage": []interface{}{ + map[string]interface{}{ + "data_plane_available": false, + }, + }, + }, + }, + Expected: features.UserFeatures{ + Storage: features.StorageFeatures{ + DataPlaneAvailable: false, + }, + }, + }, + } + + for _, testCase := range testData { + t.Logf("[DEBUG] Test Case: %q", testCase.Name) + result := expandFeatures(testCase.Input) + if !reflect.DeepEqual(result.Storage, testCase.Expected.Storage) { + t.Fatalf("Expected %+v but got %+v", result.Storage, testCase.Expected.Storage) + } + } +} + func TestExpandFeaturesSubscription(t *testing.T) { testData := []struct { Name string diff --git a/internal/provider/framework/config.go b/internal/provider/framework/config.go index d1664c6b06ff..8e35270b4370 100644 --- a/internal/provider/framework/config.go +++ b/internal/provider/framework/config.go @@ -409,6 +409,19 @@ func (p *ProviderConfig) Load(ctx context.Context, data *ProviderModel, tfVersio f.ManagedDisk.ExpandWithoutDowntime = true } + if !features.Storage.IsNull() && !features.Storage.IsUnknown() { + var feature []Storage + d := features.Storage.ElementsAs(ctx, &feature, true) + diags.Append(d...) + if diags.HasError() { + return + } + f.Storage.DataPlaneAvailable = true + if !feature[0].DataPlaneAvailable.IsNull() && !feature[0].DataPlaneAvailable.IsUnknown() { + f.Storage.DataPlaneAvailable = feature[0].DataPlaneAvailable.ValueBool() + } + } + if !features.Subscription.IsNull() && !features.Subscription.IsUnknown() { var feature []Subscription d := features.Subscription.ElementsAs(ctx, &feature, true) diff --git a/internal/provider/framework/config_test.go b/internal/provider/framework/config_test.go index bdb681400d98..a5a72fabca5a 100644 --- a/internal/provider/framework/config_test.go +++ b/internal/provider/framework/config_test.go @@ -13,6 +13,8 @@ import ( "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + + providerfeatures "github.com/hashicorp/terraform-provider-azurerm/internal/features" ) var ( @@ -137,8 +139,16 @@ func TestProviderConfig_LoadDefault(t *testing.T) { t.Errorf("expected key_vault.recover_soft_deleted_hsm_keys to be true") } - if !features.LogAnalyticsWorkspace.PermanentlyDeleteOnDestroy { - t.Errorf("expected log_analytics_workspace.permanently_delete_on_destroy to be true") + if !providerfeatures.FourPointOhBeta() { + if !features.LogAnalyticsWorkspace.PermanentlyDeleteOnDestroy { + t.Errorf("expected log_analytics_workspace.permanently_delete_on_destroy to be true") + } + } + + if providerfeatures.FourPointOhBeta() { + if features.LogAnalyticsWorkspace.PermanentlyDeleteOnDestroy { + t.Errorf("expected log_analytics_workspace.permanently_delete_on_destroy to be false") + } } if features.TemplateDeployment.DeleteNestedItemsDuringDeletion { @@ -200,6 +210,10 @@ func TestProviderConfig_LoadDefault(t *testing.T) { if features.RecoveryService.PurgeProtectedItemsFromVaultOnDestroy { t.Errorf("expected recovery_service.PurgeProtectedItemsFromVaultOnDestroy to be false") } + + if !features.Storage.DataPlaneAvailable { + t.Errorf("expected storage.DataPlaneAvailable to be true") + } } // TODO - helper functions to make setting up test date more easily so we can add more configuration coverage @@ -275,6 +289,11 @@ func defaultFeaturesList() types.List { }) managedDiskList, _ := basetypes.NewListValue(types.ObjectType{}.WithAttributeTypes(ManagedDiskAttributes), []attr.Value{managedDisk}) + storage, _ := basetypes.NewObjectValueFrom(context.Background(), StorageAttributes, map[string]attr.Value{ + "data_plane_available": basetypes.NewBoolNull(), + }) + storageList, _ := basetypes.NewListValue(types.ObjectType{}.WithAttributeTypes(StorageAttributes), []attr.Value{storage}) + subscription, _ := basetypes.NewObjectValueFrom(context.Background(), SubscriptionAttributes, map[string]attr.Value{ "prevent_cancellation_on_destroy": basetypes.NewBoolNull(), }) @@ -314,6 +333,7 @@ func defaultFeaturesList() types.List { "virtual_machine_scale_set": virtualMachineScaleSetList, "resource_group": resourceGroupList, "managed_disk": managedDiskList, + "storage": storageList, "subscription": subscriptionList, "postgresql_flexible_server": postgresqlFlexibleServerList, "machine_learning": machineLearningList, diff --git a/internal/provider/framework/model.go b/internal/provider/framework/model.go index 59a8923b8f38..84388a4b5d57 100644 --- a/internal/provider/framework/model.go +++ b/internal/provider/framework/model.go @@ -52,6 +52,7 @@ type Features struct { VirtualMachineScaleSet types.List `tfsdk:"virtual_machine_scale_set"` ResourceGroup types.List `tfsdk:"resource_group"` ManagedDisk types.List `tfsdk:"managed_disk"` + Storage types.List `tfsdk:"storage"` Subscription types.List `tfsdk:"subscription"` PostgresqlFlexibleServer types.List `tfsdk:"postgresql_flexible_server"` MachineLearning types.List `tfsdk:"machine_learning"` @@ -73,6 +74,7 @@ var FeaturesAttributes = map[string]attr.Type{ "virtual_machine_scale_set": types.ListType{}.WithElementType(types.ObjectType{}.WithAttributeTypes(VirtualMachineScaleSetAttributes)), "resource_group": types.ListType{}.WithElementType(types.ObjectType{}.WithAttributeTypes(ResourceGroupAttributes)), "managed_disk": types.ListType{}.WithElementType(types.ObjectType{}.WithAttributeTypes(ManagedDiskAttributes)), + "storage": types.ListType{}.WithElementType(types.ObjectType{}.WithAttributeTypes(StorageAttributes)), "subscription": types.ListType{}.WithElementType(types.ObjectType{}.WithAttributeTypes(SubscriptionAttributes)), "postgresql_flexible_server": types.ListType{}.WithElementType(types.ObjectType{}.WithAttributeTypes(PostgresqlFlexibleServerAttributes)), "machine_learning": types.ListType{}.WithElementType(types.ObjectType{}.WithAttributeTypes(MachineLearningAttributes)), @@ -204,6 +206,14 @@ var ManagedDiskAttributes = map[string]attr.Type{ "expand_without_downtime": types.BoolType, } +type Storage struct { + DataPlaneAvailable types.Bool `tfsdk:"data_plane_available"` +} + +var StorageAttributes = map[string]attr.Type{ + "data_plane_available": types.BoolType, +} + type Subscription struct { PreventCancellationOnDestroy types.Bool `tfsdk:"prevent_cancellation_on_destroy"` } diff --git a/internal/provider/framework/provider.go b/internal/provider/framework/provider.go index 6a92879b7e44..a14a4a7890e9 100644 --- a/internal/provider/framework/provider.go +++ b/internal/provider/framework/provider.go @@ -407,6 +407,15 @@ func (p *azureRmFrameworkProvider) Schema(_ context.Context, _ provider.SchemaRe }, }, }, + "storage": schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "data_plane_available": schema.BoolAttribute{ + Optional: true, + }, + }, + }, + }, "subscription": schema.ListNestedBlock{ NestedObject: schema.NestedBlockObject{ Attributes: map[string]schema.Attribute{ diff --git a/internal/services/storage/custompollers/data_plane_blob_containers_availability_poller.go b/internal/services/storage/custompollers/data_plane_blob_containers_availability_poller.go index 4d2426b5b069..fb0cf8236555 100644 --- a/internal/services/storage/custompollers/data_plane_blob_containers_availability_poller.go +++ b/internal/services/storage/custompollers/data_plane_blob_containers_availability_poller.go @@ -35,6 +35,11 @@ func NewDataPlaneBlobContainersAvailabilityPoller(ctx context.Context, client *s func (d *DataPlaneBlobContainersAvailabilityPoller) Poll(ctx context.Context) (*pollers.PollResult, error) { resp, err := d.client.GetServiceProperties(ctx, d.accountName) if err != nil { + if resp.HttpResponse == nil { + return nil, pollers.PollingDroppedConnectionError{ + Message: err.Error(), + } + } if !response.WasNotFound(resp.HttpResponse) { return nil, pollers.PollingFailedError{ Message: err.Error(), diff --git a/internal/services/storage/custompollers/data_plane_queues_availability_poller.go b/internal/services/storage/custompollers/data_plane_queues_availability_poller.go index 5d2257b53033..ac589bd611f1 100644 --- a/internal/services/storage/custompollers/data_plane_queues_availability_poller.go +++ b/internal/services/storage/custompollers/data_plane_queues_availability_poller.go @@ -5,6 +5,7 @@ package custompollers import ( "context" + "errors" "fmt" "time" @@ -35,6 +36,10 @@ func NewDataPlaneQueuesAvailabilityPoller(ctx context.Context, client *storageCl func (d *DataPlaneQueuesAvailabilityPoller) Poll(ctx context.Context) (*pollers.PollResult, error) { resp, err := d.client.GetServiceProperties(ctx) + var e pollers.PollingDroppedConnectionError + if errors.As(err, &e) { + return nil, err + } if err != nil { return nil, pollers.PollingFailedError{ Message: err.Error(), diff --git a/internal/services/storage/custompollers/data_plane_static_website_availability_poller.go b/internal/services/storage/custompollers/data_plane_static_website_availability_poller.go index c13667a11c70..9f4f4f718c2e 100644 --- a/internal/services/storage/custompollers/data_plane_static_website_availability_poller.go +++ b/internal/services/storage/custompollers/data_plane_static_website_availability_poller.go @@ -39,6 +39,11 @@ func (d *DataPlaneStaticWebsiteAvailabilityPoller) Poll(ctx context.Context) (*p resp, err := d.client.GetServiceProperties(ctx, d.storageAccountId.StorageAccountName) if err != nil { if !response.WasNotFound(resp.HttpResponse) { + if resp.HttpResponse == nil { + return nil, pollers.PollingDroppedConnectionError{ + Message: err.Error(), + } + } return nil, pollers.PollingFailedError{ Message: err.Error(), HttpResponse: &client.Response{ diff --git a/internal/services/storage/registration.go b/internal/services/storage/registration.go index 1af5be1cc41f..eccf0480f205 100644 --- a/internal/services/storage/registration.go +++ b/internal/services/storage/registration.go @@ -82,6 +82,8 @@ func (r Registration) DataSources() []sdk.DataSource { func (r Registration) Resources() []sdk.Resource { return []sdk.Resource{ + AccountQueuePropertiesResource{}, + AccountStaticWebsiteResource{}, LocalUserResource{}, StorageContainerImmutabilityPolicyResource{}, SyncServerEndpointResource{}, diff --git a/internal/services/storage/shim/queues_data_plane.go b/internal/services/storage/shim/queues_data_plane.go index 8b52b1e9d068..9c9f67796f9e 100644 --- a/internal/services/storage/shim/queues_data_plane.go +++ b/internal/services/storage/shim/queues_data_plane.go @@ -8,6 +8,7 @@ import ( "github.com/hashicorp/go-azure-helpers/lang/pointer" "github.com/hashicorp/go-azure-helpers/lang/response" + "github.com/hashicorp/go-azure-sdk/sdk/client/pollers" "github.com/tombuildsstuff/giovanni/storage/2023-11-03/queue/queues" ) @@ -62,6 +63,11 @@ func (w DataPlaneStorageQueueWrapper) Get(ctx context.Context, queueName string) func (w DataPlaneStorageQueueWrapper) GetServiceProperties(ctx context.Context) (*queues.StorageServiceProperties, error) { serviceProps, err := w.client.GetServiceProperties(ctx) if err != nil { + if serviceProps.HttpResponse == nil { + return nil, pollers.PollingDroppedConnectionError{ + Message: err.Error(), + } + } if response.WasNotFound(serviceProps.HttpResponse) { return nil, nil } diff --git a/internal/services/storage/shim/shares_data_plane.go b/internal/services/storage/shim/shares_data_plane.go index a251059fd1b3..1ed00bbd05f9 100644 --- a/internal/services/storage/shim/shares_data_plane.go +++ b/internal/services/storage/shim/shares_data_plane.go @@ -9,6 +9,7 @@ import ( "github.com/hashicorp/go-azure-helpers/lang/pointer" "github.com/hashicorp/go-azure-helpers/lang/response" + "github.com/hashicorp/go-azure-sdk/sdk/client/pollers" "github.com/tombuildsstuff/giovanni/storage/2023-11-03/file/shares" ) @@ -51,6 +52,11 @@ func (w DataPlaneStorageShareWrapper) Exists(ctx context.Context, shareName stri func (w DataPlaneStorageShareWrapper) Get(ctx context.Context, shareName string) (*StorageShareProperties, error) { props, err := w.client.GetProperties(ctx, shareName) if err != nil { + if props.HttpResponse == nil { + return nil, pollers.PollingDroppedConnectionError{ + Message: err.Error(), + } + } if response.WasNotFound(props.HttpResponse) { return nil, nil } diff --git a/internal/services/storage/storage_account_data_plane_helpers.go b/internal/services/storage/storage_account_data_plane_helpers.go index 5230c8574878..db974e3a51df 100644 --- a/internal/services/storage/storage_account_data_plane_helpers.go +++ b/internal/services/storage/storage_account_data_plane_helpers.go @@ -5,8 +5,10 @@ package storage import ( "context" + "errors" "fmt" "log" + "regexp" "slices" "time" @@ -64,8 +66,10 @@ func waitForDataPlaneToBecomeAvailableForAccount(ctx context.Context, client *cl return fmt.Errorf("building Blob Service Poller: %+v", err) } poller := pollers.NewPoller(pollerType, initialDelayDuration, pollers.DefaultNumberOfDroppedConnectionsToAllow) - if err := poller.PollUntilDone(ctx); err != nil { - return fmt.Errorf("waiting for the Blob Service to become available: %+v", err) + if err = poller.PollUntilDone(ctx); err != nil { + if !connectionError(err) { + return fmt.Errorf("waiting for the Blob Service to become available: %+v", err) + } } } @@ -76,8 +80,10 @@ func waitForDataPlaneToBecomeAvailableForAccount(ctx context.Context, client *cl return fmt.Errorf("building Queues Poller: %+v", err) } poller := pollers.NewPoller(pollerType, initialDelayDuration, pollers.DefaultNumberOfDroppedConnectionsToAllow) - if err := poller.PollUntilDone(ctx); err != nil { - return fmt.Errorf("waiting for the Queues Service to become available: %+v", err) + if err = poller.PollUntilDone(ctx); err != nil { + if !connectionError(err) { + return fmt.Errorf("waiting for the Queues Service to become available: %+v", err) + } } } @@ -88,8 +94,10 @@ func waitForDataPlaneToBecomeAvailableForAccount(ctx context.Context, client *cl return fmt.Errorf("building File Share Poller: %+v", err) } poller := pollers.NewPoller(pollerType, initialDelayDuration, pollers.DefaultNumberOfDroppedConnectionsToAllow) - if err := poller.PollUntilDone(ctx); err != nil { - return fmt.Errorf("waiting for the File Service to become available: %+v", err) + if err = poller.PollUntilDone(ctx); err != nil { + if !connectionError(err) { + return fmt.Errorf("waiting for the File Service to become available: %+v", err) + } } } @@ -100,10 +108,21 @@ func waitForDataPlaneToBecomeAvailableForAccount(ctx context.Context, client *cl return fmt.Errorf("building Static Website Poller: %+v", err) } poller := pollers.NewPoller(pollerType, initialDelayDuration, pollers.DefaultNumberOfDroppedConnectionsToAllow) - if err := poller.PollUntilDone(ctx); err != nil { - return fmt.Errorf("waiting for the Static Website to become available: %+v", err) + if err = poller.PollUntilDone(ctx); err != nil { + if !connectionError(err) { + return fmt.Errorf("waiting for the Static Website to become available: %+v", err) + } } } return nil } + +func connectionError(e error) bool { + var pollingDroppedConnectionError pollers.PollingDroppedConnectionError + if errors.As(e, &pollingDroppedConnectionError) { + return true + } + + return regexp.MustCompile(`dial tcp .*:`).MatchString(e.Error()) || regexp.MustCompile(`EOF$`).MatchString(e.Error()) +} diff --git a/internal/services/storage/storage_account_queue_properties_data_plane_resource.go b/internal/services/storage/storage_account_queue_properties_data_plane_resource.go new file mode 100644 index 000000000000..7d19b6189e39 --- /dev/null +++ b/internal/services/storage/storage_account_queue_properties_data_plane_resource.go @@ -0,0 +1,666 @@ +package storage + +import ( + "context" + "fmt" + "reflect" + "strings" + "time" + + "github.com/hashicorp/go-azure-helpers/lang/pointer" + "github.com/hashicorp/go-azure-helpers/resourcemanager/commonids" + "github.com/hashicorp/go-azure-sdk/resource-manager/storage/2023-01-01/storageaccounts" + "github.com/hashicorp/terraform-provider-azurerm/internal/sdk" + "github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk" + "github.com/hashicorp/terraform-provider-azurerm/internal/tf/validation" + "github.com/tombuildsstuff/giovanni/storage/2023-11-03/queue/queues" +) + +type AccountQueuePropertiesResource struct{} + +var _ sdk.ResourceWithUpdate = AccountQueuePropertiesResource{} + +type AccountQueuePropertiesModel struct { + StorageAccountId string `json:"storage_account_id" tfschema:"storage_account_id"` + CorsRule []AccountQueuePropertiesCorsRule `tfschema:"cors_rule"` + HourMetrics []AccountQueuePropertiesHourMetrics `tfschema:"hour_metrics"` + MinuteMetrics []AccountQueuePropertiesMinuteMetrics `tfschema:"minute_metrics"` + Logging []AccountQueuePropertiesLogging `tfschema:"logging"` +} + +type AccountQueuePropertiesCorsRule struct { + AllowedOrigins []string `tfschema:"allowed_origins"` + AllowedMethods []string `tfschema:"allowed_methods"` + AllowedHeaders []string `tfschema:"allowed_headers"` + ExposedHeaders []string `tfschema:"exposed_headers"` + MaxAgeSeconds int64 `tfschema:"max_age_in_seconds"` +} + +type AccountQueuePropertiesHourMetrics struct { + Version string `tfschema:"version"` + IncludeAPIS bool `tfschema:"include_apis"` + RetentionPolicyDays int64 `tfschema:"retention_policy_days"` +} + +type AccountQueuePropertiesMinuteMetrics struct { + Version string `tfschema:"version"` + IncludeAPIS bool `tfschema:"include_apis"` + RetentionPolicyDays int64 `tfschema:"retention_policy_days"` +} + +type AccountQueuePropertiesLogging struct { + Version string `tfschema:"version"` + Delete bool `tfschema:"delete"` + Read bool `tfschema:"read"` + Write bool `tfschema:"write"` + RetentionPolicyDays int64 `tfschema:"retention_policy_days"` +} + +var defaultCorsProperties = queues.Cors{ + CorsRule: []queues.CorsRule{}, +} + +var defaultHourMetricsProperties = queues.MetricsConfig{ + Version: "1.0", + Enabled: false, + RetentionPolicy: queues.RetentionPolicy{ + Enabled: false, + }, +} + +var defaultMinuteMetricsProperties = queues.MetricsConfig{ + Version: "1.0", + Enabled: false, + RetentionPolicy: queues.RetentionPolicy{ + Enabled: false, + }, +} + +var defaultLoggingProperties = queues.LoggingConfig{ + Version: "1.0", + Delete: false, + Read: false, + Write: false, + RetentionPolicy: queues.RetentionPolicy{ + Enabled: false, + }, +} + +func (s AccountQueuePropertiesResource) Arguments() map[string]*pluginsdk.Schema { + return map[string]*pluginsdk.Schema{ + "storage_account_id": { + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: commonids.ValidateStorageAccountID, + }, + + "cors_rule": { + Type: pluginsdk.TypeList, + Optional: true, + MaxItems: 5, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "allowed_origins": { + Type: pluginsdk.TypeList, + Required: true, + MaxItems: 64, + Elem: &pluginsdk.Schema{ + Type: pluginsdk.TypeString, + ValidateFunc: validation.StringIsNotEmpty, + }, + }, + "exposed_headers": { + Type: pluginsdk.TypeList, + Required: true, + MaxItems: 64, + MinItems: 1, + Elem: &pluginsdk.Schema{ + Type: pluginsdk.TypeString, + }, + }, + "allowed_headers": { + Type: pluginsdk.TypeList, + Required: true, + MaxItems: 64, + MinItems: 1, + Elem: &pluginsdk.Schema{ + Type: pluginsdk.TypeString, + }, + }, + "allowed_methods": { + Type: pluginsdk.TypeList, + Required: true, + MaxItems: 64, + Elem: &pluginsdk.Schema{ + Type: pluginsdk.TypeString, + ValidateFunc: validation.StringInSlice([]string{ + "DELETE", + "GET", + "HEAD", + "MERGE", + "POST", + "OPTIONS", + "PUT", + }, false), + }, + }, + "max_age_in_seconds": { + Type: pluginsdk.TypeInt, + Required: true, + ValidateFunc: validation.IntBetween(0, 2000000000), + }, + }, + }, + AtLeastOneOf: []string{"minute_metrics", "hour_metrics", "logging", "cors_rule"}, + }, + + "hour_metrics": { + Type: pluginsdk.TypeList, + Optional: true, + Computed: true, + MaxItems: 1, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "version": { + Type: pluginsdk.TypeString, + Required: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + "include_apis": { + Type: pluginsdk.TypeBool, + Optional: true, + Default: false, + }, + "retention_policy_days": { + Type: pluginsdk.TypeInt, + Optional: true, + ValidateFunc: validation.IntBetween(1, 365), + }, + }, + }, + AtLeastOneOf: []string{"minute_metrics", "hour_metrics", "logging", "cors_rule"}, + }, + "logging": { + Type: pluginsdk.TypeList, + Optional: true, + Computed: true, + MaxItems: 1, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "version": { + Type: pluginsdk.TypeString, + Required: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + "delete": { + Type: pluginsdk.TypeBool, + Required: true, + }, + "read": { + Type: pluginsdk.TypeBool, + Required: true, + }, + "write": { + Type: pluginsdk.TypeBool, + Required: true, + }, + "retention_policy_days": { + Type: pluginsdk.TypeInt, + Optional: true, + ValidateFunc: validation.IntBetween(1, 365), + }, + }, + }, + AtLeastOneOf: []string{"minute_metrics", "hour_metrics", "logging", "cors_rule"}, + }, + "minute_metrics": { + Type: pluginsdk.TypeList, + Optional: true, + Computed: true, + MaxItems: 1, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "version": { + Type: pluginsdk.TypeString, + Required: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + "include_apis": { + Type: pluginsdk.TypeBool, + Optional: true, + Default: false, + }, + "retention_policy_days": { + Type: pluginsdk.TypeInt, + Optional: true, + ValidateFunc: validation.IntBetween(1, 365), + }, + }, + }, + AtLeastOneOf: []string{"minute_metrics", "hour_metrics", "logging", "cors_rule"}, + }, + } +} + +func (s AccountQueuePropertiesResource) Attributes() map[string]*pluginsdk.Schema { + return map[string]*pluginsdk.Schema{} +} + +func (s AccountQueuePropertiesResource) ModelObject() interface{} { + return &AccountQueuePropertiesModel{} +} + +func (s AccountQueuePropertiesResource) ResourceType() string { + return "azurerm_storage_account_queue_properties" +} + +func (s AccountQueuePropertiesResource) IDValidationFunc() pluginsdk.SchemaValidateFunc { + return commonids.ValidateStorageAccountID +} + +func (s AccountQueuePropertiesResource) Create() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 30 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + storageClient := metadata.Client.Storage + var model AccountQueuePropertiesModel + if err := metadata.Decode(&model); err != nil { + return fmt.Errorf("decoding: %+v", err) + } + + accountID, err := commonids.ParseStorageAccountID(model.StorageAccountId) + if err != nil { + return err + } + + // Get the target account to ensure it supports queues + account, err := storageClient.ResourceManager.StorageAccounts.GetProperties(ctx, *accountID, storageaccounts.DefaultGetPropertiesOperationOptions()) + if err != nil { + return fmt.Errorf("retrieving %s: %+v", *accountID, err) + } + if account.Model == nil { + return fmt.Errorf("retrieving %s: `model` was nil", *accountID) + } + + if account.Model.Sku == nil || account.Model.Sku.Tier == nil || string(account.Model.Sku.Name) == "" { + return fmt.Errorf("could not read SKU details for %s", *accountID) + } + + accountTier := *account.Model.Sku.Tier + accountReplicationTypeParts := strings.Split(string(account.Model.Sku.Name), "_") + if len(accountReplicationTypeParts) != 2 { + return fmt.Errorf("could not read SKU replication type for %s", *accountID) + } + accountReplicationType := accountReplicationTypeParts[1] + + accountDetails, err := storageClient.FindAccount(ctx, accountID.SubscriptionId, accountID.StorageAccountName) + if err != nil { + return err + } + + supportLevel := availableFunctionalityForAccount(accountDetails.Kind, accountTier, accountReplicationType) + + if !supportLevel.supportQueue { + return fmt.Errorf("account %s does not support queues", *accountID) + } + + client, err := storageClient.QueuesDataPlaneClient(ctx, *accountDetails, storageClient.DataPlaneOperationSupportingAnyAuthMethod()) + if err != nil { + return fmt.Errorf("creating Queues Data Plane Client for %s: %+v", accountID, err) + } + + props := DefaultValueForAccountQueueProperties() + + if len(model.CorsRule) >= 1 { + corsRules := make([]queues.CorsRule, 0) + for _, corsRule := range model.CorsRule { + corsRules = append(corsRules, queues.CorsRule{ + AllowedOrigins: strings.Join(corsRule.AllowedOrigins, ","), + AllowedMethods: strings.Join(corsRule.AllowedMethods, ","), + AllowedHeaders: strings.Join(corsRule.AllowedHeaders, ","), + ExposedHeaders: strings.Join(corsRule.ExposedHeaders, ","), + MaxAgeInSeconds: int(corsRule.MaxAgeSeconds), + }) + } + + props.Cors.CorsRule = corsRules + } + + if len(model.HourMetrics) == 1 { + metrics := model.HourMetrics[0] + props.HourMetrics.Enabled = true + props.HourMetrics.Version = metrics.Version + if metrics.RetentionPolicyDays != 0 { + props.HourMetrics.RetentionPolicy = queues.RetentionPolicy{ + Days: int(metrics.RetentionPolicyDays), + Enabled: true, + } + } + + props.HourMetrics.IncludeAPIs = pointer.To(metrics.IncludeAPIS) + } + + if len(model.MinuteMetrics) != 0 { + metrics := model.MinuteMetrics[0] + props.MinuteMetrics.Enabled = true + props.MinuteMetrics.Version = metrics.Version + if metrics.RetentionPolicyDays != 0 { + props.MinuteMetrics.RetentionPolicy = queues.RetentionPolicy{ + Days: int(metrics.RetentionPolicyDays), + Enabled: true, + } + } + + props.MinuteMetrics.IncludeAPIs = pointer.To(metrics.IncludeAPIS) + } + + if len(model.Logging) != 0 { + logging := model.Logging[0] + props.Logging.Version = logging.Version + props.Logging.Delete = logging.Delete + props.Logging.Read = logging.Read + props.Logging.Write = logging.Write + if logging.RetentionPolicyDays != 0 { + props.Logging.RetentionPolicy = queues.RetentionPolicy{ + Enabled: true, + Days: int(logging.RetentionPolicyDays), + } + } + } + + if err = client.UpdateServiceProperties(ctx, props); err != nil { + return fmt.Errorf("updating Queue Properties for %s: %+v", accountID, err) + } + + metadata.SetID(accountID) + + return nil + }, + } +} + +func (s AccountQueuePropertiesResource) Read() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 5 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + storageClient := metadata.Client.Storage + + var state AccountQueuePropertiesModel + + id, err := commonids.ParseStorageAccountID(metadata.ResourceData.Id()) + if err != nil { + return err + } + + state.StorageAccountId = id.ID() + + account, err := storageClient.FindAccount(ctx, id.SubscriptionId, id.StorageAccountName) + if err != nil { + return metadata.MarkAsGone(id) + } + if account == nil { + return fmt.Errorf("unable to locate %s", *id) + } + + client, err := storageClient.QueuesDataPlaneClient(ctx, *account, storageClient.DataPlaneOperationSupportingAnyAuthMethod()) + if err != nil { + return fmt.Errorf("building Queues Client for %s: %v", *id, err) + } + + props, err := client.GetServiceProperties(ctx) + if err != nil { + return fmt.Errorf("retrieving Queue Properties for %s: %+v", *id, err) + } + + if props != nil { + if props.Cors != nil && !reflect.DeepEqual(*props.Cors, &defaultCorsProperties) { + corsRules := make([]AccountQueuePropertiesCorsRule, 0) + for _, rule := range props.Cors.CorsRule { + corsRule := AccountQueuePropertiesCorsRule{ + AllowedOrigins: strings.Split(rule.AllowedOrigins, ","), + AllowedMethods: strings.Split(rule.AllowedMethods, ","), + AllowedHeaders: strings.Split(rule.AllowedHeaders, ","), + ExposedHeaders: strings.Split(rule.ExposedHeaders, ","), + MaxAgeSeconds: int64(rule.MaxAgeInSeconds), + } + corsRules = append(corsRules, corsRule) + } + state.CorsRule = corsRules + } + + if props.HourMetrics != nil && !reflect.DeepEqual(*props.HourMetrics, &defaultHourMetricsProperties) { + state.HourMetrics = []AccountQueuePropertiesHourMetrics{ + { + Version: props.HourMetrics.Version, + IncludeAPIS: pointer.From(props.HourMetrics.IncludeAPIs), + RetentionPolicyDays: int64(props.HourMetrics.RetentionPolicy.Days), + }, + } + } + + if props.MinuteMetrics != nil && !reflect.DeepEqual(*props.MinuteMetrics, &defaultMinuteMetricsProperties) { + state.MinuteMetrics = []AccountQueuePropertiesMinuteMetrics{ + { + Version: props.MinuteMetrics.Version, + IncludeAPIS: pointer.From(props.MinuteMetrics.IncludeAPIs), + RetentionPolicyDays: int64(props.MinuteMetrics.RetentionPolicy.Days), + }, + } + } + + if props.Logging != nil && !reflect.DeepEqual(*props.Logging, &defaultLoggingProperties) { + state.Logging = []AccountQueuePropertiesLogging{ + { + Version: props.Logging.Version, + Delete: props.Logging.Delete, + Read: props.Logging.Read, + Write: props.Logging.Write, + RetentionPolicyDays: int64(props.Logging.RetentionPolicy.Days), + }, + } + } + } + + return metadata.Encode(&state) + }, + } +} + +func (s AccountQueuePropertiesResource) Delete() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 30 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + storageClient := metadata.Client.Storage + + id, err := commonids.ParseStorageAccountID(metadata.ResourceData.Id()) + if err != nil { + return err + } + + account, err := storageClient.FindAccount(ctx, id.SubscriptionId, id.StorageAccountName) + if err != nil { + return fmt.Errorf("retrieving %s: %+v", *id, err) + } + if account == nil { + return fmt.Errorf("unable to locate %s", *id) + } + + client, err := storageClient.QueuesDataPlaneClient(ctx, *account, storageClient.DataPlaneOperationSupportingAnyAuthMethod()) + if err != nil { + return fmt.Errorf("building Queues Client for %s: %v", *id, err) + } + + if err = client.UpdateServiceProperties(ctx, DefaultValueForAccountQueueProperties()); err != nil { + return fmt.Errorf("updating Queue Properties for %s: %+v", *id, err) + } + + return nil + }, + } +} + +func (s AccountQueuePropertiesResource) Update() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 30 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + storageClient := metadata.Client.Storage + + id, err := commonids.ParseStorageAccountID(metadata.ResourceData.Id()) + if err != nil { + return err + } + + account, err := storageClient.FindAccount(ctx, id.SubscriptionId, id.StorageAccountName) + if err != nil { + return fmt.Errorf("retrieving %s: %+v", *id, err) + } + + if account == nil { + return fmt.Errorf("unable to locate %s", *id) + } + + client, err := storageClient.QueuesDataPlaneClient(ctx, *account, storageClient.DataPlaneOperationSupportingAnyAuthMethod()) + if err != nil { + return fmt.Errorf("building Queues Client for %s: %v", *id, err) + } + + props, err := client.GetServiceProperties(ctx) + if err != nil { + return fmt.Errorf("retrieving %s: %+v", *id, err) + } + + var model AccountQueuePropertiesModel + + if err = metadata.Decode(&model); err != nil { + return fmt.Errorf("decoding: %+v", err) + } + + if metadata.ResourceData.HasChange("cors_rule") { + if len(model.CorsRule) >= 1 { + corsRules := make([]queues.CorsRule, 0) + for _, corsRule := range model.CorsRule { + corsRules = append(corsRules, queues.CorsRule{ + AllowedOrigins: strings.Join(corsRule.AllowedOrigins, ","), + AllowedMethods: strings.Join(corsRule.AllowedMethods, ","), + AllowedHeaders: strings.Join(corsRule.AllowedHeaders, ","), + ExposedHeaders: strings.Join(corsRule.ExposedHeaders, ","), + MaxAgeInSeconds: int(corsRule.MaxAgeSeconds), + }) + } + + props.Cors.CorsRule = corsRules + } else { + props.Cors = pointer.To(defaultCorsProperties) + } + } + + if metadata.ResourceData.HasChange("hour_metrics") { + if len(model.HourMetrics) == 1 { + metrics := model.HourMetrics[0] + if metadata.ResourceData.HasChange("hour_metrics.0.version") { + props.HourMetrics.Version = metrics.Version + } + + if metadata.ResourceData.HasChange("hour_metrics.0.include_apis") { + props.HourMetrics.IncludeAPIs = pointer.To(metrics.IncludeAPIS) + } + + if metadata.ResourceData.HasChange("hour_metrics.0.retention_policy_days") { + props.HourMetrics.RetentionPolicy = queues.RetentionPolicy{ + Days: int(metrics.RetentionPolicyDays), + Enabled: true, + } + } + } else { + props.HourMetrics = pointer.To(defaultHourMetricsProperties) + } + } + + if metadata.ResourceData.HasChange("minute_metrics") { + if len(model.MinuteMetrics) == 1 { + metrics := model.MinuteMetrics[0] + if metadata.ResourceData.HasChange("minute_metrics.0.version") { + props.MinuteMetrics.Version = metrics.Version + } + + if metadata.ResourceData.HasChange("minute_metrics.0.include_apis") { + props.MinuteMetrics.IncludeAPIs = pointer.To(metrics.IncludeAPIS) + } + + if metadata.ResourceData.HasChange("minute_metrics.0.retention_policy_days") { + props.MinuteMetrics.RetentionPolicy = queues.RetentionPolicy{ + Days: int(metrics.RetentionPolicyDays), + Enabled: true, + } + } + } else { + props.MinuteMetrics = pointer.To(defaultMinuteMetricsProperties) + } + } + + if metadata.ResourceData.HasChange("logging") { + if len(model.Logging) == 1 { + logging := model.Logging[0] + if metadata.ResourceData.HasChange("logging.0.version") { + props.Logging.Version = logging.Version + } + if metadata.ResourceData.HasChange("logging.0.delete") { + props.Logging.Delete = logging.Delete + } + if metadata.ResourceData.HasChange("logging.0.read") { + props.Logging.Read = logging.Read + } + if metadata.ResourceData.HasChange("logging.0.write") { + props.Logging.Write = logging.Write + } + if metadata.ResourceData.HasChange("logging.0.retention_policy_days") { + props.Logging.RetentionPolicy = queues.RetentionPolicy{ + Days: int(logging.RetentionPolicyDays), + Enabled: true, + } + } + } else { + props.Logging = pointer.To(defaultLoggingProperties) + } + } + + if err = client.UpdateServiceProperties(ctx, *props); err != nil { + return fmt.Errorf("updating Queue Properties for %s: %+v", *id, err) + } + + return nil + }, + } +} + +func DefaultValueForAccountQueueProperties() queues.StorageServiceProperties { + return queues.StorageServiceProperties{ + Logging: &queues.LoggingConfig{ + Version: "1.0", + Delete: false, + Read: false, + Write: false, + RetentionPolicy: queues.RetentionPolicy{ + Enabled: false, + }, + }, + HourMetrics: &queues.MetricsConfig{ + Version: "1.0", + Enabled: false, + RetentionPolicy: queues.RetentionPolicy{ + Enabled: false, + }, + }, + MinuteMetrics: &queues.MetricsConfig{ + Version: "1.0", + Enabled: false, + RetentionPolicy: queues.RetentionPolicy{ + Enabled: false, + }, + }, + Cors: &queues.Cors{ + CorsRule: []queues.CorsRule{}, + }, + } +} diff --git a/internal/services/storage/storage_account_queue_properties_data_plane_resource_test.go b/internal/services/storage/storage_account_queue_properties_data_plane_resource_test.go new file mode 100644 index 000000000000..125818f615ad --- /dev/null +++ b/internal/services/storage/storage_account_queue_properties_data_plane_resource_test.go @@ -0,0 +1,322 @@ +package storage_test + +import ( + "context" + "fmt" + "reflect" + "testing" + + "github.com/hashicorp/go-azure-helpers/lang/pointer" + "github.com/hashicorp/go-azure-helpers/resourcemanager/commonids" + "github.com/hashicorp/terraform-provider-azurerm/internal/acceptance" + "github.com/hashicorp/terraform-provider-azurerm/internal/acceptance/check" + "github.com/hashicorp/terraform-provider-azurerm/internal/clients" + "github.com/hashicorp/terraform-provider-azurerm/internal/services/storage" + "github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk" +) + +type AccountQueuePropertiesResource struct{} + +func TestAccStorageAccountQueueProperties_corsOnly(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_storage_account_queue_properties", "test") + r := AccountQueuePropertiesResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.corsOnly(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +func TestAccStorageAccountQueueProperties_loggingOnly(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_storage_account_queue_properties", "test") + r := AccountQueuePropertiesResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.loggingOnly(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +func TestAccStorageAccountQueueProperties_hourMetricsOnly(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_storage_account_queue_properties", "test") + r := AccountQueuePropertiesResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.hourMetricsOnly(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +func TestAccStorageAccountQueueProperties_minuteMetricsOnly(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_storage_account_queue_properties", "test") + r := AccountQueuePropertiesResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.minuteMetricsOnly(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +func TestAccStorageAccountQueueProperties_update(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_storage_account_queue_properties", "test") + r := AccountQueuePropertiesResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.complete(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + { + Config: r.corsOnly(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + { + Config: r.loggingOnly(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + { + Config: r.hourMetricsOnly(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + { + Config: r.minuteMetricsOnly(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + { + Config: r.corsOnly(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + { + Config: r.complete(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +func TestAccStorageAccountQueueProperties_complete(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_storage_account_queue_properties", "test") + r := AccountQueuePropertiesResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.complete(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +func (r AccountQueuePropertiesResource) Exists(ctx context.Context, client *clients.Client, state *pluginsdk.InstanceState) (*bool, error) { + id, err := commonids.ParseStorageAccountID(state.ID) + if err != nil { + return nil, err + } + + account, err := client.Storage.FindAccount(ctx, id.SubscriptionId, id.StorageAccountName) + if err != nil { + return nil, fmt.Errorf("retrieving %s: %+v", *id, err) + } + if account == nil { + return nil, fmt.Errorf("unable to locate %s", *id) + } + + queuesClient, err := client.Storage.QueuesDataPlaneClient(ctx, *account, client.Storage.DataPlaneOperationSupportingAnyAuthMethod()) + if err != nil { + return nil, fmt.Errorf("building Queues Client for %s: %v", *id, err) + } + + props, err := queuesClient.GetServiceProperties(ctx) + if err != nil { + return nil, fmt.Errorf("retrieving Queue Properties for %s: %+v", *id, err) + } + + present := !reflect.DeepEqual(storage.DefaultValueForAccountQueueProperties(), props) + return pointer.To(present), nil +} + +func (r AccountQueuePropertiesResource) corsOnly(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +%s + +resource "azurerm_storage_account_queue_properties" "test" { + storage_account_id = azurerm_storage_account.test.id + cors_rule { + allowed_origins = ["http://www.example.com"] + exposed_headers = ["x-tempo-*"] + allowed_headers = ["x-tempo-*"] + allowed_methods = ["GET", "PUT"] + max_age_in_seconds = "500" + } + + cors_rule { + allowed_origins = ["http://www.contoso.com"] + exposed_headers = ["x-example-*"] + allowed_headers = ["x-example-*"] + allowed_methods = ["GET"] + max_age_in_seconds = "60" + } +} +`, r.template(data)) +} + +func (r AccountQueuePropertiesResource) hourMetricsOnly(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +%s + +resource "azurerm_storage_account_queue_properties" "test" { + storage_account_id = azurerm_storage_account.test.id + hour_metrics { + version = "1.0" + retention_policy_days = 7 + } +} +`, r.template(data)) +} + +func (r AccountQueuePropertiesResource) minuteMetricsOnly(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +%s + +resource "azurerm_storage_account_queue_properties" "test" { + storage_account_id = azurerm_storage_account.test.id + minute_metrics { + version = "1.0" + retention_policy_days = 7 + } +} +`, r.template(data)) +} + +func (r AccountQueuePropertiesResource) loggingOnly(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +%s + +resource "azurerm_storage_account_queue_properties" "test" { + storage_account_id = azurerm_storage_account.test.id + logging { + version = "1.0" + delete = true + read = true + write = true + retention_policy_days = 7 + } +} +`, r.template(data)) +} + +func (r AccountQueuePropertiesResource) complete(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +%s + +resource "azurerm_storage_account_queue_properties" "test" { + storage_account_id = azurerm_storage_account.test.id + cors_rule { + allowed_origins = ["http://www.example.com"] + exposed_headers = ["x-tempo-*"] + allowed_headers = ["x-tempo-*"] + allowed_methods = ["GET", "PUT"] + max_age_in_seconds = "500" + } + + logging { + version = "1.0" + delete = true + read = true + write = true + retention_policy_days = 7 + } + + hour_metrics { + version = "1.0" + retention_policy_days = 7 + } + + minute_metrics { + version = "1.0" + retention_policy_days = 7 + } +} +`, r.template(data)) +} + +func (r AccountQueuePropertiesResource) template(data acceptance.TestData) string { + return fmt.Sprintf(` + +resource "azurerm_resource_group" "test" { + name = "acctestRG-storage-%d" + location = "%s" +} + +resource "azurerm_storage_account" "test" { + name = "unlikely23exst2acct%s" + resource_group_name = azurerm_resource_group.test.name + + location = azurerm_resource_group.test.location + account_tier = "Standard" + account_replication_type = "LRS" +} +`, data.RandomInteger, data.Locations.Primary, data.RandomString) +} diff --git a/internal/services/storage/storage_account_resource.go b/internal/services/storage/storage_account_resource.go index 2343dc63c3ba..19f2816c8503 100644 --- a/internal/services/storage/storage_account_resource.go +++ b/internal/services/storage/storage_account_resource.go @@ -28,7 +28,7 @@ import ( "github.com/hashicorp/terraform-provider-azurerm/internal/clients" "github.com/hashicorp/terraform-provider-azurerm/internal/features" "github.com/hashicorp/terraform-provider-azurerm/internal/locks" - keyVaultClient "github.com/hashicorp/terraform-provider-azurerm/internal/services/keyvault/client" + keyVaultsClient "github.com/hashicorp/terraform-provider-azurerm/internal/services/keyvault/client" keyVaultParse "github.com/hashicorp/terraform-provider-azurerm/internal/services/keyvault/parse" keyVaultValidate "github.com/hashicorp/terraform-provider-azurerm/internal/services/keyvault/validate" managedHsmParse "github.com/hashicorp/terraform-provider-azurerm/internal/services/managedhsm/parse" @@ -753,6 +753,7 @@ func resourceStorageAccount() *pluginsdk.Resource { "static_website": { Type: pluginsdk.TypeList, Optional: true, + Computed: true, MaxItems: 1, Elem: &pluginsdk.Resource{ Schema: map[string]*pluginsdk.Schema{ @@ -1246,6 +1247,15 @@ func resourceStorageAccount() *pluginsdk.Resource { } } + if !v.(*clients.Client).Features.Storage.DataPlaneAvailable { + if _, ok := d.GetOk("queue_properties"); ok { + return fmt.Errorf("cannot configure 'queue_properties' when the Provider Feature 'data_plane_available' is set to 'false'") + } + if _, ok := d.GetOk("static_website"); ok { + return fmt.Errorf("cannot configure 'static_website' when the Provider Feature 'data_plane_available' is set to 'false'") + } + } + return nil }), pluginsdk.ForceNewIfChange("account_replication_type", func(ctx context.Context, old, new, meta interface{}) bool { @@ -1288,6 +1298,7 @@ func resourceStorageAccountCreate(d *pluginsdk.ResourceData, meta interface{}) e client := meta.(*clients.Client).Storage.ResourceManager.StorageAccounts storageClient := meta.(*clients.Client).Storage keyVaultClient := meta.(*clients.Client).KeyVault + dataPlaneAvailable := meta.(*clients.Client).Features.Storage.DataPlaneAvailable ctx, cancel := timeouts.ForCreate(meta.(*clients.Client).StopContext, d) defer cancel() @@ -1486,19 +1497,61 @@ func resourceStorageAccountCreate(d *pluginsdk.ResourceData, meta interface{}) e return fmt.Errorf("populating cache for %s: %+v", id, err) } - dataPlaneAccount, err := storageClient.FindAccount(ctx, id.SubscriptionId, id.StorageAccountName) - if err != nil { - return fmt.Errorf("retrieving %s: %+v", id, err) - } - if dataPlaneAccount == nil { - return fmt.Errorf("unable to locate %q", id) - } - supportLevel := availableFunctionalityForAccount(accountKind, accountTier, replicationType) - if err := waitForDataPlaneToBecomeAvailableForAccount(ctx, storageClient, dataPlaneAccount, supportLevel); err != nil { - return fmt.Errorf("waiting for the Data Plane for %s to become available: %+v", id, err) + // Start of Data Plane access + if dataPlaneAvailable { + dataPlaneAccount, err := storageClient.FindAccount(ctx, id.SubscriptionId, id.StorageAccountName) + if err != nil { + return fmt.Errorf("retrieving %s: %+v", id, err) + } + if dataPlaneAccount == nil { + return fmt.Errorf("unable to locate %q", id) + } + + if err := waitForDataPlaneToBecomeAvailableForAccount(ctx, storageClient, dataPlaneAccount, supportLevel); err != nil { + return fmt.Errorf("waiting for the Data Plane for %s to become available: %+v", id, err) + } + + // Data Plane block + if val, ok := d.GetOk("queue_properties"); ok { + if !supportLevel.supportQueue { + return fmt.Errorf("`queue_properties` aren't supported for account kind %q in sku tier %q", accountKind, accountTier) + } + + queueClient, err := storageClient.QueuesDataPlaneClient(ctx, *dataPlaneAccount, storageClient.DataPlaneOperationSupportingAnyAuthMethod()) + if err != nil { + return fmt.Errorf("building Queues Client: %s", err) + } + + queueProperties, err := expandAccountQueueProperties(val.([]interface{})) + if err != nil { + return fmt.Errorf("expanding `queue_properties`: %+v", err) + } + + if err = queueClient.UpdateServiceProperties(ctx, *queueProperties); err != nil { + return fmt.Errorf("updating Queue Properties: %+v", err) + } + } + + if val, ok := d.GetOk("static_website"); ok { + if !supportLevel.supportStaticWebsite { + return fmt.Errorf("`static_website` aren't supported for account kind %q in sku tier %q", accountKind, accountTier) + } + + accountsClient, err := storageClient.AccountsDataPlaneClient(ctx, *dataPlaneAccount, storageClient.DataPlaneOperationSupportingAnyAuthMethod()) + if err != nil { + return fmt.Errorf("building Accounts Data Plane Client: %s", err) + } + + staticWebsiteProps := expandAccountStaticWebsiteProperties(val.([]interface{})) + + if _, err = accountsClient.SetServiceProperties(ctx, id.StorageAccountName, staticWebsiteProps); err != nil { + return fmt.Errorf("updating `static_website`: %+v", err) + } + } } + // Resource Manager only after here if val, ok := d.GetOk("blob_properties"); ok { if !supportLevel.supportBlob { return fmt.Errorf("`blob_properties` aren't supported for account kind %q in sku tier %q", accountKind, accountTier) @@ -1552,26 +1605,6 @@ func resourceStorageAccountCreate(d *pluginsdk.ResourceData, meta interface{}) e } } - if val, ok := d.GetOk("queue_properties"); ok { - if !supportLevel.supportQueue { - return fmt.Errorf("`queue_properties` aren't supported for account kind %q in sku tier %q", accountKind, accountTier) - } - - queueClient, err := storageClient.QueuesDataPlaneClient(ctx, *dataPlaneAccount, storageClient.DataPlaneOperationSupportingAnyAuthMethod()) - if err != nil { - return fmt.Errorf("building Queues Client: %s", err) - } - - queueProperties, err := expandAccountQueueProperties(val.([]interface{})) - if err != nil { - return fmt.Errorf("expanding `queue_properties`: %+v", err) - } - - if err = queueClient.UpdateServiceProperties(ctx, *queueProperties); err != nil { - return fmt.Errorf("updating Queue Properties: %+v", err) - } - } - if val, ok := d.GetOk("share_properties"); ok { if !supportLevel.supportShare { return fmt.Errorf("`share_properties` aren't supported for account kind %q in sku tier %q", accountKind, accountTier) @@ -1597,23 +1630,6 @@ func resourceStorageAccountCreate(d *pluginsdk.ResourceData, meta interface{}) e } } - if val, ok := d.GetOk("static_website"); ok { - if !supportLevel.supportStaticWebsite { - return fmt.Errorf("`static_website` aren't supported for account kind %q in sku tier %q", accountKind, accountTier) - } - - accountsClient, err := storageClient.AccountsDataPlaneClient(ctx, *dataPlaneAccount, storageClient.DataPlaneOperationSupportingAnyAuthMethod()) - if err != nil { - return fmt.Errorf("building Accounts Data Plane Client: %s", err) - } - - staticWebsiteProps := expandAccountStaticWebsiteProperties(val.([]interface{})) - - if _, err = accountsClient.SetServiceProperties(ctx, id.StorageAccountName, staticWebsiteProps); err != nil { - return fmt.Errorf("updating `static_website`: %+v", err) - } - } - return resourceStorageAccountRead(d, meta) } @@ -1983,6 +1999,7 @@ func resourceStorageAccountUpdate(d *pluginsdk.ResourceData, meta interface{}) e func resourceStorageAccountRead(d *pluginsdk.ResourceData, meta interface{}) error { storageClient := meta.(*clients.Client).Storage client := storageClient.ResourceManager.StorageAccounts + dataPlaneAvailable := meta.(*clients.Client).Features.Storage.DataPlaneAvailable env := meta.(*clients.Client).Account.Environment ctx, cancel := timeouts.ForRead(meta.(*clients.Client).StopContext, d) defer cancel() @@ -2216,25 +2233,6 @@ func resourceStorageAccountRead(d *pluginsdk.ResourceData, meta interface{}) err return fmt.Errorf("setting `blob_properties` for %s: %+v", *id, err) } - queueProperties := make([]interface{}, 0) - if supportLevel.supportQueue { - queueClient, err := storageClient.QueuesDataPlaneClient(ctx, *account, storageClient.DataPlaneOperationSupportingAnyAuthMethod()) - if err != nil { - return fmt.Errorf("building Queues Client: %s", err) - } - - queueProps, err := queueClient.GetServiceProperties(ctx) - if err != nil { - return fmt.Errorf("retrieving queue properties for %s: %+v", *id, err) - } - - queueProperties = flattenAccountQueueProperties(queueProps) - } - - if err := d.Set("queue_properties", queueProperties); err != nil { - return fmt.Errorf("setting `queue_properties`: %+v", err) - } - shareProperties := make([]interface{}, 0) if supportLevel.supportShare { shareProps, err := storageClient.ResourceManager.FileService.GetServiceProperties(ctx, *id) @@ -2248,22 +2246,48 @@ func resourceStorageAccountRead(d *pluginsdk.ResourceData, meta interface{}) err return fmt.Errorf("setting `share_properties` for %s: %+v", *id, err) } - staticWebsiteProperties := make([]interface{}, 0) - if supportLevel.supportStaticWebsite { - accountsClient, err := storageClient.AccountsDataPlaneClient(ctx, *account, storageClient.DataPlaneOperationSupportingAnyAuthMethod()) - if err != nil { - return fmt.Errorf("building Accounts Data Plane Client: %s", err) - } + if dataPlaneAvailable { + queueProperties := make([]interface{}, 0) + if supportLevel.supportQueue { + queueClient, err := storageClient.QueuesDataPlaneClient(ctx, *account, storageClient.DataPlaneOperationSupportingAnyAuthMethod()) + if err != nil { + return fmt.Errorf("building Queues Client: %s", err) + } - staticWebsiteProps, err := accountsClient.GetServiceProperties(ctx, id.StorageAccountName) - if err != nil { - return fmt.Errorf("retrieving static website properties for %s: %+v", *id, err) + queueProps, err := queueClient.GetServiceProperties(ctx) + if err != nil { + // Queue properties is a data plane only service, so we tolerate connection errors here in case of firewalls + // and other connectivity issues that are not guaranteed. + if !connectionError(err) { + return fmt.Errorf("retrieving queue properties for %s: %+v", *id, err) + } + } + + queueProperties = flattenAccountQueueProperties(queueProps) + } + if err := d.Set("queue_properties", queueProperties); err != nil { + return fmt.Errorf("setting `queue_properties`: %+v", err) } - staticWebsiteProperties = flattenAccountStaticWebsiteProperties(staticWebsiteProps) - } - if err := d.Set("static_website", staticWebsiteProperties); err != nil { - return fmt.Errorf("setting `static_website`: %+v", err) + staticWebsiteProperties := make([]interface{}, 0) + if supportLevel.supportStaticWebsite { + accountsClient, err := storageClient.AccountsDataPlaneClient(ctx, *account, storageClient.DataPlaneOperationSupportingAnyAuthMethod()) + if err != nil { + return fmt.Errorf("building Accounts Data Plane Client: %s", err) + } + + staticWebsiteProps, err := accountsClient.GetServiceProperties(ctx, id.StorageAccountName) + if err != nil { + if !connectionError(err) { + return fmt.Errorf("retrieving static website properties for %s: %+v", *id, err) + } + } + + staticWebsiteProperties = flattenAccountStaticWebsiteProperties(staticWebsiteProps) + } + if err := d.Set("static_website", staticWebsiteProperties); err != nil { + return fmt.Errorf("setting `static_website`: %+v", err) + } } return nil @@ -2285,6 +2309,10 @@ func resourceStorageAccountDelete(d *pluginsdk.ResourceData, meta interface{}) e existing, err := client.GetProperties(ctx, *id, storageaccounts.DefaultGetPropertiesOperationOptions()) if err != nil { + if response.WasNotFound(existing.HttpResponse) { + return nil + } + return fmt.Errorf("retrieving %s: %+v", *id, err) } @@ -2349,7 +2377,7 @@ func flattenAccountCustomDomain(input *storageaccounts.CustomDomain) []interface return output } -func expandAccountCustomerManagedKey(ctx context.Context, keyVaultClient *keyVaultClient.Client, subscriptionId string, input []interface{}, accountTier storageaccounts.SkuTier, accountKind storageaccounts.Kind, expandedIdentity identity.LegacySystemAndUserAssignedMap, queueEncryptionKeyType, tableEncryptionKeyType storageaccounts.KeyType) (*storageaccounts.Encryption, error) { +func expandAccountCustomerManagedKey(ctx context.Context, keyVaultClient *keyVaultsClient.Client, subscriptionId string, input []interface{}, accountTier storageaccounts.SkuTier, accountKind storageaccounts.Kind, expandedIdentity identity.LegacySystemAndUserAssignedMap, queueEncryptionKeyType, tableEncryptionKeyType storageaccounts.KeyType) (*storageaccounts.Encryption, error) { if accountKind != storageaccounts.KindStorageVTwo { if queueEncryptionKeyType == storageaccounts.KeyTypeAccount { return nil, fmt.Errorf("`queue_encryption_key_type = %q` can only be used with account kind `%q`", string(storageaccounts.KeyTypeAccount), string(storageaccounts.KindStorageVTwo)) diff --git a/internal/services/storage/storage_account_resource_test.go b/internal/services/storage/storage_account_resource_test.go index 0fb433f9cd28..56ee7d439f86 100644 --- a/internal/services/storage/storage_account_resource_test.go +++ b/internal/services/storage/storage_account_resource_test.go @@ -1784,6 +1784,47 @@ func TestAccStorageAccount_StorageV1_sharePropertiesRAGRS(t *testing.T) { }) } +// Data Plane tests + +func TestAccStorageAccount_noDataPlane(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_storage_account", "test") + r := StorageAccountResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.noDataPlane(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +func TestAccStorageAccount_noDataPlaneQueueShouldError(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_storage_account", "test") + r := StorageAccountResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.noDataPlaneExpectQueueError(data), + ExpectError: regexp.MustCompile("cannot configure 'queue_properties' when the Provider Feature 'data_plane_available' is set to 'false'"), + }, + }) +} + +func TestAccStorageAccount_noDataPlaneWebsiteShouldError(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_storage_account", "test") + r := StorageAccountResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.noDataPlaneExpectWebsiteError(data), + ExpectError: regexp.MustCompile("cannot configure 'static_website' when the Provider Feature 'data_plane_available' is set to 'false'"), + }, + }) +} + func (r StorageAccountResource) Exists(ctx context.Context, client *clients.Client, state *pluginsdk.InstanceState) (*bool, error) { id, err := commonids.ParseStorageAccountID(state.ID) if err != nil { @@ -5222,3 +5263,110 @@ resource "azurerm_key_vault_managed_hardware_security_module_key" "test" { } `, data.RandomString, data.Locations.Primary, data.RandomInteger) } + +// Data Plane configs + +func (r StorageAccountResource) noDataPlane(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features { + storage { + data_plane_available = false + } + } +} + +resource "azurerm_resource_group" "test" { + name = "acctestRG-storage-%d" + location = "%s" +} + +resource "azurerm_storage_account" "test" { + name = "unlikely23exst2acct%s" + resource_group_name = azurerm_resource_group.test.name + + location = azurerm_resource_group.test.location + account_tier = "Standard" + account_replication_type = "LRS" + + tags = { + environment = "production" + } +} +`, data.RandomInteger, data.Locations.Primary, data.RandomString) +} + +func (r StorageAccountResource) noDataPlaneExpectQueueError(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features { + storage { + data_plane_available = false + } + } +} + +resource "azurerm_resource_group" "test" { + name = "acctestRG-storage-%d" + location = "%s" +} + +resource "azurerm_storage_account" "test" { + name = "unlikely23exst2acct%s" + resource_group_name = azurerm_resource_group.test.name + + location = azurerm_resource_group.test.location + account_tier = "Standard" + account_replication_type = "LRS" + + queue_properties { + logging { + version = "1.0" + delete = true + read = true + write = true + retention_policy_days = 7 + } + } + + tags = { + environment = "production" + } +} +`, data.RandomInteger, data.Locations.Primary, data.RandomString) +} + +func (r StorageAccountResource) noDataPlaneExpectWebsiteError(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features { + storage { + data_plane_available = false + } + } +} + +resource "azurerm_resource_group" "test" { + name = "acctestRG-storage-%d" + location = "%s" +} + +resource "azurerm_storage_account" "test" { + name = "unlikely23exst2acct%s" + resource_group_name = azurerm_resource_group.test.name + + location = azurerm_resource_group.test.location + account_tier = "Standard" + account_replication_type = "LRS" + + static_website { + index_document = "index.html" + error_404_document = "404.html" + } + + tags = { + environment = "production" + } +} +`, data.RandomInteger, data.Locations.Primary, data.RandomString) +} diff --git a/internal/services/storage/storage_account_static_website_data_plane_resource.go b/internal/services/storage/storage_account_static_website_data_plane_resource.go new file mode 100644 index 000000000000..f46b7e1fabdc --- /dev/null +++ b/internal/services/storage/storage_account_static_website_data_plane_resource.go @@ -0,0 +1,268 @@ +package storage + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/hashicorp/go-azure-helpers/resourcemanager/commonids" + "github.com/hashicorp/go-azure-sdk/resource-manager/storage/2023-01-01/storageaccounts" + "github.com/hashicorp/terraform-provider-azurerm/internal/sdk" + "github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk" + "github.com/hashicorp/terraform-provider-azurerm/internal/tf/validation" + "github.com/tombuildsstuff/giovanni/storage/2023-11-03/blob/accounts" +) + +type AccountStaticWebsiteResource struct{} + +var _ sdk.ResourceWithUpdate = AccountStaticWebsiteResource{} + +type AccountStaticWebsiteResourceModel struct { + StorageAccountId string `tfschema:"storage_account_id"` + Error404Document string `tfschema:"error_404_document"` + IndexDocument string `tfschema:"index_document"` +} + +func (a AccountStaticWebsiteResource) Arguments() map[string]*pluginsdk.Schema { + return map[string]*pluginsdk.Schema{ + "storage_account_id": { + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: commonids.ValidateStorageAccountID, + }, + + "error_404_document": { + Type: pluginsdk.TypeString, + Optional: true, + ValidateFunc: validation.StringIsNotEmpty, + AtLeastOneOf: []string{"error_404_document", "index_document"}, + }, + + "index_document": { + Type: pluginsdk.TypeString, + Optional: true, + ValidateFunc: validation.StringIsNotEmpty, + AtLeastOneOf: []string{"error_404_document", "index_document"}, + }, + } +} + +func (a AccountStaticWebsiteResource) Attributes() map[string]*pluginsdk.Schema { + return map[string]*pluginsdk.Schema{} +} + +func (a AccountStaticWebsiteResource) ModelObject() interface{} { + return &AccountStaticWebsiteResourceModel{} +} + +func (a AccountStaticWebsiteResource) ResourceType() string { + return "azurerm_storage_account_static_website" +} + +func (a AccountStaticWebsiteResource) IDValidationFunc() pluginsdk.SchemaValidateFunc { + return commonids.ValidateStorageAccountID +} + +func (a AccountStaticWebsiteResource) Create() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 30 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + storageClient := metadata.Client.Storage + var model AccountStaticWebsiteResourceModel + if err := metadata.Decode(&model); err != nil { + return fmt.Errorf("decoding: %+v", err) + } + + accountID, err := commonids.ParseStorageAccountID(model.StorageAccountId) + if err != nil { + return err + } + + // Get the target account to ensure it supports queues + account, err := storageClient.ResourceManager.StorageAccounts.GetProperties(ctx, *accountID, storageaccounts.DefaultGetPropertiesOperationOptions()) + if err != nil { + return fmt.Errorf("retrieving %s: %+v", *accountID, err) + } + if account.Model == nil { + return fmt.Errorf("retrieving %s: `model` was nil", *accountID) + } + + if account.Model.Sku == nil || account.Model.Sku.Tier == nil || string(account.Model.Sku.Name) == "" { + return fmt.Errorf("could not read SKU details for %s", *accountID) + } + + accountTier := *account.Model.Sku.Tier + accountReplicationTypeParts := strings.Split(string(account.Model.Sku.Name), "_") + if len(accountReplicationTypeParts) != 2 { + return fmt.Errorf("could not read SKU replication type for %s", *accountID) + } + accountReplicationType := accountReplicationTypeParts[1] + + accountDetails, err := storageClient.FindAccount(ctx, accountID.SubscriptionId, accountID.StorageAccountName) + if err != nil { + return err + } + + supportLevel := availableFunctionalityForAccount(accountDetails.Kind, accountTier, accountReplicationType) + + if !supportLevel.supportStaticWebsite { + return fmt.Errorf("account %s does not support Static Websites", *accountID) + } + + client, err := storageClient.AccountsDataPlaneClient(ctx, *accountDetails, storageClient.DataPlaneOperationSupportingAnyAuthMethod()) + if err != nil { + return fmt.Errorf("building Accounts Data Plane Client: %s", err) + } + + properties := accounts.StorageServiceProperties{ + StaticWebsite: &accounts.StaticWebsite{ + Enabled: true, + }, + } + if model.IndexDocument != "" { + properties.StaticWebsite.IndexDocument = model.IndexDocument + } + if model.Error404Document != "" { + properties.StaticWebsite.ErrorDocument404Path = model.Error404Document + } + + if _, err = client.SetServiceProperties(ctx, accountID.StorageAccountName, properties); err != nil { + return fmt.Errorf("creating static website for %s: %+v", accountID, err) + } + + metadata.SetID(accountID) + + return nil + }, + } +} + +func (a AccountStaticWebsiteResource) Read() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 5 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + storageClient := metadata.Client.Storage + + var state AccountStaticWebsiteResourceModel + + id, err := commonids.ParseStorageAccountID(metadata.ResourceData.Id()) + if err != nil { + return err + } + + state.StorageAccountId = id.ID() + + accountDetails, err := storageClient.FindAccount(ctx, id.SubscriptionId, id.StorageAccountName) + if err != nil { + return metadata.MarkAsGone(id) + } + + accountsClient, err := storageClient.AccountsDataPlaneClient(ctx, *accountDetails, storageClient.DataPlaneOperationSupportingAnyAuthMethod()) + if err != nil { + return fmt.Errorf("building Accounts Data Plane Client for %s: %+v", *id, err) + } + + props, err := accountsClient.GetServiceProperties(ctx, id.StorageAccountName) + if err != nil { + return fmt.Errorf("retrieving static website properties for %s: %+v", *id, err) + } + + if website := props.StaticWebsite; website != nil { + state.IndexDocument = website.IndexDocument + state.Error404Document = website.ErrorDocument404Path + } + + return metadata.Encode(&state) + }, + } +} + +func (a AccountStaticWebsiteResource) Delete() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 30 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + storageClient := metadata.Client.Storage + + id, err := commonids.ParseStorageAccountID(metadata.ResourceData.Id()) + if err != nil { + return err + } + + accountDetails, err := storageClient.FindAccount(ctx, id.SubscriptionId, id.StorageAccountName) + if err != nil { + // If we don't find the account we can safely assume we don't need to remove the website since it must already be deleted + return nil + } + + properties := accounts.StorageServiceProperties{ + StaticWebsite: &accounts.StaticWebsite{ + Enabled: false, + }, + } + + client, err := storageClient.AccountsDataPlaneClient(ctx, *accountDetails, storageClient.DataPlaneOperationSupportingAnyAuthMethod()) + if err != nil { + return fmt.Errorf("building Accounts Data Plane Client: %s", err) + } + + if _, err = client.SetServiceProperties(ctx, id.StorageAccountName, properties); err != nil { + return fmt.Errorf("deleting static website for %s: %+v", id, err) + } + + return nil + }, + } +} + +func (a AccountStaticWebsiteResource) Update() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 30 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + storageClient := metadata.Client.Storage + var model AccountStaticWebsiteResourceModel + if err := metadata.Decode(&model); err != nil { + return fmt.Errorf("decoding: %+v", err) + } + + id, err := commonids.ParseStorageAccountID(metadata.ResourceData.Id()) + if err != nil { + return err + } + + accountDetails, err := storageClient.FindAccount(ctx, id.SubscriptionId, id.StorageAccountName) + if err != nil { + return err + } + + client, err := storageClient.AccountsDataPlaneClient(ctx, *accountDetails, storageClient.DataPlaneOperationSupportingAnyAuthMethod()) + if err != nil { + return fmt.Errorf("building Accounts Data Plane Client: %s", err) + } + + props, err := client.GetServiceProperties(ctx, id.StorageAccountName) + if err != nil || props.StaticWebsite == nil { + return fmt.Errorf("retrieving static website properties for %s: %+v", *id, err) + } + + properties := accounts.StorageServiceProperties{ + StaticWebsite: props.StaticWebsite, + } + + if metadata.ResourceData.HasChange("index_document") { + properties.StaticWebsite.IndexDocument = model.IndexDocument + } + + if metadata.ResourceData.HasChange("error_404_document") { + properties.StaticWebsite.ErrorDocument404Path = model.Error404Document + } + + if _, err = client.SetServiceProperties(ctx, id.StorageAccountName, properties); err != nil { + return fmt.Errorf("updating static website for %s: %+v", *id, err) + } + + return nil + }, + } +} diff --git a/internal/services/storage/storage_account_static_website_data_plane_resource_test.go b/internal/services/storage/storage_account_static_website_data_plane_resource_test.go new file mode 100644 index 000000000000..1af89c0f784d --- /dev/null +++ b/internal/services/storage/storage_account_static_website_data_plane_resource_test.go @@ -0,0 +1,180 @@ +package storage_test + +import ( + "context" + "fmt" + "testing" + + "github.com/hashicorp/go-azure-helpers/lang/pointer" + "github.com/hashicorp/go-azure-helpers/resourcemanager/commonids" + "github.com/hashicorp/terraform-provider-azurerm/internal/acceptance" + "github.com/hashicorp/terraform-provider-azurerm/internal/acceptance/check" + "github.com/hashicorp/terraform-provider-azurerm/internal/clients" + "github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk" +) + +type AccountStaticWebsiteResource struct{} + +func TestAccountStaticWebsiteResource_complete(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_storage_account_static_website", "test") + r := AccountStaticWebsiteResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.complete(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} +func TestAccountStaticWebsiteResource_update(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_storage_account_static_website", "test") + r := AccountStaticWebsiteResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.complete(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + { + Config: r.withIndex(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + { + Config: r.with404(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + { + Config: r.complete(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +func TestAccountStaticWebsiteResource_with404(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_storage_account_static_website", "test") + r := AccountStaticWebsiteResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.with404(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +func TestAccountStaticWebsiteResource_withIndex(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_storage_account_static_website", "test") + r := AccountStaticWebsiteResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.withIndex(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +func (r AccountStaticWebsiteResource) Exists(ctx context.Context, client *clients.Client, state *pluginsdk.InstanceState) (*bool, error) { + id, err := commonids.ParseStorageAccountID(state.ID) + if err != nil { + return nil, err + } + + accountDetails, err := client.Storage.FindAccount(ctx, id.SubscriptionId, id.StorageAccountName) + if err != nil { + return nil, fmt.Errorf("retrieving %s: %+v", *id, err) + } + if accountDetails == nil { + return nil, fmt.Errorf("unable to locate %s", *id) + } + + accountsClient, err := client.Storage.AccountsDataPlaneClient(ctx, *accountDetails, client.Storage.DataPlaneOperationSupportingAnyAuthMethod()) + if err != nil { + return nil, fmt.Errorf("building Accounts Data Plane Client for %s: %+v", *id, err) + } + + props, err := accountsClient.GetServiceProperties(ctx, id.StorageAccountName) + if err != nil { + return nil, fmt.Errorf("retrieving static website properties for %s: %+v", *id, err) + } + + if props.StaticWebsite == nil { + return nil, nil + } + + return pointer.To(props.StaticWebsite.Enabled), nil +} + +func (r AccountStaticWebsiteResource) complete(data acceptance.TestData) string { + return fmt.Sprintf(` +%s + +resource "azurerm_storage_account_static_website" "test" { + storage_account_id = azurerm_storage_account.test.id + error_404_document = "sadpanda_2.html" + index_document = "index_2.html" +} +`, r.template(data)) +} + +func (r AccountStaticWebsiteResource) with404(data acceptance.TestData) string { + return fmt.Sprintf(` +%s + +resource "azurerm_storage_account_static_website" "test" { + storage_account_id = azurerm_storage_account.test.id + error_404_document = "sadpanda.html" +} +`, r.template(data)) +} + +func (r AccountStaticWebsiteResource) withIndex(data acceptance.TestData) string { + return fmt.Sprintf(` +%s + +resource "azurerm_storage_account_static_website" "test" { + storage_account_id = azurerm_storage_account.test.id + index_document = "index.html" +} +`, r.template(data)) +} + +func (r AccountStaticWebsiteResource) template(data acceptance.TestData) string { + return fmt.Sprintf(` + +resource "azurerm_resource_group" "test" { + name = "acctestRG-storage-%d" + location = "%s" +} + +resource "azurerm_storage_account" "test" { + name = "unlikely23exst2acct%s" + resource_group_name = azurerm_resource_group.test.name + + location = azurerm_resource_group.test.location + account_tier = "Standard" + account_replication_type = "LRS" +} +`, data.RandomInteger, data.Locations.Primary, data.RandomString) +} diff --git a/vendor/github.com/hashicorp/go-azure-sdk/sdk/client/client.go b/vendor/github.com/hashicorp/go-azure-sdk/sdk/client/client.go index 1355029b7b03..52de494374ae 100644 --- a/vendor/github.com/hashicorp/go-azure-sdk/sdk/client/client.go +++ b/vendor/github.com/hashicorp/go-azure-sdk/sdk/client/client.go @@ -9,6 +9,7 @@ import ( "crypto/tls" "encoding/json" "encoding/xml" + "errors" "fmt" "io" "log" @@ -16,6 +17,7 @@ import ( "net" "net/http" "net/url" + "regexp" "runtime" "strconv" "strings" @@ -443,16 +445,8 @@ func (c *Client) Execute(ctx context.Context, req *Request) (*Response, error) { // Instantiate a RetryableHttp client and configure its CheckRetry func r := c.retryableClient(ctx, func(ctx context.Context, r *http.Response, err error) (bool, error) { - // First check for badly malformed responses - if r == nil { - if req.IsIdempotent() { - return true, nil - } - return false, fmt.Errorf("HTTP response was nil; connection may have been reset") - } - // Eventual consistency checks - if !c.DisableRetries { + if r != nil && !c.DisableRetries { if r.StatusCode == http.StatusFailedDependency { return true, nil } @@ -474,6 +468,14 @@ func (c *Client) Execute(ctx context.Context, req *Request) (*Response, error) { } } + // Check for failed connections etc and decide if retries are appropriate + if r == nil { + if req.IsIdempotent() { + return extendedRetryPolicy(r, err) + } + return false, fmt.Errorf("HTTP response was nil; connection may have been reset") + } + // Fall back to default retry policy to handle rate limiting, server errors etc. return retryablehttp.DefaultRetryPolicy(ctx, r, err) }) @@ -692,14 +694,28 @@ func (c *Client) retryableClient(ctx context.Context, checkRetry retryablehttp.C r.RetryWaitMin = 1 * time.Second r.RetryWaitMax = 61 * time.Second + // The default backoff results into the following formula T(n): + // ("t" repr. total time in sec, "n" repr. total retry count): + // - t = 2**(n+1) - 1 (0<=n<6) + // - t = (1+2+4+8+16+32) + 61*(n-6) (n>6) + // This results into the following N(t) (by guaranteeing T(n) <= t): + // - n = floor(log(t+1)) - 1 (0<=t<=63) + // - n = (t - 63)/61 + 6 (t > 63) + var safeRetryNumber = func(t time.Duration) int { + sec := t.Seconds() + if sec <= 63 { + return int(math.Floor(math.Log2(sec+1))) - 1 + } + return (int(sec)-63)/61 + 6 + } + // Default RetryMax of 16 takes approx 10 minutes to iterate r.RetryMax = 16 - // Extend the RetryMax if the context timeout exceeds 10 minutes + // In case the context has deadline defined, adjust the retry count to a value + // that the total time spent for retrying is right before the deadline exceeded. if deadline, ok := ctx.Deadline(); ok { - if timeout := deadline.Sub(time.Now()); timeout > 10*time.Minute { - r.RetryMax = int(math.Round(timeout.Minutes())) + 6 - } + r.RetryMax = safeRetryNumber(deadline.Sub(time.Now())) } tlsConfig := tls.Config{ @@ -735,3 +751,92 @@ func containsStatusCode(expected []int, actual int) bool { return false } + +// extendedRetryPolicy extends the defaultRetryPolicy implementation in go-retryablehhtp with +// additional error conditions that should not be retried indefinitely +func extendedRetryPolicy(resp *http.Response, err error) (bool, error) { + // A regular expression to match the error returned by net/http when the + // configured number of redirects is exhausted. This error isn't typed + // specifically so we resort to matching on the error string. + redirectsErrorRe := regexp.MustCompile(`stopped after \d+ redirects\z`) + + // A regular expression to match the error returned by net/http when the + // scheme specified in the URL is invalid. This error isn't typed + // specifically so we resort to matching on the error string. + schemeErrorRe := regexp.MustCompile(`unsupported protocol scheme`) + + // A regular expression to match the error returned by net/http when a + // request header or value is invalid. This error isn't typed + // specifically so we resort to matching on the error string. + invalidHeaderErrorRe := regexp.MustCompile(`invalid header`) + + // A regular expression to match the error returned by net/http when the + // TLS certificate is not trusted. This error isn't typed + // specifically so we resort to matching on the error string. + notTrustedErrorRe := regexp.MustCompile(`certificate is not trusted`) + + // A regular expression to catch dial timeouts in the underlying TCP session + // connection + tcpDialTCPRe := regexp.MustCompile(`dial tcp`) + + // A regular expression to match complete packet loss + completePacketLossRe := regexp.MustCompile(`EOF`) + + if err != nil { + var v *url.Error + if errors.As(err, &v) { + // Don't retry if the error was due to too many redirects. + if redirectsErrorRe.MatchString(v.Error()) { + return false, v + } + + // Don't retry if the error was due to an invalid protocol scheme. + if schemeErrorRe.MatchString(v.Error()) { + return false, v + } + + // Don't retry if the error was due to an invalid header. + if invalidHeaderErrorRe.MatchString(v.Error()) { + return false, v + } + + // Don't retry if the error was due to TLS cert verification failure. + if notTrustedErrorRe.MatchString(v.Error()) { + return false, v + } + + if tcpDialTCPRe.MatchString(v.Error()) { + return false, v + } + + if completePacketLossRe.MatchString(v.Error()) { + return false, v + } + + var certificateVerificationError *tls.CertificateVerificationError + if ok := errors.As(v.Err, &certificateVerificationError); ok { + return false, v + } + } + + // The error is likely recoverable so retry. + return true, nil + } + + // 429 Too Many Requests is recoverable. Sometimes the server puts + // a Retry-After response header to indicate when the server is + // available to start processing request from client. + if resp.StatusCode == http.StatusTooManyRequests { + return true, nil + } + + // Check the response code. We retry on 500-range responses to allow + // the server time to recover, as 500's are typically not permanent + // errors and may relate to outages on the server side. This will catch + // invalid response codes as well, like 0 and 999. + if resp.StatusCode == 0 || (resp.StatusCode >= 500 && resp.StatusCode != http.StatusNotImplemented) { + return true, fmt.Errorf("unexpected HTTP status %s", resp.Status) + } + + return false, nil +} diff --git a/vendor/github.com/hashicorp/go-azure-sdk/sdk/client/msgraph/client.go b/vendor/github.com/hashicorp/go-azure-sdk/sdk/client/msgraph/client.go index 3cf67d08f25d..0a74b22cad1c 100644 --- a/vendor/github.com/hashicorp/go-azure-sdk/sdk/client/msgraph/client.go +++ b/vendor/github.com/hashicorp/go-azure-sdk/sdk/client/msgraph/client.go @@ -35,7 +35,7 @@ type Client struct { tenantId string } -func NewMsGraphClient(api environments.Api, serviceName string, apiVersion ApiVersion) (*Client, error) { +func NewClient(api environments.Api, serviceName string, apiVersion ApiVersion) (*Client, error) { endpoint, ok := api.Endpoint() if !ok { return nil, fmt.Errorf("no `endpoint` was returned for this environment") @@ -49,6 +49,11 @@ func NewMsGraphClient(api environments.Api, serviceName string, apiVersion ApiVe }, nil } +// Deprecated: use NewClient instead +func NewMsGraphClient(api environments.Api, serviceName string, apiVersion ApiVersion) (*Client, error) { + return NewClient(api, serviceName, apiVersion) +} + func (c *Client) NewRequest(ctx context.Context, input client.RequestOptions) (*client.Request, error) { // TODO move these validations to base client method if _, ok := ctx.Deadline(); !ok { diff --git a/vendor/github.com/hashicorp/go-azure-sdk/sdk/client/resourcemanager/client.go b/vendor/github.com/hashicorp/go-azure-sdk/sdk/client/resourcemanager/client.go index 8c9b67a9c4fe..9cd3335f1ba8 100644 --- a/vendor/github.com/hashicorp/go-azure-sdk/sdk/client/resourcemanager/client.go +++ b/vendor/github.com/hashicorp/go-azure-sdk/sdk/client/resourcemanager/client.go @@ -23,7 +23,7 @@ type Client struct { apiVersion string } -func NewResourceManagerClient(api environments.Api, serviceName, apiVersion string) (*Client, error) { +func NewClient(api environments.Api, serviceName, apiVersion string) (*Client, error) { endpoint, ok := api.Endpoint() if !ok { return nil, fmt.Errorf("no `endpoint` was returned for this environment") @@ -38,6 +38,11 @@ func NewResourceManagerClient(api environments.Api, serviceName, apiVersion stri }, nil } +// Deprecated: use NewClient instead +func NewResourceManagerClient(api environments.Api, serviceName, apiVersion string) (*Client, error) { + return NewClient(api, serviceName, apiVersion) +} + func (c *Client) NewRequest(ctx context.Context, input client.RequestOptions) (*client.Request, error) { // TODO move these validations to base client method if _, ok := ctx.Deadline(); !ok { diff --git a/vendor/github.com/hashicorp/go-azure-sdk/sdk/client/resourcemanager/poller_lro.go b/vendor/github.com/hashicorp/go-azure-sdk/sdk/client/resourcemanager/poller_lro.go index 0eade8da42ee..527257c3514b 100644 --- a/vendor/github.com/hashicorp/go-azure-sdk/sdk/client/resourcemanager/poller_lro.go +++ b/vendor/github.com/hashicorp/go-azure-sdk/sdk/client/resourcemanager/poller_lro.go @@ -7,6 +7,7 @@ import ( "bytes" "context" "encoding/json" + "errors" "fmt" "io" "net/http" @@ -27,6 +28,9 @@ type longRunningOperationPoller struct { initialRetryDuration time.Duration originalUrl *url.URL pollingUrl *url.URL + + droppedConnectionCount int + maxDroppedConnections int } func pollingUriForLongRunningOperation(resp *client.Response) string { @@ -39,8 +43,9 @@ func pollingUriForLongRunningOperation(resp *client.Response) string { func longRunningOperationPollerFromResponse(resp *client.Response, client *client.Client) (*longRunningOperationPoller, error) { poller := longRunningOperationPoller{ - client: client, - initialRetryDuration: 10 * time.Second, + client: client, + initialRetryDuration: 10 * time.Second, + maxDroppedConnections: 3, } pollingUrl := pollingUriForLongRunningOperation(resp) @@ -107,9 +112,20 @@ func (p *longRunningOperationPoller) Poll(ctx context.Context) (result *pollers. } result.HttpResponse, err = req.Execute(ctx) if err != nil { + var e *url.Error + if errors.As(err, &e) { + p.droppedConnectionCount++ + if p.droppedConnectionCount < p.maxDroppedConnections { + result.Status = pollers.PollingStatusUnknown + return result, nil + } + } + return nil, err } + p.droppedConnectionCount = 0 + if result.HttpResponse != nil { var respBody []byte respBody, err = io.ReadAll(result.HttpResponse.Body) diff --git a/vendor/github.com/hashicorp/go-azure-sdk/sdk/client/resourcemanager/poller_lro_statuses.go b/vendor/github.com/hashicorp/go-azure-sdk/sdk/client/resourcemanager/poller_lro_statuses.go index bc9806bed6ed..3b0ecec14ba2 100644 --- a/vendor/github.com/hashicorp/go-azure-sdk/sdk/client/resourcemanager/poller_lro_statuses.go +++ b/vendor/github.com/hashicorp/go-azure-sdk/sdk/client/resourcemanager/poller_lro_statuses.go @@ -25,6 +25,9 @@ var longRunningOperationCustomStatuses = map[status]pollers.PollingStatus{ // NetAppVolumeReplication @ 2023-05-01 returns `AuthorizeReplication` during authorizing replication "AuthorizeReplication": pollers.PollingStatusInProgress, + // VMWare @ 2022-05-01 returns `Building` rather than `InProgress` during creation + "Building": pollers.PollingStatusInProgress, + // NetAppVolumeReplication @ 2023-05-01 returns `BreakReplication` during breaking replication "BreakReplication": pollers.PollingStatusInProgress, diff --git a/vendor/github.com/hashicorp/go-azure-sdk/sdk/client/resourcemanager/poller_provisioning_state.go b/vendor/github.com/hashicorp/go-azure-sdk/sdk/client/resourcemanager/poller_provisioning_state.go index f5c6f2e81d7a..cf80b2c4f0d9 100644 --- a/vendor/github.com/hashicorp/go-azure-sdk/sdk/client/resourcemanager/poller_provisioning_state.go +++ b/vendor/github.com/hashicorp/go-azure-sdk/sdk/client/resourcemanager/poller_provisioning_state.go @@ -5,6 +5,7 @@ package resourcemanager import ( "context" + "errors" "fmt" "net/http" "net/url" @@ -27,6 +28,9 @@ type provisioningStatePoller struct { initialRetryDuration time.Duration originalUri string resourcePath string + + droppedConnectionCount int + maxDroppedConnections int } func provisioningStatePollerFromResponse(response *client.Response, lroIsSelfReference bool, client *Client, pollingInterval time.Duration) (*provisioningStatePoller, error) { @@ -58,11 +62,12 @@ func provisioningStatePollerFromResponse(response *client.Response, lroIsSelfRef } return &provisioningStatePoller{ - apiVersion: apiVersion, - client: client, - initialRetryDuration: pollingInterval, - originalUri: originalUri, - resourcePath: resourcePath, + apiVersion: apiVersion, + client: client, + initialRetryDuration: pollingInterval, + originalUri: originalUri, + resourcePath: resourcePath, + maxDroppedConnections: 3, }, nil } @@ -84,8 +89,22 @@ func (p *provisioningStatePoller) Poll(ctx context.Context) (*pollers.PollResult } resp, err := p.client.Execute(ctx, req) if err != nil { + var e *url.Error + if errors.As(err, &e) { + p.droppedConnectionCount++ + if p.droppedConnectionCount < p.maxDroppedConnections { + return &pollers.PollResult{ + PollInterval: p.initialRetryDuration, + Status: pollers.PollingStatusUnknown, + }, nil + } + } + return nil, fmt.Errorf("executing request: %+v", err) } + + p.droppedConnectionCount = 0 + if resp == nil { return nil, pollers.PollingDroppedConnectionError{} } diff --git a/vendor/modules.txt b/vendor/modules.txt index 244123ac46f8..707fc18998d2 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1122,7 +1122,7 @@ github.com/hashicorp/go-azure-sdk/resource-manager/workloads/2023-04-01/saplands github.com/hashicorp/go-azure-sdk/resource-manager/workloads/2023-04-01/saprecommendations github.com/hashicorp/go-azure-sdk/resource-manager/workloads/2023-04-01/sapsupportedsku github.com/hashicorp/go-azure-sdk/resource-manager/workloads/2023-04-01/sapvirtualinstances -# github.com/hashicorp/go-azure-sdk/sdk v0.20240731.1212841 +# github.com/hashicorp/go-azure-sdk/sdk v0.20241025.1143247 ## explicit; go 1.21 github.com/hashicorp/go-azure-sdk/sdk/auth github.com/hashicorp/go-azure-sdk/sdk/auth/autorest diff --git a/website/docs/r/storage_account_queue_properties.html.markdown b/website/docs/r/storage_account_queue_properties.html.markdown new file mode 100644 index 000000000000..d149b95a0274 --- /dev/null +++ b/website/docs/r/storage_account_queue_properties.html.markdown @@ -0,0 +1,144 @@ +--- +subcategory: "Storage" +layout: "azurerm" +page_title: "Azure Resource Manager: azurerm_storage_account_queue_properties" +description: |- + Manages the Queue Properties of an Azure Storage Account. +--- + +# azurerm_storage_account_queue_properties + +Manages the Queue Properties of an Azure Storage Account. + +## Example Usage + +```hcl +resource "azurerm_resource_group" "example" { + name = "example-resources" + location = "West Europe" +} + +resource "azurerm_storage_account" "example" { + name = "storageaccountname" + resource_group_name = azurerm_resource_group.example.name + location = azurerm_resource_group.example.location + account_tier = "Standard" + account_replication_type = "GRS" + + tags = { + environment = "staging" + } +} + +resource "azurerm_storage_account_queue_properties" "example" { + storage_account_id = azurerm_storage_account.example.id + cors_rule { + allowed_origins = ["http://www.example.com"] + exposed_headers = ["x-tempo-*"] + allowed_headers = ["x-tempo-*"] + allowed_methods = ["GET", "PUT"] + max_age_in_seconds = "500" + } + + logging { + version = "1.0" + delete = true + read = true + write = true + retention_policy_days = 7 + } + + hour_metrics { + version = "1.0" + retention_policy_days = 7 + } + + minute_metrics { + version = "1.0" + retention_policy_days = 7 + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `storage_account_id` - (Required) The ID of the Storage Account to set Queue Properties on. Changing this forces a new resource to be created. + +* `cors_rule` - (Optional) A `cors_rule` block as defined above. + +* `logging` - (Optional) A `logging` block as defined below. + +* `minute_metrics` - (Optional) A `minute_metrics` block as defined below. + +* `hour_metrics` - (Optional) A `hour_metrics` block as defined below. + +~> **NOTE:** At least one of `cors_rule`, `logging`, `minute_metrics`, or `hour_metrics` must be specified. + +--- + +A `cors_rule` block supports the following: + +* `allowed_headers` - (Required) A list of headers that are allowed to be a part of the cross-origin request. + +* `allowed_methods` - (Required) A list of HTTP methods that are allowed to be executed by the origin. Valid options are + `DELETE`, `GET`, `HEAD`, `MERGE`, `POST`, `OPTIONS`, `PUT` or `PATCH`. + +* `allowed_origins` - (Required) A list of origin domains that will be allowed by CORS. + +* `exposed_headers` - (Required) A list of response headers that are exposed to CORS clients. + +* `max_age_in_seconds` - (Required) The number of seconds the client should cache a preflight response. + +--- + +An `hour_metrics` block supports the following: + +* `version` - (Required) The version of storage analytics to configure. + +* `include_apis` - (Optional) Indicates whether metrics should generate summary statistics for called API operations. + +* `retention_policy_days` - (Optional) Specifies the number of days that logs will be retained. + +--- + +A `logging` block supports the following: + +* `delete` - (Required) Indicates whether all delete requests should be logged. + +* `read` - (Required) Indicates whether all read requests should be logged. + +* `version` - (Required) The version of storage analytics to configure. + +* `write` - (Required) Indicates whether all write requests should be logged. + +* `retention_policy_days` - (Optional) Specifies the number of days that logs will be retained. + +--- + +A `minute_metrics` block supports the following: + +* `version` - (Required) The version of storage analytics to configure. + +* `include_apis` - (Optional) Indicates whether metrics should generate summary statistics for called API operations. + +* `retention_policy_days` - (Optional) Specifies the number of days that logs will be retained. + + +## Timeouts + +The `timeouts` block allows you to specify [timeouts](https://www.terraform.io/language/resources/syntax#operation-timeouts) for certain actions: + +* `create` - (Defaults to 30 minutes) Used when creating the Storage Account Queue Properties. +* `update` - (Defaults to 30 minutes) Used when updating the Storage Account Queue Properties. +* `read` - (Defaults to 5 minutes) Used when retrieving the Storage Account Queue Properties. +* `delete` - (Defaults to 30 minutes) Used when deleting the Storage Account Queue Properties. + +## Import + +Storage Account Queue Properties can be imported using the `resource id`, e.g. + +```shell +terraform import azurerm_storage_account_queue_properties.queueprops /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/myresourcegroup/providers/Microsoft.Storage/storageAccounts/myaccount +``` \ No newline at end of file diff --git a/website/docs/r/storage_account_static_website.html.markdown b/website/docs/r/storage_account_static_website.html.markdown new file mode 100644 index 000000000000..7f637c53a0b8 --- /dev/null +++ b/website/docs/r/storage_account_static_website.html.markdown @@ -0,0 +1,66 @@ +--- +subcategory: "Storage" +layout: "azurerm" +page_title: "Azure Resource Manager: azurerm_storage_account_static_website" +description: |- + Manages the Static Website of an Azure Storage Account. +--- + +# azurerm_storage_account_static_website + +Manages the Static Website of an Azure Storage Account. + +## Example Usage + +```hcl +resource "azurerm_resource_group" "example" { + name = "example-resources" + location = "West Europe" +} + +resource "azurerm_storage_account" "example" { + name = "storageaccountname" + resource_group_name = azurerm_resource_group.example.name + location = azurerm_resource_group.example.location + account_tier = "Standard" + account_replication_type = "GRS" + + tags = { + environment = "staging" + } +} + +resource "azurerm_storage_account_static_website" "test" { + storage_account_id = azurerm_storage_account.test.id + error_404_document = "custom_not_found.html" + index_document = "custom_index.html" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `storage_account_id` - (Required) The ID of the Storage Account to set Static Website on. Changing this forces a new resource to be created. + +* `error_404_document` - (Optional) The absolute path to a custom webpage that should be used when a request is made which does not correspond to an existing file. + +* `index_document` - (Optional) The webpage that Azure Storage serves for requests to the root of a website or any subfolder. For example, index.html. + + +## Timeouts + +The `timeouts` block allows you to specify [timeouts](https://www.terraform.io/language/resources/syntax#operation-timeouts) for certain actions: + +* `create` - (Defaults to 30 minutes) Used when creating the Storage Account Static Website . +* `update` - (Defaults to 30 minutes) Used when updating the Storage Account Static Website . +* `read` - (Defaults to 5 minutes) Used when retrieving the Storage Account Static Website . +* `delete` - (Defaults to 30 minutes) Used when deleting the Storage Account Static Website . + +## Import + +Storage Account Static Websites can be imported using the `resource id`, e.g. + +```shell +terraform import azurerm_storage_account_static_website.mysite /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/myresourcegroup/providers/Microsoft.Storage/storageAccounts/myaccount +``` \ No newline at end of file