diff --git a/.github/labeler-issue-triage.yml b/.github/labeler-issue-triage.yml index fd8d38ae89d5..cb412caf3b43 100644 --- a/.github/labeler-issue-triage.yml +++ b/.github/labeler-issue-triage.yml @@ -312,7 +312,7 @@ 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/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)*)###' diff --git a/internal/features/defaults.go b/internal/features/defaults.go index 1d152cea3acc..182016f29801 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 1c42fa1b7bcf..e39c3ce0c741 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, @@ -580,6 +595,15 @@ 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{}) diff --git a/internal/provider/features_test.go b/internal/provider/features_test.go index c33addf281d0..81cfe4b6ade8 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 5fabeb930c92..c34676741127 100644 --- a/internal/provider/framework/config.go +++ b/internal/provider/framework/config.go @@ -415,6 +415,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..485420b0a392 100644 --- a/internal/provider/framework/config_test.go +++ b/internal/provider/framework/config_test.go @@ -275,6 +275,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 +319,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 73846ced22b4..66a6c596a0d9 100644 --- a/internal/provider/framework/provider.go +++ b/internal/provider/framework/provider.go @@ -406,6 +406,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 87e33432fca6..87dda6070658 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" @@ -209,17 +209,11 @@ func resourceStorageAccount() *pluginsdk.Resource { }, }, - "cross_tenant_replication_enabled": func() *pluginsdk.Schema { - s := &pluginsdk.Schema{ - Type: pluginsdk.TypeBool, - Optional: true, - Default: false, - } - if !features.FourPointOhBeta() { - s.Default = true - } - return s - }(), + "cross_tenant_replication_enabled": { + Type: pluginsdk.TypeBool, + Optional: true, + Default: false, + }, "custom_domain": { Type: pluginsdk.TypeList, @@ -524,108 +518,6 @@ func resourceStorageAccount() *pluginsdk.Resource { }, }, - "queue_properties": { - Type: pluginsdk.TypeList, - Optional: true, - Computed: true, - MaxItems: 1, - Elem: &pluginsdk.Resource{ - Schema: map[string]*pluginsdk.Schema{ - "cors_rule": helpers.SchemaStorageAccountCorsRule(false), - "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, - }, - // TODO 4.0: Remove this property and determine whether to enable based on existence of the out side block. - "enabled": { - Type: pluginsdk.TypeBool, - Required: true, - }, - "include_apis": { - Type: pluginsdk.TypeBool, - Optional: true, - }, - "retention_policy_days": { - Type: pluginsdk.TypeInt, - Optional: true, - ValidateFunc: validation.IntBetween(1, 365), - }, - }, - }, - }, - "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), - }, - }, - }, - }, - "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, - }, - // TODO 4.0: Remove this property and determine whether to enable based on existence of the out side block. - "enabled": { - Type: pluginsdk.TypeBool, - Required: true, - }, - "include_apis": { - Type: pluginsdk.TypeBool, - Optional: true, - }, - "retention_policy_days": { - Type: pluginsdk.TypeInt, - Optional: true, - ValidateFunc: validation.IntBetween(1, 365), - }, - }, - }, - }, - }, - }, - }, - "routing": { Type: pluginsdk.TypeList, Optional: true, @@ -658,7 +550,6 @@ func resourceStorageAccount() *pluginsdk.Resource { "share_properties": { Type: pluginsdk.TypeList, Optional: true, - // (@jackofallops) TODO - This should not be computed, however, this would be a breaking change with unknown implications for user data so needs to be addressed for 4.0 Computed: true, MaxItems: 1, Elem: &pluginsdk.Resource{ @@ -749,27 +640,6 @@ func resourceStorageAccount() *pluginsdk.Resource { }, }, - // lintignore:XS003 - "static_website": { - Type: pluginsdk.TypeList, - Optional: true, - MaxItems: 1, - Elem: &pluginsdk.Resource{ - Schema: map[string]*pluginsdk.Schema{ - "error_404_document": { - Type: pluginsdk.TypeString, - Optional: true, - ValidateFunc: validation.StringIsNotEmpty, - }, - "index_document": { - Type: pluginsdk.TypeString, - Optional: true, - ValidateFunc: validation.StringIsNotEmpty, - }, - }, - }, - }, - "queue_encryption_key_type": { Type: pluginsdk.TypeString, Optional: true, @@ -1246,6 +1116,15 @@ func resourceStorageAccount() *pluginsdk.Resource { } } + if !features.FivePointOhBeta() && !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 { @@ -1266,28 +1145,144 @@ func resourceStorageAccount() *pluginsdk.Resource { ), } - if !features.FourPointOhBeta() { - resource.Schema["https_traffic_only_enabled"].Computed = true - resource.Schema["https_traffic_only_enabled"].Default = nil - - resource.Schema["enable_https_traffic_only"] = &pluginsdk.Schema{ - Type: pluginsdk.TypeBool, - Optional: true, - Computed: true, - ConflictsWith: []string{"https_traffic_only_enabled"}, - Deprecated: "The property `enable_https_traffic_only` has been superseded by `https_traffic_only_enabled` and will be removed in v4.0 of the AzureRM Provider.", + if !features.FivePointOhBeta() { + // lintignore:XS003 + resource.Schema["static_website"] = &pluginsdk.Schema{ + Type: pluginsdk.TypeList, + Optional: true, + Computed: true, + MaxItems: 1, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "error_404_document": { + Type: pluginsdk.TypeString, + Optional: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + "index_document": { + Type: pluginsdk.TypeString, + Optional: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + }, + }, + Deprecated: "this block has been deprecated and superseded by the `azurerm_storage_account_static_website` resource and will be removed in v5.0 of the AzureRM provider", } } + resource.Schema["queue_properties"] = &pluginsdk.Schema{ + Type: pluginsdk.TypeList, + Optional: true, + Computed: true, + MaxItems: 1, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "cors_rule": helpers.SchemaStorageAccountCorsRule(false), + "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, + }, + "enabled": { + Type: pluginsdk.TypeBool, + Required: true, + }, + "include_apis": { + Type: pluginsdk.TypeBool, + Optional: true, + }, + "retention_policy_days": { + Type: pluginsdk.TypeInt, + Optional: true, + ValidateFunc: validation.IntBetween(1, 365), + }, + }, + }, + }, + "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), + }, + }, + }, + }, + "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, + }, + // TODO 4.0: Remove this property and determine whether to enable based on existence of the out side block. + "enabled": { + Type: pluginsdk.TypeBool, + Required: true, + }, + "include_apis": { + Type: pluginsdk.TypeBool, + Optional: true, + }, + "retention_policy_days": { + Type: pluginsdk.TypeInt, + Optional: true, + ValidateFunc: validation.IntBetween(1, 365), + }, + }, + }, + }, + }, + }, + Deprecated: "this block has been deprecated and superseded by the `azurerm_storage_account_queue_properties` resource and will be removed in v5.0 of the AzureRM provider", + } + return resource } func resourceStorageAccountCreate(d *pluginsdk.ResourceData, meta interface{}) error { tenantId := meta.(*clients.Client).Account.TenantId subscriptionId := meta.(*clients.Client).Account.SubscriptionId - client := meta.(*clients.Client).Storage.ResourceManager.StorageAccounts - storageClient := meta.(*clients.Client).Storage + storageUtils := meta.(*clients.Client).Storage + storageClient := meta.(*clients.Client).Storage.ResourceManager + client := storageClient.StorageAccounts keyVaultClient := meta.(*clients.Client).KeyVault + dataPlaneAvailable := meta.(*clients.Client).Features.Storage.DataPlaneAvailable ctx, cancel := timeouts.ForCreate(meta.(*clients.Client).StopContext, d) defer cancel() @@ -1322,11 +1317,6 @@ func resourceStorageAccountCreate(d *pluginsdk.ResourceData, meta interface{}) e // nolint staticcheck if v, ok := d.GetOkExists("https_traffic_only_enabled"); ok { httpsTrafficOnlyEnabled = v.(bool) - } else if !features.FourPointOhBeta() { - // nolint staticcheck - if v, ok := d.GetOkExists("enable_https_traffic_only"); ok { - httpsTrafficOnlyEnabled = v.(bool) - } } dnsEndpointType := d.Get("dns_endpoint_type").(string) @@ -1440,7 +1430,7 @@ func resourceStorageAccountCreate(d *pluginsdk.ResourceData, meta interface{}) e payload.Properties.RoutingPreference = expandAccountRoutingPreference(v.([]interface{})) } - // TODO 4.0: look into standardizing this across resources that support CMK and at the very least look at improving the UX + // TODO look into standardizing this across resources that support CMK and at the very least look at improving the UX // for encryption of blob, file, table and queue // // By default (by leaving empty), the table and queue encryption key type is set to "Service". While users can change it to "Account" so that @@ -1482,21 +1472,70 @@ func resourceStorageAccountCreate(d *pluginsdk.ResourceData, meta interface{}) e if account.Model == nil { return fmt.Errorf("retrieving %s: `model` was nil", id) } - if err := storageClient.AddToCache(id, *account.Model); err != nil { + if err := storageUtils.AddToCache(id, *account.Model); err != nil { 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 - this entire block can be removed for 5.0, as the data_plane_available flag becomes redundant at that time. + if !features.FivePointOhBeta() && dataPlaneAvailable { + dataPlaneClient := meta.(*clients.Client).Storage + dataPlaneAccount, err := storageUtils.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, dataPlaneClient, dataPlaneAccount, supportLevel); err != nil { + return fmt.Errorf("waiting for the Data Plane for %s to become available: %+v", id, err) + } + + 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 := dataPlaneClient.QueuesDataPlaneClient(ctx, *dataPlaneAccount, dataPlaneClient.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 err = d.Set("queue_properties", val); err != nil { + return fmt.Errorf("setting `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 := dataPlaneClient.AccountsDataPlaneClient(ctx, *dataPlaneAccount, dataPlaneClient.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) + } + + if err = d.Set("static_website", val); err != nil { + return fmt.Errorf("setting `static_website`: %+v", err) + } + } } if val, ok := d.GetOk("blob_properties"); ok { @@ -1547,31 +1586,11 @@ func resourceStorageAccountCreate(d *pluginsdk.ResourceData, meta interface{}) e } } - if _, err = storageClient.ResourceManager.BlobService.SetServiceProperties(ctx, id, *blobProperties); err != nil { + if _, err = storageClient.BlobService.SetServiceProperties(ctx, id, *blobProperties); err != nil { return fmt.Errorf("updating `blob_properties`: %+v", err) } } - 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) @@ -1592,35 +1611,18 @@ func resourceStorageAccountCreate(d *pluginsdk.ResourceData, meta interface{}) e } } - if _, err = storageClient.ResourceManager.FileService.SetServiceProperties(ctx, id, sharePayload); err != nil { + if _, err = storageClient.FileService.SetServiceProperties(ctx, id, sharePayload); err != nil { return fmt.Errorf("updating `share_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) - } - } - return resourceStorageAccountRead(d, meta) } func resourceStorageAccountUpdate(d *pluginsdk.ResourceData, meta interface{}) error { tenantId := meta.(*clients.Client).Account.TenantId - storageClient := meta.(*clients.Client).Storage - client := storageClient.ResourceManager.StorageAccounts + storageClient := meta.(*clients.Client).Storage.ResourceManager + client := storageClient.StorageAccounts keyVaultClient := meta.(*clients.Client).KeyVault ctx, cancel := timeouts.ForUpdate(meta.(*clients.Client).StopContext, d) defer cancel() @@ -1755,11 +1757,6 @@ func resourceStorageAccountUpdate(d *pluginsdk.ResourceData, meta interface{}) e if d.HasChange("https_traffic_only_enabled") { props.SupportsHTTPSTrafficOnly = pointer.To(d.Get("https_traffic_only_enabled").(bool)) } - if !features.FourPointOhBeta() { - if d.HasChange("enable_https_traffic_only") { - props.SupportsHTTPSTrafficOnly = pointer.To(d.Get("enable_https_traffic_only").(bool)) - } - } if d.HasChange("large_file_share_enabled") { // largeFileSharesState can only be set to `Enabled` and not `Disabled`, even if it is currently `Disabled` @@ -1890,7 +1887,7 @@ func resourceStorageAccountUpdate(d *pluginsdk.ResourceData, meta interface{}) e RestorePolicy: expandAccountBlobPropertiesRestorePolicy(v.([]interface{})), }, } - if _, err := storageClient.ResourceManager.BlobService.SetServiceProperties(ctx, *id, blobPayload); err != nil { + if _, err := storageClient.BlobService.SetServiceProperties(ctx, *id, blobPayload); err != nil { return fmt.Errorf("updating Azure Storage Account blob restore policy %q: %+v", id.StorageAccountName, err) } } @@ -1904,36 +1901,64 @@ func resourceStorageAccountUpdate(d *pluginsdk.ResourceData, meta interface{}) e } } - if _, err = storageClient.ResourceManager.BlobService.SetServiceProperties(ctx, *id, *blobProperties); err != nil { + if _, err = storageClient.BlobService.SetServiceProperties(ctx, *id, *blobProperties); err != nil { return fmt.Errorf("updating `blob_properties` for %s: %+v", *id, err) } } - if d.HasChange("queue_properties") { - if !supportLevel.supportQueue { - return fmt.Errorf("`queue_properties` aren't supported for account kind %q in sku tier %q", accountKind, accountTier) - } + if !features.FivePointOhBeta() { + dataPlaneClient := meta.(*clients.Client).Storage + if d.HasChange("queue_properties") { + if !supportLevel.supportQueue { + return fmt.Errorf("`queue_properties` aren't supported for account kind %q in sku tier %q", accountKind, accountTier) + } - 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) - } + account, err := dataPlaneClient.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) + } - queueClient, err := storageClient.QueuesDataPlaneClient(ctx, *account, storageClient.DataPlaneOperationSupportingAnyAuthMethod()) - if err != nil { - return fmt.Errorf("building Queues Client: %s", err) - } + queueClient, err := dataPlaneClient.QueuesDataPlaneClient(ctx, *account, dataPlaneClient.DataPlaneOperationSupportingAnyAuthMethod()) + if err != nil { + return fmt.Errorf("building Queues Client: %s", err) + } - queueProperties, err := expandAccountQueueProperties(d.Get("queue_properties").([]interface{})) - if err != nil { - return fmt.Errorf("expanding `queue_properties` for %s: %+v", *id, err) + queueProperties, err := expandAccountQueueProperties(d.Get("queue_properties").([]interface{})) + if err != nil { + return fmt.Errorf("expanding `queue_properties` for %s: %+v", *id, err) + } + + if err = queueClient.UpdateServiceProperties(ctx, *queueProperties); err != nil { + return fmt.Errorf("updating Queue Properties for %s: %+v", *id, err) + } } - if err = queueClient.UpdateServiceProperties(ctx, *queueProperties); err != nil { - return fmt.Errorf("updating Queue Properties for %s: %+v", *id, err) + if d.HasChange("static_website") { + if !supportLevel.supportStaticWebsite { + return fmt.Errorf("`static_website` aren't supported for account kind %q in sku tier %q", accountKind, accountTier) + } + + account, err := dataPlaneClient.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) + } + + accountsClient, err := dataPlaneClient.AccountsDataPlaneClient(ctx, *account, dataPlaneClient.DataPlaneOperationSupportingAnyAuthMethod()) + if err != nil { + return fmt.Errorf("building Data Plane client for %s: %+v", *id, err) + } + + staticWebsiteProps := expandAccountStaticWebsiteProperties(d.Get("static_website").([]interface{})) + + if _, err = accountsClient.SetServiceProperties(ctx, id.StorageAccountName, staticWebsiteProps); err != nil { + return fmt.Errorf("updating `static_website` for %s: %+v", *id, err) + } } } @@ -1955,42 +1980,19 @@ func resourceStorageAccountUpdate(d *pluginsdk.ResourceData, meta interface{}) e sharePayload.Properties.ProtocolSettings.Smb.Multichannel = nil } - if _, err = storageClient.ResourceManager.FileService.SetServiceProperties(ctx, *id, sharePayload); err != nil { + if _, err = storageClient.FileService.SetServiceProperties(ctx, *id, sharePayload); err != nil { return fmt.Errorf("updating File Share Properties for %s: %+v", *id, err) } } - if d.HasChange("static_website") { - if !supportLevel.supportStaticWebsite { - return fmt.Errorf("`static_website` aren't supported for account kind %q in sku tier %q", accountKind, accountTier) - } - - 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) - } - - accountsClient, err := storageClient.AccountsDataPlaneClient(ctx, *account, storageClient.DataPlaneOperationSupportingAnyAuthMethod()) - if err != nil { - return fmt.Errorf("building Data Plane client for %s: %+v", *id, err) - } - - staticWebsiteProps := expandAccountStaticWebsiteProperties(d.Get("static_website").([]interface{})) - - if _, err = accountsClient.SetServiceProperties(ctx, id.StorageAccountName, staticWebsiteProps); err != nil { - return fmt.Errorf("updating `static_website` for %s: %+v", *id, err) - } - } - return resourceStorageAccountRead(d, meta) } func resourceStorageAccountRead(d *pluginsdk.ResourceData, meta interface{}) error { - storageClient := meta.(*clients.Client).Storage - client := storageClient.ResourceManager.StorageAccounts + storageUtils := meta.(*clients.Client).Storage + storageClient := meta.(*clients.Client).Storage.ResourceManager + client := storageClient.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() @@ -2016,7 +2018,7 @@ func resourceStorageAccountRead(d *pluginsdk.ResourceData, meta interface{}) err } // we then need to find the storage account - account, err := storageClient.FindAccount(ctx, id.SubscriptionId, id.StorageAccountName) + account, err := storageUtils.FindAccount(ctx, id.SubscriptionId, id.StorageAccountName) if err != nil { return fmt.Errorf("retrieving %s: %+v", *id, err) } @@ -2080,9 +2082,6 @@ func resourceStorageAccountRead(d *pluginsdk.ResourceData, meta interface{}) err } d.Set("cross_tenant_replication_enabled", pointer.From(props.AllowCrossTenantReplication)) d.Set("https_traffic_only_enabled", pointer.From(props.SupportsHTTPSTrafficOnly)) - if !features.FourPointOhBeta() { - d.Set("enable_https_traffic_only", pointer.From(props.SupportsHTTPSTrafficOnly)) - } d.Set("is_hns_enabled", pointer.From(props.IsHnsEnabled)) d.Set("nfsv3_enabled", pointer.From(props.IsNfsV3Enabled)) d.Set("primary_location", pointer.From(props.PrimaryLocation)) @@ -2213,7 +2212,7 @@ func resourceStorageAccountRead(d *pluginsdk.ResourceData, meta interface{}) err blobProperties := make([]interface{}, 0) if supportLevel.supportBlob { - blobProps, err := storageClient.ResourceManager.BlobService.GetServiceProperties(ctx, *id) + blobProps, err := storageClient.BlobService.GetServiceProperties(ctx, *id) if err != nil { return fmt.Errorf("reading blob properties for %s: %+v", *id, err) } @@ -2224,28 +2223,9 @@ 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) + shareProps, err := storageClient.FileService.GetServiceProperties(ctx, *id) if err != nil { return fmt.Errorf("retrieving share properties for %s: %+v", *id, err) } @@ -2256,30 +2236,58 @@ 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 !features.FivePointOhBeta() && dataPlaneAvailable { + dataPlaneClient := meta.(*clients.Client).Storage + queueProperties := make([]interface{}, 0) + if supportLevel.supportQueue { + queueClient, err := dataPlaneClient.QueuesDataPlaneClient(ctx, *account, dataPlaneClient.DataPlaneOperationSupportingAnyAuthMethod()) + if err != nil { + return fmt.Errorf("building Queues Client: %s", 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) } - staticWebsiteProps, err := accountsClient.GetServiceProperties(ctx, id.StorageAccountName) - if err != nil { - return fmt.Errorf("retrieving static website properties for %s: %+v", *id, err) + staticWebsiteProperties := make([]interface{}, 0) + if supportLevel.supportStaticWebsite { + accountsClient, err := dataPlaneClient.AccountsDataPlaneClient(ctx, *account, dataPlaneClient.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) } - staticWebsiteProperties = flattenAccountStaticWebsiteProperties(staticWebsiteProps) - } - if err := d.Set("static_website", staticWebsiteProperties); err != nil { - return fmt.Errorf("setting `static_website`: %+v", err) + if err = d.Set("static_website", staticWebsiteProperties); err != nil { + return fmt.Errorf("setting `static_website`: %+v", err) + } } return nil } func resourceStorageAccountDelete(d *pluginsdk.ResourceData, meta interface{}) error { - storageClient := meta.(*clients.Client).Storage - client := storageClient.ResourceManager.StorageAccounts + storageUtils := meta.(*clients.Client).Storage + client := meta.(*clients.Client).Storage.ResourceManager.StorageAccounts ctx, cancel := timeouts.ForDelete(meta.(*clients.Client).StopContext, d) defer cancel() @@ -2296,6 +2304,7 @@ func resourceStorageAccountDelete(d *pluginsdk.ResourceData, meta interface{}) e if response.WasNotFound(existing.HttpResponse) { return nil } + return fmt.Errorf("retrieving %s: %+v", *id, err) } @@ -2330,7 +2339,7 @@ func resourceStorageAccountDelete(d *pluginsdk.ResourceData, meta interface{}) e } // remove this from the cache - storageClient.RemoveAccountFromCache(*id) + storageUtils.RemoveAccountFromCache(*id) return nil } @@ -2360,7 +2369,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.KindStorage { if queueEncryptionKeyType == storageaccounts.KeyTypeAccount { return nil, fmt.Errorf("`queue_encryption_key_type = %q` cannot be used with account kind `%q`", string(storageaccounts.KeyTypeAccount), string(storageaccounts.KindStorage)) diff --git a/internal/services/storage/storage_account_resource_test.go b/internal/services/storage/storage_account_resource_test.go index 2c88afa756cb..b05e6ad5de4d 100644 --- a/internal/services/storage/storage_account_resource_test.go +++ b/internal/services/storage/storage_account_resource_test.go @@ -17,6 +17,7 @@ import ( "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/features" "github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk" "github.com/hashicorp/terraform-provider-azurerm/utils" ) @@ -754,6 +755,10 @@ func TestAccStorageAccount_blobProperties_kindStorageNotSupportLastAccessTimeEna } func TestAccStorageAccount_queueProperties(t *testing.T) { + if features.FivePointOhBeta() { + t.Skip("test not valid in 5.0") + } + data := acceptance.BuildTestData(t, "azurerm_storage_account", "test") r := StorageAccountResource{} @@ -790,6 +795,10 @@ func TestAccStorageAccount_queueProperties(t *testing.T) { } func TestAccStorageAccount_staticWebsiteEnabled(t *testing.T) { + if features.FivePointOhBeta() { + t.Skip("test not valid in 5.0") + } + data := acceptance.BuildTestData(t, "azurerm_storage_account", "test") r := StorageAccountResource{} @@ -820,6 +829,10 @@ func TestAccStorageAccount_staticWebsiteEnabled(t *testing.T) { } func TestAccStorageAccount_staticWebsitePropertiesForStorageV2(t *testing.T) { + if features.FivePointOhBeta() { + t.Skip("test not valid in 5.0") + } + data := acceptance.BuildTestData(t, "azurerm_storage_account", "test") r := StorageAccountResource{} @@ -842,6 +855,10 @@ func TestAccStorageAccount_staticWebsitePropertiesForStorageV2(t *testing.T) { } func TestAccStorageAccount_staticWebsitePropertiesForBlockBlobStorage(t *testing.T) { + if features.FivePointOhBeta() { + t.Skip("test not valid in 5.0") + } + data := acceptance.BuildTestData(t, "azurerm_storage_account", "test") r := StorageAccountResource{} @@ -1695,6 +1712,10 @@ func TestAccStorageAccount_StorageV1_blobProperties(t *testing.T) { } func TestAccStorageAccount_StorageV1_queuePropertiesLRS(t *testing.T) { + if features.FivePointOhBeta() { + t.Skip("test not valid in 5.0") + } + data := acceptance.BuildTestData(t, "azurerm_storage_account", "test") r := StorageAccountResource{} @@ -1710,6 +1731,10 @@ func TestAccStorageAccount_StorageV1_queuePropertiesLRS(t *testing.T) { } func TestAccStorageAccount_StorageV1_queuePropertiesGRS(t *testing.T) { + if features.FivePointOhBeta() { + t.Skip("test not valid in 5.0") + } + data := acceptance.BuildTestData(t, "azurerm_storage_account", "test") r := StorageAccountResource{} @@ -1725,6 +1750,10 @@ func TestAccStorageAccount_StorageV1_queuePropertiesGRS(t *testing.T) { } func TestAccStorageAccount_StorageV1_queuePropertiesRAGRS(t *testing.T) { + if features.FivePointOhBeta() { + t.Skip("test not valid in 5.0") + } + data := acceptance.BuildTestData(t, "azurerm_storage_account", "test") r := StorageAccountResource{} @@ -1784,6 +1813,52 @@ func TestAccStorageAccount_StorageV1_sharePropertiesRAGRS(t *testing.T) { }) } +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) { + if features.FivePointOhBeta() { + t.Skip("test not valid in 5.0") + } + 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) { + if features.FivePointOhBeta() { + t.Skip("test not valid in 5.0") + } + + 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 +5297,108 @@ resource "azurerm_key_vault_managed_hardware_security_module_key" "test" { } `, data.RandomString, data.Locations.Primary, data.RandomInteger) } + +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/website/docs/5.0-upgrade-guide.html.markdown b/website/docs/5.0-upgrade-guide.html.markdown index 95b6939aaf1c..808a662ed3bb 100644 --- a/website/docs/5.0-upgrade-guide.html.markdown +++ b/website/docs/5.0-upgrade-guide.html.markdown @@ -57,6 +57,20 @@ Please follow the format in the example below for listing breaking changes in re * The `example_property_with_changed_default` property now defaults to `NewDefault`. ``` +### `azurerm_storage_account` + +* The deprecated `queue_properties` block has been removed and superseded by the `azurerm_storage_account_queue_properties` resource. +* The deprecated `static_website` block has been removed and superseded by the `azurerm_storage_account_static_website` resource. + +### `azurerm_storage_container` + +* The deprecated `storage_account_name` property has been removed in favour of the `storage_account_id` property. +* The deprecated `resource_manager_id` property has been removed in favour of the `id` property. + +### `azurerm_storage_share` + +* The deprecated `storage_account_name` property has been removed in favour of the `storage_account_id` property. +* The deprecated `resource_manager_id` property has been removed in favour of the `id` property. ## Breaking Changes in Data Sources @@ -68,3 +82,13 @@ Please follow the format in the example below for listing breaking changes in da * The deprecated `example_old_property` property has been removed in favour of the `example_new_property` property. * The deprecated `example_property_with_no_replacement` property has been removed. ``` + +### `azurerm_storage_container` + +* The deprecated `storage_account_name` property has been removed in favour of the `storage_account_id` property. +* The deprecated `resource_manager_id` property has been removed in favour of the `id` property. +* +### `azurerm_storage_share` + +* The deprecated `storage_account_name` property has been removed in favour of the `storage_account_id` property. +* The deprecated `resource_manager_id` property has been removed in favour of the `id` property. 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..8e654ea4aadf --- /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