registry/storage/driver/azure: fix Move method

Something seems broken on azure/azure sdk side - it is currently not
possible to copy a blob of type AppendBlob using `CopyFromURL`.
Using the AppendBlob client via NewAppendBlobClient does not work
either.

According to Azure the correct way to do this is by using
StartCopyFromURL. Because this is an async operation, we need to do
polling ourselves. A simple backoff mechanism is used, where during each
iteration, the configured delay is multiplied by the retry number.

Also introduces two new config options for the Azure driver:
copy_status_poll_max_retry, and copy_status_poll_delay.

Signed-off-by: Flavian Missi <fmissi@redhat.com>
This commit is contained in:
Flavian Missi 2023-06-01 16:19:34 +02:00
parent ba46c769b3
commit 2b72c4d1ca
5 changed files with 149 additions and 29 deletions

View file

@ -103,6 +103,8 @@ storage:
clientid: client_id_string clientid: client_id_string
tenantid: tenant_id_string tenantid: tenant_id_string
secret: secret_string secret: secret_string
copy_status_poll_max_retry: 10
copy_status_poll_delay: 100ms
gcs: gcs:
bucket: bucketname bucket: bucketname
keyfile: /path/to/keyfile keyfile: /path/to/keyfile

View file

@ -9,11 +9,13 @@ An implementation of the `storagedriver.StorageDriver` interface which uses [Mic
## Parameters ## Parameters
| Parameter | Required | Description | | Parameter | Required | Description |
|:--------------|:---------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| |:-----------------------------------|:---------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `accountname` | yes | Name of the Azure Storage Account. | | `accountname` | yes | Name of the Azure Storage Account. |
| `accountkey` | yes | Primary or Secondary Key for the Storage Account. | | `accountkey` | yes | Primary or Secondary Key for the Storage Account. |
| `container` | yes | Name of the Azure root storage container in which all registry data is stored. Must comply the storage container name [requirements](https://docs.microsoft.com/rest/api/storageservices/fileservices/naming-and-referencing-containers--blobs--and-metadata). For example, if your url is `https://myaccount.blob.core.windows.net/myblob` use the container value of `myblob`.| | `container` | yes | Name of the Azure root storage container in which all registry data is stored. Must comply the storage container name [requirements](https://docs.microsoft.com/rest/api/storageservices/fileservices/naming-and-referencing-containers--blobs--and-metadata). For example, if your url is `https://myaccount.blob.core.windows.net/myblob` use the container value of `myblob`.|
| `realm` | no | Domain name suffix for the Storage Service API endpoint. For example realm for "Azure in China" would be `core.chinacloudapi.cn` and realm for "Azure Government" would be `core.usgovcloudapi.net`. By default, this is `core.windows.net`. | | `realm` | no | Domain name suffix for the Storage Service API endpoint. For example realm for "Azure in China" would be `core.chinacloudapi.cn` and realm for "Azure Government" would be `core.usgovcloudapi.net`. By default, this is `core.windows.net`. |
| `copy_status_poll_max_retry` | no | Max retry number for polling of copy operation status. Retries use a simple backoff algorithm where each retry number is multiplied by `copy_status_poll_delay`, and this number is used as the delay. Set to -1 to disable retries and abort if the copy does not complete immediately. Defaults to 5. |
| `copy_status_poll_delay` | no | Time to wait between retries for polling of copy operation status. This time is multiplied by N on each retry, where N is the retry number. Defaults to 100ms |
## Related information ## Related information

View file

@ -21,9 +21,8 @@ import (
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/container" "github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/container"
) )
const driverName = "azure"
const ( const (
driverName = "azure"
maxChunkSize = 4 * 1024 * 1024 maxChunkSize = 4 * 1024 * 1024
) )
@ -31,6 +30,8 @@ type driver struct {
azClient *azureClient azClient *azureClient
client *container.Client client *container.Client
rootDirectory string rootDirectory string
copyStatusPollMaxRetry int
copyStatusPollDelay time.Duration
} }
type baseEmbed struct{ base.Base } type baseEmbed struct{ base.Base }
@ -59,11 +60,19 @@ func New(params *Parameters) (*Driver, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
copyStatusPollDelay, err := time.ParseDuration(params.CopyStatusPollDelay)
if err != nil {
return nil, err
}
client := azClient.ContainerClient() client := azClient.ContainerClient()
d := &driver{ d := &driver{
azClient: azClient, azClient: azClient,
client: client, client: client,
rootDirectory: params.RootDirectory, rootDirectory: params.RootDirectory,
copyStatusPollMaxRetry: params.CopyStatusPollMaxRetry,
copyStatusPollDelay: copyStatusPollDelay,
} }
return &Driver{baseEmbed: baseEmbed{Base: base.Base{StorageDriver: d}}}, nil return &Driver{baseEmbed: baseEmbed{Base: base.Base{StorageDriver: d}}}, nil
} }
@ -282,7 +291,7 @@ func (d *driver) Move(ctx context.Context, sourcePath string, destPath string) e
return err return err
} }
destBlobRef := d.client.NewBlockBlobClient(d.blobName(destPath)) destBlobRef := d.client.NewBlockBlobClient(d.blobName(destPath))
_, err = destBlobRef.CopyFromURL(ctx, sourceBlobURL, nil) resp, err := destBlobRef.StartCopyFromURL(ctx, sourceBlobURL, nil)
if err != nil { if err != nil {
if is404(err) { if is404(err) {
return storagedriver.PathNotFoundError{Path: sourcePath} return storagedriver.PathNotFoundError{Path: sourcePath}
@ -290,6 +299,39 @@ func (d *driver) Move(ctx context.Context, sourcePath string, destPath string) e
return err return err
} }
copyStatus := *resp.CopyStatus
if d.copyStatusPollMaxRetry == -1 && copyStatus == blob.CopyStatusTypePending {
destBlobRef.AbortCopyFromURL(ctx, *resp.CopyID, nil)
return nil
}
retryCount := 1
for copyStatus == blob.CopyStatusTypePending {
props, err := destBlobRef.GetProperties(ctx, nil)
if err != nil {
return err
}
if retryCount >= d.copyStatusPollMaxRetry {
destBlobRef.AbortCopyFromURL(ctx, *props.CopyID, nil)
return fmt.Errorf("max retries for copy polling reached, aborting copy")
}
copyStatus = *props.CopyStatus
if copyStatus == blob.CopyStatusTypeAborted || copyStatus == blob.CopyStatusTypeFailed {
if props.CopyStatusDescription != nil {
return fmt.Errorf("failed to move blob: %s", *props.CopyStatusDescription)
}
return fmt.Errorf("failed to move blob with copy id %s", *props.CopyID)
}
if copyStatus == blob.CopyStatusTypePending {
time.Sleep(d.copyStatusPollDelay * time.Duration(retryCount))
}
retryCount++
}
_, err = d.client.NewBlobClient(d.blobName(sourcePath)).Delete(ctx, nil) _, err = d.client.NewBlobClient(d.blobName(sourcePath)).Delete(ctx, nil)
return err return err
} }

View file

@ -1,7 +1,9 @@
package azure package azure
import ( import (
"context"
"fmt" "fmt"
"math/rand"
"os" "os"
"strings" "strings"
"testing" "testing"
@ -19,6 +21,8 @@ const (
envRootDirectory = "AZURE_ROOT_DIRECTORY" envRootDirectory = "AZURE_ROOT_DIRECTORY"
) )
var azureDriverConstructor func() (storagedriver.StorageDriver, error)
// Hook up gocheck into the "go test" runner. // Hook up gocheck into the "go test" runner.
func Test(t *testing.T) { check.TestingT(t) } func Test(t *testing.T) { check.TestingT(t) }
@ -36,10 +40,10 @@ func init() {
value *string value *string
missingOk bool missingOk bool
}{ }{
{envAccountName, &accountName, false}, {envAccountName, &accountName, true},
{envAccountKey, &accountKey, false}, {envAccountKey, &accountKey, true},
{envContainer, &container, false}, {envContainer, &container, true},
{envRealm, &realm, false}, {envRealm, &realm, true},
{envRootDirectory, &rootDirectory, true}, {envRootDirectory, &rootDirectory, true},
} }
@ -51,7 +55,7 @@ func init() {
} }
} }
azureDriverConstructor := func() (storagedriver.StorageDriver, error) { azureDriverConstructor = func() (storagedriver.StorageDriver, error) {
parameters := map[string]interface{}{ parameters := map[string]interface{}{
"container": container, "container": container,
"accountname": accountName, "accountname": accountName,
@ -77,6 +81,66 @@ func init() {
testsuites.RegisterSuite(azureDriverConstructor, skipCheck) testsuites.RegisterSuite(azureDriverConstructor, skipCheck)
} }
var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
func randStringRunes(n int) string {
b := make([]rune, n)
for i := range b {
b[i] = letterRunes[rand.Intn(len(letterRunes))]
}
return string(b)
}
func TestCommitAfterMove(t *testing.T) {
driver, err := azureDriverConstructor()
if err != nil {
t.Fatalf("unexpected error creating azure driver: %v", err)
}
contents := randStringRunes(4 * 1024 * 1024)
sourcePath := "/source/file"
destPath := "/dest/file"
ctx := context.Background()
defer driver.Delete(ctx, sourcePath)
defer driver.Delete(ctx, destPath)
writer, err := driver.Writer(ctx, sourcePath, false)
if err != nil {
t.Fatalf("unexpected error from driver.Writer: %v", err)
}
_, err = writer.Write([]byte(contents))
if err != nil {
t.Fatalf("writer.Write: unexpected error: %v", err)
}
err = writer.Commit()
if err != nil {
t.Fatalf("writer.Commit: unexpected error: %v", err)
}
err = writer.Close()
if err != nil {
t.Fatalf("writer.Close: unexpected error: %v", err)
}
_, err = driver.GetContent(ctx, sourcePath)
if err != nil {
t.Fatalf("driver.GetContent(sourcePath): unexpected error: %v", err)
}
err = driver.Move(ctx, sourcePath, destPath)
if err != nil {
t.Fatalf("driver.Move: unexpected error: %v", err)
}
_, err = driver.GetContent(ctx, destPath)
if err != nil {
t.Fatalf("GetContent(destPath): unexpected error: %v", err)
}
}
func TestParamParsing(t *testing.T) { func TestParamParsing(t *testing.T) {
expectErrors := []map[string]interface{}{ expectErrors := []map[string]interface{}{
{}, {},

View file

@ -9,6 +9,8 @@ import (
const ( const (
defaultRealm = "core.windows.net" defaultRealm = "core.windows.net"
defaultCopyStatusPollMaxRetry = 5
defaultCopyStatusPollDelay = "100ms"
) )
type Credentials struct { type Credentials struct {
@ -27,6 +29,8 @@ type Parameters struct {
Realm string `mapstructure:"realm"` Realm string `mapstructure:"realm"`
RootDirectory string `mapstructure:"rootdirectory"` RootDirectory string `mapstructure:"rootdirectory"`
ServiceURL string `mapstructure:"serviceurl"` ServiceURL string `mapstructure:"serviceurl"`
CopyStatusPollMaxRetry int `mapstructure:"copy_status_poll_max_retry"`
CopyStatusPollDelay string `mapstructure:"copy_status_poll_delay"`
} }
func NewParameters(parameters map[string]interface{}) (*Parameters, error) { func NewParameters(parameters map[string]interface{}) (*Parameters, error) {
@ -45,5 +49,11 @@ func NewParameters(parameters map[string]interface{}) (*Parameters, error) {
if params.ServiceURL == "" { if params.ServiceURL == "" {
params.ServiceURL = fmt.Sprintf("https://%s.blob.%s", params.AccountName, params.Realm) params.ServiceURL = fmt.Sprintf("https://%s.blob.%s", params.AccountName, params.Realm)
} }
if params.CopyStatusPollMaxRetry == 0 {
params.CopyStatusPollMaxRetry = defaultCopyStatusPollMaxRetry
}
if params.CopyStatusPollDelay == "" {
params.CopyStatusPollDelay = defaultCopyStatusPollDelay
}
return &params, nil return &params, nil
} }