github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/storage/cloud/azure_storage.go (about) 1 // Copyright 2019 The Cockroach Authors. 2 // 3 // Use of this software is governed by the Business Source License 4 // included in the file licenses/BSL.txt. 5 // 6 // As of the Change Date specified in that file, in accordance with 7 // the Business Source License, use of this software will be governed 8 // by the Apache License, Version 2.0, included in the file 9 // licenses/APL.txt. 10 11 package cloud 12 13 import ( 14 "context" 15 "fmt" 16 "io" 17 "net/url" 18 "path" 19 "strings" 20 21 "github.com/Azure/azure-storage-blob-go/azblob" 22 "github.com/cockroachdb/cockroach/pkg/roachpb" 23 "github.com/cockroachdb/cockroach/pkg/settings/cluster" 24 "github.com/cockroachdb/cockroach/pkg/util/contextutil" 25 "github.com/cockroachdb/errors" 26 ) 27 28 func azureQueryParams(conf *roachpb.ExternalStorage_Azure) string { 29 q := make(url.Values) 30 if conf.AccountName != "" { 31 q.Set(AzureAccountNameParam, conf.AccountName) 32 } 33 if conf.AccountKey != "" { 34 q.Set(AzureAccountKeyParam, conf.AccountKey) 35 } 36 return q.Encode() 37 } 38 39 type azureStorage struct { 40 conf *roachpb.ExternalStorage_Azure 41 container azblob.ContainerURL 42 prefix string 43 settings *cluster.Settings 44 } 45 46 var _ ExternalStorage = &azureStorage{} 47 48 func makeAzureStorage( 49 conf *roachpb.ExternalStorage_Azure, settings *cluster.Settings, 50 ) (ExternalStorage, error) { 51 if conf == nil { 52 return nil, errors.Errorf("azure upload requested but info missing") 53 } 54 credential, err := azblob.NewSharedKeyCredential(conf.AccountName, conf.AccountKey) 55 if err != nil { 56 return nil, errors.Wrap(err, "azure credential") 57 } 58 p := azblob.NewPipeline(credential, azblob.PipelineOptions{}) 59 u, err := url.Parse(fmt.Sprintf("https://%s.blob.core.windows.net", conf.AccountName)) 60 if err != nil { 61 return nil, errors.Wrap(err, "azure: account name is not valid") 62 } 63 serviceURL := azblob.NewServiceURL(*u, p) 64 return &azureStorage{ 65 conf: conf, 66 container: serviceURL.NewContainerURL(conf.Container), 67 prefix: conf.Prefix, 68 settings: settings, 69 }, nil 70 } 71 72 func (s *azureStorage) getBlob(basename string) azblob.BlockBlobURL { 73 name := path.Join(s.prefix, basename) 74 return s.container.NewBlockBlobURL(name) 75 } 76 77 func (s *azureStorage) Conf() roachpb.ExternalStorage { 78 return roachpb.ExternalStorage{ 79 Provider: roachpb.ExternalStorageProvider_Azure, 80 AzureConfig: s.conf, 81 } 82 } 83 84 func (s *azureStorage) WriteFile( 85 ctx context.Context, basename string, content io.ReadSeeker, 86 ) error { 87 err := contextutil.RunWithTimeout(ctx, "write azure file", timeoutSetting.Get(&s.settings.SV), 88 func(ctx context.Context) error { 89 blob := s.getBlob(basename) 90 _, err := blob.Upload( 91 ctx, content, azblob.BlobHTTPHeaders{}, azblob.Metadata{}, azblob.BlobAccessConditions{}, 92 ) 93 return err 94 }) 95 return errors.Wrapf(err, "write file: %s", basename) 96 } 97 98 func (s *azureStorage) ReadFile(ctx context.Context, basename string) (io.ReadCloser, error) { 99 // https://github.com/cockroachdb/cockroach/issues/23859 100 blob := s.getBlob(basename) 101 get, err := blob.Download(ctx, 0, 0, azblob.BlobAccessConditions{}, false) 102 if err != nil { 103 return nil, errors.Wrap(err, "failed to create azure reader") 104 } 105 reader := get.Body(azblob.RetryReaderOptions{MaxRetryRequests: 3}) 106 return reader, nil 107 } 108 109 func (s *azureStorage) ListFiles(ctx context.Context, patternSuffix string) ([]string, error) { 110 pattern := s.prefix 111 if patternSuffix != "" { 112 if containsGlob(s.prefix) { 113 return nil, errors.New("prefix cannot contain globs pattern when passing an explicit pattern") 114 } 115 pattern = path.Join(pattern, patternSuffix) 116 } 117 var fileList []string 118 response, err := s.container.ListBlobsFlatSegment(ctx, 119 azblob.Marker{}, 120 azblob.ListBlobsSegmentOptions{Prefix: getPrefixBeforeWildcard(s.prefix)}, 121 ) 122 if err != nil { 123 return nil, errors.Wrap(err, "unable to list files for specified blob") 124 } 125 126 for _, blob := range response.Segment.BlobItems { 127 matches, err := path.Match(pattern, blob.Name) 128 if err != nil { 129 continue 130 } 131 if matches { 132 azureURL := url.URL{ 133 Scheme: "azure", 134 Host: strings.TrimPrefix(s.container.URL().Path, "/"), 135 Path: blob.Name, 136 RawQuery: azureQueryParams(s.conf), 137 } 138 fileList = append(fileList, azureURL.String()) 139 } 140 } 141 142 return fileList, nil 143 } 144 145 func (s *azureStorage) Delete(ctx context.Context, basename string) error { 146 err := contextutil.RunWithTimeout(ctx, "delete azure file", timeoutSetting.Get(&s.settings.SV), 147 func(ctx context.Context) error { 148 blob := s.getBlob(basename) 149 _, err := blob.Delete(ctx, azblob.DeleteSnapshotsOptionNone, azblob.BlobAccessConditions{}) 150 return err 151 }) 152 return errors.Wrap(err, "delete file") 153 } 154 155 func (s *azureStorage) Size(ctx context.Context, basename string) (int64, error) { 156 var props *azblob.BlobGetPropertiesResponse 157 err := contextutil.RunWithTimeout(ctx, "size azure file", timeoutSetting.Get(&s.settings.SV), 158 func(ctx context.Context) error { 159 blob := s.getBlob(basename) 160 var err error 161 props, err = blob.GetProperties(ctx, azblob.BlobAccessConditions{}) 162 return err 163 }) 164 if err != nil { 165 return 0, errors.Wrap(err, "get file properties") 166 } 167 return props.ContentLength(), nil 168 } 169 170 func (s *azureStorage) Close() error { 171 return nil 172 }