mirror of
https://github.com/kopia/kopia.git
synced 2026-10-22 14:15:03 -04:00
feat(repository): Add support for Azure DeleteBlob operations where immutability protection is on (#3394)
When immutability is enabled on an Azure Blob storage account and/or container, a blob may be protected by a policy that causes the deletion of the blob to fail. This causes various kopia operations to fail, including the deletion of session marker blobs and the deletion of blobs as part of kopia "maintenance" operations. This changes allows creating an Azure-blob-native deletion marker when the blob is protected on containers where **version-level immutability** is enabled. This approach will likely NOT work with the many other flavors of immutability supported by Azure Blob. That is, the approach is ONLY expected to work with storage containers with "version-level immutability" enabled.
This commit is contained in:
1 parent
1ea14a04e6
commit
174f6141e1
1 file changed
+105
-25
@@ -4,20 +4,24 @@ package azure
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
|
||||
|
||||
azblobblob "github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/blob"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/bloberror"
|
||||
azblockblob "github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/blockblob"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/kopia/kopia/internal/clock"
|
||||
"github.com/kopia/kopia/internal/gather"
|
||||
"github.com/kopia/kopia/internal/iocopy"
|
||||
"github.com/kopia/kopia/internal/timestampmeta"
|
||||
"github.com/kopia/kopia/repo/blob"
|
||||
"github.com/kopia/kopia/repo/blob/retrying"
|
||||
"github.com/kopia/kopia/repo/logging"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -122,31 +126,9 @@ func (az *azStorage) PutBlob(ctx context.Context, b blob.ID, data blob.Bytes, op
|
||||
return errors.Wrap(blob.ErrUnsupportedPutBlobOption, "do-not-recreate")
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
_, err := az.putBlob(ctx, b, data, opts)
|
||||
|
||||
tsMetadata := timestampmeta.ToMap(opts.SetModTime, timeMapKey)
|
||||
|
||||
metadata := make(map[string]*string, len(tsMetadata))
|
||||
|
||||
for k, v := range tsMetadata {
|
||||
metadata[k] = to.Ptr(v)
|
||||
}
|
||||
|
||||
uso := &azblob.UploadStreamOptions{
|
||||
Metadata: metadata,
|
||||
}
|
||||
|
||||
resp, err := az.service.UploadStream(ctx, az.container, az.getObjectNameString(b), data.Reader(), uso)
|
||||
if err != nil {
|
||||
return translateError(err)
|
||||
}
|
||||
|
||||
if opts.GetModTime != nil {
|
||||
*opts.GetModTime = *resp.LastModified
|
||||
}
|
||||
|
||||
return nil
|
||||
return err
|
||||
}
|
||||
|
||||
// DeleteBlob deletes azure blob from container with given ID.
|
||||
@@ -159,6 +141,13 @@ func (az *azStorage) DeleteBlob(ctx context.Context, b blob.ID) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
var re *azcore.ResponseError
|
||||
|
||||
if errors.As(err, &re) && re.ErrorCode == string(bloberror.BlobImmutableDueToPolicy) {
|
||||
// if a policy prevents the deletion then try to create a delete marker version & delete that instead.
|
||||
return az.retryDeleteBlob(ctx, b)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -226,6 +215,97 @@ func (az *azStorage) DisplayName() string {
|
||||
return fmt.Sprintf("Azure: %v", az.Options.Container)
|
||||
}
|
||||
|
||||
func (az *azStorage) putBlob(ctx context.Context, b blob.ID, data blob.Bytes, opts blob.PutOptions) (azblockblob.UploadResponse, error) {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
tsMetadata := timestampmeta.ToMap(opts.SetModTime, timeMapKey)
|
||||
|
||||
metadata := make(map[string]*string, len(tsMetadata))
|
||||
|
||||
for k, v := range tsMetadata {
|
||||
metadata[k] = to.Ptr(v)
|
||||
}
|
||||
|
||||
uo := &azblockblob.UploadOptions{
|
||||
Metadata: metadata,
|
||||
}
|
||||
|
||||
if opts.HasRetentionOptions() {
|
||||
mode := azblobblob.ImmutabilityPolicySetting(opts.RetentionMode)
|
||||
retainUntilDate := clock.Now().Add(opts.RetentionPeriod).UTC()
|
||||
uo.ImmutabilityPolicyMode = &mode
|
||||
uo.ImmutabilityPolicyExpiryTime = &retainUntilDate
|
||||
}
|
||||
|
||||
resp, err := az.service.ServiceClient().
|
||||
NewContainerClient(az.container).
|
||||
NewBlockBlobClient(az.getObjectNameString(b)).
|
||||
Upload(ctx, data.Reader(), uo)
|
||||
if err != nil {
|
||||
return resp, translateError(err)
|
||||
}
|
||||
|
||||
if opts.GetModTime != nil {
|
||||
*opts.GetModTime = *resp.LastModified
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// retryDeleteBlob creates a delete marker version which is set to an unlocked protective state.
|
||||
// This protection is then removed and the main blob is deleted. Finally, the delete marker version is also deleted.
|
||||
// The original blob version protected by the policy is still protected from permanent deletion until the period has passed.
|
||||
func (az *azStorage) retryDeleteBlob(ctx context.Context, b blob.ID) error {
|
||||
blobName := az.getObjectNameString(b)
|
||||
|
||||
resp, err := az.putBlob(ctx, b, gather.FromSlice([]byte(nil)), blob.PutOptions{
|
||||
RetentionMode: blob.RetentionMode(azblobblob.ImmutabilityPolicySettingUnlocked),
|
||||
RetentionPeriod: time.Minute,
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to put blob version needed to create delete marker")
|
||||
}
|
||||
|
||||
_, err = az.service.ServiceClient().
|
||||
NewContainerClient(az.container).
|
||||
NewBlobClient(blobName).
|
||||
DeleteImmutabilityPolicy(ctx, nil)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to create delete marker for immutable blob")
|
||||
}
|
||||
|
||||
_, err = az.service.DeleteBlob(ctx, az.container, blobName, nil)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to soft delete blob")
|
||||
}
|
||||
|
||||
log := logging.Module("azure-immutability")
|
||||
|
||||
if resp.VersionID == nil || *resp.VersionID == "" {
|
||||
// shouldn't happen
|
||||
log(ctx).Info("VersionID not returned, exiting without deleting the delete marker version")
|
||||
return nil
|
||||
}
|
||||
|
||||
bc, err := az.service.ServiceClient().
|
||||
NewContainerClient(az.container).
|
||||
NewBlobClient(blobName).
|
||||
WithVersionID(*resp.VersionID)
|
||||
if err != nil {
|
||||
log(ctx).Infof("Issue preparing versioned blob client: %v", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err = bc.Delete(ctx, nil)
|
||||
if err != nil {
|
||||
log(ctx).Infof("Issue deleting blob delete marker: %v", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// New creates new Azure Blob Storage-backed storage with specified options:
|
||||
//
|
||||
// - the 'Container', 'StorageAccount' and 'StorageKey' fields are required and all other parameters are optional.
|
||||
|
||||
Reference in new issue
Block a user