diff --git a/docs/resources/cloud_project_storage.md b/docs/resources/cloud_project_storage.md index c69c1a07d..c4855b475 100644 --- a/docs/resources/cloud_project_storage.md +++ b/docs/resources/cloud_project_storage.md @@ -93,6 +93,7 @@ Required: Optional: - `storage_class` (String) Destination storage class +- `remove_on_main_bucket_deletion` (Boolean) Whether to remove replicated bucket when the main bucket is deleted (make sure to apply your configuration when changing this value before deleting the main bucket) diff --git a/ovh/resource_cloud_project_storage.go b/ovh/resource_cloud_project_storage.go index f3755a7da..aa5d0fc0f 100644 --- a/ovh/resource_cloud_project_storage.go +++ b/ovh/resource_cloud_project_storage.go @@ -3,6 +3,7 @@ package ovh import ( "context" "fmt" + "net/http" "net/url" "strconv" "strings" @@ -10,6 +11,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/ovh/go-ovh/ovh" ovhtypes "github.com/ovh/terraform-provider-ovh/v2/ovh/types" ) @@ -252,4 +254,33 @@ func (r *cloudProjectStorageResource) Delete(ctx context.Context, req resource.D err.Error(), ) } + + // If replicas exist, delete them manually after the main bucket deletion if option is set + for _, rule := range data.Replication.Rules.Elements() { + destination := rule.(ReplicationRulesValue).Destination + + // Empty region means that replica is already deleted + if destination.Region.ValueString() == "" { + continue + } + + if destination.RemoveOnMainBucketDeletion.ValueBool() { + replicaEndpoint := "/cloud/project/" + url.PathEscape(data.ServiceName.ValueString()) + + "/region/" + url.PathEscape(destination.Region.ValueString()) + + "/storage/" + url.PathEscape(destination.Name.ValueString()) + + tflog.Info(ctx, fmt.Sprintf("removing replica bucket %s", replicaEndpoint)) + if err := r.config.OVHClient.Delete(replicaEndpoint, nil); err != nil { + if ovhErr, ok := err.(*ovh.APIError); ok && ovhErr.Code == http.StatusNotFound { + // If replica was already deleted, ignore the error + continue + } + + resp.Diagnostics.AddError( + fmt.Sprintf("Error removing replica %s", replicaEndpoint), + err.Error(), + ) + } + } + } } diff --git a/ovh/resource_cloud_project_storage_gen.go b/ovh/resource_cloud_project_storage_gen.go index ee40febe8..9516f5490 100644 --- a/ovh/resource_cloud_project_storage_gen.go +++ b/ovh/resource_cloud_project_storage_gen.go @@ -235,6 +235,11 @@ func CloudProjectRegionStorageResourceSchema(ctx context.Context) schema.Schema ), }, }, + "remove_on_main_bucket_deletion": schema.BoolAttribute{ + CustomType: ovhtypes.TfBoolType{}, + Optional: true, + Description: "Whether to remove replicated bucket when the main bucket is deleted", + }, }, CustomType: ReplicationRulesDestinationType{ ObjectType: types.ObjectType{ @@ -787,7 +792,7 @@ func (v *EncryptionValue) MergeWith(other *EncryptionValue) { func (v EncryptionValue) Attributes() map[string]attr.Value { return map[string]attr.Value{ - "sseAlgorithm": v.SseAlgorithm, + "sse_algorithm": v.SseAlgorithm, } } func (v EncryptionValue) ToTerraformValue(ctx context.Context) (tftypes.Value, error) { @@ -1459,14 +1464,14 @@ func (v *ObjectsValue) MergeWith(other *ObjectsValue) { func (v ObjectsValue) Attributes() map[string]attr.Value { return map[string]attr.Value{ - "etag": v.Etag, - "isDeleteMarker": v.IsDeleteMarker, - "isLatest": v.IsLatest, - "key": v.Key, - "lastModified": v.LastModified, - "size": v.Size, - "storageClass": v.StorageClass, - "versionId": v.VersionId, + "etag": v.Etag, + "is_delete_marker": v.IsDeleteMarker, + "is_latest": v.IsLatest, + "key": v.Key, + "last_modified": v.LastModified, + "size": v.Size, + "storage_class": v.StorageClass, + "version_id": v.VersionId, } } func (v ObjectsValue) ToTerraformValue(ctx context.Context) (tftypes.Value, error) { @@ -2605,12 +2610,12 @@ func (v *ReplicationRulesValue) MergeWith(other *ReplicationRulesValue) { func (v ReplicationRulesValue) Attributes() map[string]attr.Value { return map[string]attr.Value{ - "deleteMarkerReplication": v.DeleteMarkerReplication, - "destination": v.Destination, - "filter": v.Filter, - "id": v.Id, - "priority": v.Priority, - "status": v.Status, + "delete_marker_replication": v.DeleteMarkerReplication, + "destination": v.Destination, + "filter": v.Filter, + "id": v.Id, + "priority": v.Priority, + "status": v.Status, } } func (v ReplicationRulesValue) ToTerraformValue(ctx context.Context) (tftypes.Value, error) { @@ -2882,15 +2887,34 @@ func (t ReplicationRulesDestinationType) ValueFromObject(ctx context.Context, in fmt.Sprintf(`storage_class expected to be ovhtypes.TfStringValue, was: %T`, storageClassAttribute)) } + removeOnMainBucketDeletionAttribute, ok := attributes["remove_on_main_bucket_deletion"] + + if !ok { + diags.AddError( + "Attribute Missing", + `remove_on_main_bucket_deletion is missing from object`) + + return nil, diags + } + + removeOnMainBucketDeletionVal, ok := removeOnMainBucketDeletionAttribute.(ovhtypes.TfBoolValue) + + if !ok { + diags.AddError( + "Attribute Wrong Type", + fmt.Sprintf(`remove_on_main_bucket_deletion expected to be ovhtypes.TfBoolValue, was: %T`, removeOnMainBucketDeletionAttribute)) + } + if diags.HasError() { return nil, diags } return ReplicationRulesDestinationValue{ - Name: nameVal, - Region: regionVal, - StorageClass: storageClassVal, - state: attr.ValueStateKnown, + Name: nameVal, + Region: regionVal, + StorageClass: storageClassVal, + RemoveOnMainBucketDeletion: removeOnMainBucketDeletionVal, + state: attr.ValueStateKnown, }, diags } @@ -3011,15 +3035,34 @@ func NewReplicationRulesDestinationValue(attributeTypes map[string]attr.Type, at fmt.Sprintf(`storage_class expected to be ovhtypes.TfStringValue, was: %T`, storageClassAttribute)) } + removeOnMainBucketDeletionAttribute, ok := attributes["remove_on_main_bucket_deletion"] + + if !ok { + diags.AddError( + "Attribute Missing", + `remove_on_main_bucket_deletion is missing from object`) + + return NewReplicationRulesDestinationValueUnknown(), diags + } + + removeOnMainBucketDeletionVal, ok := removeOnMainBucketDeletionAttribute.(ovhtypes.TfBoolValue) + + if !ok { + diags.AddError( + "Attribute Wrong Type", + fmt.Sprintf(`remove_on_main_bucket_deletion expected to be ovhtypes.TfBoolValue, was: %T`, removeOnMainBucketDeletionAttribute)) + } + if diags.HasError() { return NewReplicationRulesDestinationValueUnknown(), diags } return ReplicationRulesDestinationValue{ - Name: nameVal, - Region: regionVal, - StorageClass: storageClassVal, - state: attr.ValueStateKnown, + Name: nameVal, + Region: regionVal, + StorageClass: storageClassVal, + RemoveOnMainBucketDeletion: removeOnMainBucketDeletionVal, + state: attr.ValueStateKnown, }, diags } @@ -3091,10 +3134,11 @@ func (t ReplicationRulesDestinationType) ValueType(ctx context.Context) attr.Val var _ basetypes.ObjectValuable = ReplicationRulesDestinationValue{} type ReplicationRulesDestinationValue struct { - Name ovhtypes.TfStringValue `tfsdk:"name" json:"name"` - Region ovhtypes.TfStringValue `tfsdk:"region" json:"region"` - StorageClass ovhtypes.TfStringValue `tfsdk:"storage_class" json:"storageClass"` - state attr.ValueState + Name ovhtypes.TfStringValue `tfsdk:"name" json:"name"` + Region ovhtypes.TfStringValue `tfsdk:"region" json:"region"` + StorageClass ovhtypes.TfStringValue `tfsdk:"storage_class" json:"storageClass"` + RemoveOnMainBucketDeletion ovhtypes.TfBoolValue `tfsdk:"remove_on_main_bucket_deletion" json:"-"` + state attr.ValueState } type ReplicationRulesDestinationWritableValue struct { @@ -3150,6 +3194,7 @@ func (v *ReplicationRulesDestinationValue) UnmarshalJSON(data []byte) error { v.Name = tmp.Name v.Region = tmp.Region v.StorageClass = tmp.StorageClass + v.RemoveOnMainBucketDeletion = tmp.RemoveOnMainBucketDeletion v.state = attr.ValueStateKnown @@ -3170,6 +3215,10 @@ func (v *ReplicationRulesDestinationValue) MergeWith(other *ReplicationRulesDest v.StorageClass = other.StorageClass } + if (v.RemoveOnMainBucketDeletion.IsUnknown() || v.RemoveOnMainBucketDeletion.IsNull()) && !other.RemoveOnMainBucketDeletion.IsUnknown() { + v.RemoveOnMainBucketDeletion = other.RemoveOnMainBucketDeletion + } + if (v.state == attr.ValueStateUnknown || v.state == attr.ValueStateNull) && other.state != attr.ValueStateUnknown { v.state = other.state } @@ -3177,13 +3226,14 @@ func (v *ReplicationRulesDestinationValue) MergeWith(other *ReplicationRulesDest func (v ReplicationRulesDestinationValue) Attributes() map[string]attr.Value { return map[string]attr.Value{ - "name": v.Name, - "region": v.Region, - "storageClass": v.StorageClass, + "name": v.Name, + "region": v.Region, + "storage_class": v.StorageClass, + "remove_on_main_bucket_deletion": v.RemoveOnMainBucketDeletion, } } func (v ReplicationRulesDestinationValue) ToTerraformValue(ctx context.Context) (tftypes.Value, error) { - attrTypes := make(map[string]tftypes.Type, 3) + attrTypes := make(map[string]tftypes.Type, 4) var val tftypes.Value var err error @@ -3191,12 +3241,13 @@ func (v ReplicationRulesDestinationValue) ToTerraformValue(ctx context.Context) attrTypes["name"] = basetypes.StringType{}.TerraformType(ctx) attrTypes["region"] = basetypes.StringType{}.TerraformType(ctx) attrTypes["storage_class"] = basetypes.StringType{}.TerraformType(ctx) + attrTypes["remove_on_main_bucket_deletion"] = basetypes.BoolType{}.TerraformType(ctx) objectType := tftypes.Object{AttributeTypes: attrTypes} switch v.state { case attr.ValueStateKnown: - vals := make(map[string]tftypes.Value, 3) + vals := make(map[string]tftypes.Value, 4) val, err = v.Name.ToTerraformValue(ctx) @@ -3222,10 +3273,17 @@ func (v ReplicationRulesDestinationValue) ToTerraformValue(ctx context.Context) vals["storage_class"] = val - if err := tftypes.ValidateValue(objectType, vals); err != nil { + val, err = v.RemoveOnMainBucketDeletion.ToTerraformValue(ctx) + + if err != nil { return tftypes.NewValue(objectType, tftypes.UnknownValue), err } + vals["remove_on_main_bucket_deletion"] = val + + if err := tftypes.ValidateValue(objectType, vals); err != nil { + return tftypes.NewValue(objectType, tftypes.UnknownValue), err + } return tftypes.NewValue(objectType, vals), nil case attr.ValueStateNull: return tftypes.NewValue(objectType, nil), nil @@ -3253,14 +3311,16 @@ func (v ReplicationRulesDestinationValue) ToObjectValue(ctx context.Context) (ba objVal, diags := types.ObjectValue( map[string]attr.Type{ - "name": ovhtypes.TfStringType{}, - "region": ovhtypes.TfStringType{}, - "storage_class": ovhtypes.TfStringType{}, + "name": ovhtypes.TfStringType{}, + "region": ovhtypes.TfStringType{}, + "storage_class": ovhtypes.TfStringType{}, + "remove_on_main_bucket_deletion": ovhtypes.TfBoolType{}, }, map[string]attr.Value{ - "name": v.Name, - "region": v.Region, - "storage_class": v.StorageClass, + "name": v.Name, + "region": v.Region, + "storage_class": v.StorageClass, + "remove_on_main_bucket_deletion": v.RemoveOnMainBucketDeletion, }) return objVal, diags @@ -3293,6 +3353,10 @@ func (v ReplicationRulesDestinationValue) Equal(o attr.Value) bool { return false } + if !v.RemoveOnMainBucketDeletion.Equal(other.RemoveOnMainBucketDeletion) { + return false + } + return true } @@ -3306,9 +3370,10 @@ func (v ReplicationRulesDestinationValue) Type(ctx context.Context) attr.Type { func (v ReplicationRulesDestinationValue) AttributeTypes(ctx context.Context) map[string]attr.Type { return map[string]attr.Type{ - "name": ovhtypes.TfStringType{}, - "region": ovhtypes.TfStringType{}, - "storage_class": ovhtypes.TfStringType{}, + "name": ovhtypes.TfStringType{}, + "region": ovhtypes.TfStringType{}, + "storage_class": ovhtypes.TfStringType{}, + "remove_on_main_bucket_deletion": ovhtypes.TfBoolType{}, } } diff --git a/ovh/resource_cloud_project_storage_test.go b/ovh/resource_cloud_project_storage_test.go index 914734f74..80f6b1c5d 100644 --- a/ovh/resource_cloud_project_storage_test.go +++ b/ovh/resource_cloud_project_storage_test.go @@ -76,6 +76,7 @@ func TestAccCloudProjectRegionStorage_withReplication(t *testing.T) { destination = { name = "%s" region = "GRA" + remove_on_main_bucket_deletion = true } filter = { "prefix" = "test" @@ -123,6 +124,7 @@ func TestAccCloudProjectRegionStorage_withReplication(t *testing.T) { destination = { name = "%s" region = "GRA" + remove_on_main_bucket_deletion = true } filter = { "prefix" = "test-updated" @@ -156,7 +158,9 @@ func TestAccCloudProjectRegionStorage_withReplication(t *testing.T) { ImportStateVerifyIdentifierAttribute: "name", ResourceName: "ovh_cloud_project_storage.storage", ImportStateId: fmt.Sprintf("%s/GRA/%s", os.Getenv("OVH_CLOUD_PROJECT_SERVICE_TEST"), bucketName), - ImportStateVerifyIgnore: []string{"created_at"}, // Ignore created_at since its value is invalid in response of the POST. + // Ignore created_at since its value is invalid in response of the POST. + // Also ignore remove_on_main_bucket_deletion since its computed value is not returned by the API. + ImportStateVerifyIgnore: []string{"created_at", "replication.rules.0.destination.remove_on_main_bucket_deletion"}, }, }, }) diff --git a/templates/resources/cloud_project_storage.md.tmpl b/templates/resources/cloud_project_storage.md.tmpl index 704231593..025f89c2c 100644 --- a/templates/resources/cloud_project_storage.md.tmpl +++ b/templates/resources/cloud_project_storage.md.tmpl @@ -88,6 +88,7 @@ Required: Optional: - `storage_class` (String) Destination storage class +- `remove_on_main_bucket_deletion` (Boolean) Whether to remove replicated bucket when the main bucket is deleted (make sure to apply your configuration when changing this value before deleting the main bucket)