github.com/minio/minio@v0.0.0-20240328213742-3f72439b8a27/cmd/warm-backend-azure.go (about) 1 // Copyright (c) 2015-2021 MinIO, Inc. 2 // 3 // This file is part of MinIO Object Storage stack 4 // 5 // This program is free software: you can redistribute it and/or modify 6 // it under the terms of the GNU Affero General Public License as published by 7 // the Free Software Foundation, either version 3 of the License, or 8 // (at your option) any later version. 9 // 10 // This program is distributed in the hope that it will be useful 11 // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 // GNU Affero General Public License for more details. 14 // 15 // You should have received a copy of the GNU Affero General Public License 16 // along with this program. If not, see <http://www.gnu.org/licenses/>. 17 18 package cmd 19 20 import ( 21 "context" 22 "encoding/base64" 23 "errors" 24 "fmt" 25 "io" 26 "net/http" 27 "net/url" 28 "strings" 29 "time" 30 31 "github.com/Azure/azure-storage-blob-go/azblob" 32 "github.com/Azure/go-autorest/autorest/adal" 33 "github.com/Azure/go-autorest/autorest/azure" 34 "github.com/minio/madmin-go/v3" 35 ) 36 37 type warmBackendAzure struct { 38 serviceURL azblob.ServiceURL 39 Bucket string 40 Prefix string 41 StorageClass string 42 } 43 44 func (az *warmBackendAzure) getDest(object string) string { 45 destObj := object 46 if az.Prefix != "" { 47 destObj = fmt.Sprintf("%s/%s", az.Prefix, object) 48 } 49 return destObj 50 } 51 52 func (az *warmBackendAzure) tier() azblob.AccessTierType { 53 for _, t := range azblob.PossibleAccessTierTypeValues() { 54 if strings.EqualFold(az.StorageClass, string(t)) { 55 return t 56 } 57 } 58 return azblob.AccessTierType("") 59 } 60 61 // FIXME: add support for remote version ID in Azure remote tier and remove 62 // this. Currently it's a no-op. 63 64 func (az *warmBackendAzure) Put(ctx context.Context, object string, r io.Reader, length int64) (remoteVersionID, error) { 65 blobURL := az.serviceURL.NewContainerURL(az.Bucket).NewBlockBlobURL(az.getDest(object)) 66 // set tier if specified - 67 if az.StorageClass != "" { 68 if _, err := blobURL.SetTier(ctx, az.tier(), azblob.LeaseAccessConditions{}, azblob.RehydratePriorityStandard); err != nil { 69 return "", azureToObjectError(err, az.Bucket, object) 70 } 71 } 72 res, err := azblob.UploadStreamToBlockBlob(ctx, r, blobURL, azblob.UploadStreamToBlockBlobOptions{}) 73 if err != nil { 74 return "", azureToObjectError(err, az.Bucket, object) 75 } 76 return remoteVersionID(res.Version()), nil 77 } 78 79 func (az *warmBackendAzure) Get(ctx context.Context, object string, rv remoteVersionID, opts WarmBackendGetOpts) (r io.ReadCloser, err error) { 80 if opts.startOffset < 0 { 81 return nil, InvalidRange{} 82 } 83 blobURL := az.serviceURL.NewContainerURL(az.Bucket).NewBlobURL(az.getDest(object)) 84 blob, err := blobURL.Download(ctx, opts.startOffset, opts.length, azblob.BlobAccessConditions{}, false, azblob.ClientProvidedKeyOptions{}) 85 if err != nil { 86 return nil, azureToObjectError(err, az.Bucket, object) 87 } 88 89 rc := blob.Body(azblob.RetryReaderOptions{}) 90 return rc, nil 91 } 92 93 func (az *warmBackendAzure) Remove(ctx context.Context, object string, rv remoteVersionID) error { 94 blob := az.serviceURL.NewContainerURL(az.Bucket).NewBlobURL(az.getDest(object)) 95 _, err := blob.Delete(ctx, azblob.DeleteSnapshotsOptionNone, azblob.BlobAccessConditions{}) 96 return azureToObjectError(err, az.Bucket, object) 97 } 98 99 func (az *warmBackendAzure) InUse(ctx context.Context) (bool, error) { 100 containerURL := az.serviceURL.NewContainerURL(az.Bucket) 101 resp, err := containerURL.ListBlobsHierarchySegment(ctx, azblob.Marker{}, "/", azblob.ListBlobsSegmentOptions{ 102 Prefix: az.Prefix, 103 MaxResults: int32(1), 104 }) 105 if err != nil { 106 return false, azureToObjectError(err, az.Bucket, az.Prefix) 107 } 108 if len(resp.Segment.BlobPrefixes) > 0 || len(resp.Segment.BlobItems) > 0 { 109 return true, nil 110 } 111 return false, nil 112 } 113 114 func newCredentialFromSP(conf madmin.TierAzure) (azblob.Credential, error) { 115 oauthConfig, err := adal.NewOAuthConfig(azure.PublicCloud.ActiveDirectoryEndpoint, conf.SPAuth.TenantID) 116 if err != nil { 117 return nil, err 118 } 119 spt, err := adal.NewServicePrincipalToken(*oauthConfig, conf.SPAuth.ClientID, conf.SPAuth.ClientSecret, azure.PublicCloud.ResourceIdentifiers.Storage) 120 if err != nil { 121 return nil, err 122 } 123 124 // Refresh obtains a fresh token 125 err = spt.Refresh() 126 if err != nil { 127 return nil, err 128 } 129 130 tc := azblob.NewTokenCredential(spt.Token().AccessToken, func(tc azblob.TokenCredential) time.Duration { 131 err := spt.Refresh() 132 if err != nil { 133 return 0 134 } 135 // set the new token value 136 tc.SetToken(spt.Token().AccessToken) 137 138 // get the next token before the current one expires 139 nextRenewal := float64(time.Until(spt.Token().Expires())) * 0.8 140 if nextRenewal <= 0 { 141 nextRenewal = float64(time.Second) 142 } 143 144 return time.Duration(nextRenewal) 145 }) 146 147 return tc, nil 148 } 149 150 func newWarmBackendAzure(conf madmin.TierAzure, _ string) (*warmBackendAzure, error) { 151 var ( 152 credential azblob.Credential 153 err error 154 ) 155 156 switch { 157 case conf.AccountName == "": 158 return nil, errors.New("the account name is required") 159 case conf.AccountKey != "" && (conf.SPAuth.TenantID != "" || conf.SPAuth.ClientID != "" || conf.SPAuth.ClientSecret != ""): 160 return nil, errors.New("multiple authentication mechanisms are provided") 161 case conf.AccountKey == "" && (conf.SPAuth.TenantID == "" || conf.SPAuth.ClientID == "" || conf.SPAuth.ClientSecret == ""): 162 return nil, errors.New("no authentication mechanism was provided") 163 } 164 165 if conf.Bucket == "" { 166 return nil, errors.New("no bucket name was provided") 167 } 168 169 if conf.IsSPEnabled() { 170 credential, err = newCredentialFromSP(conf) 171 } else { 172 credential, err = azblob.NewSharedKeyCredential(conf.AccountName, conf.AccountKey) 173 } 174 if err != nil { 175 if _, ok := err.(base64.CorruptInputError); ok { 176 return nil, errors.New("invalid Azure credentials") 177 } 178 return nil, err 179 } 180 p := azblob.NewPipeline(credential, azblob.PipelineOptions{}) 181 var u *url.URL 182 if conf.Endpoint != "" { 183 u, err = url.Parse(conf.Endpoint) 184 if err != nil { 185 return nil, err 186 } 187 } else { 188 u, err = url.Parse(fmt.Sprintf("https://%s.blob.core.windows.net", conf.AccountName)) 189 if err != nil { 190 return nil, err 191 } 192 } 193 serviceURL := azblob.NewServiceURL(*u, p) 194 return &warmBackendAzure{ 195 serviceURL: serviceURL, 196 Bucket: conf.Bucket, 197 Prefix: strings.TrimSuffix(conf.Prefix, slashSeparator), 198 StorageClass: conf.StorageClass, 199 }, nil 200 } 201 202 // Convert azure errors to minio object layer errors. 203 func azureToObjectError(err error, params ...string) error { 204 if err == nil { 205 return nil 206 } 207 208 bucket := "" 209 object := "" 210 if len(params) >= 1 { 211 bucket = params[0] 212 } 213 if len(params) == 2 { 214 object = params[1] 215 } 216 217 azureErr, ok := err.(azblob.StorageError) 218 if !ok { 219 // We don't interpret non Azure errors. As azure errors will 220 // have StatusCode to help to convert to object errors. 221 return err 222 } 223 224 serviceCode := string(azureErr.ServiceCode()) 225 statusCode := azureErr.Response().StatusCode 226 227 return azureCodesToObjectError(err, serviceCode, statusCode, bucket, object) 228 } 229 230 func azureCodesToObjectError(err error, serviceCode string, statusCode int, bucket string, object string) error { 231 switch serviceCode { 232 case "ContainerNotFound", "ContainerBeingDeleted": 233 err = BucketNotFound{Bucket: bucket} 234 case "ContainerAlreadyExists": 235 err = BucketExists{Bucket: bucket} 236 case "InvalidResourceName": 237 err = BucketNameInvalid{Bucket: bucket} 238 case "RequestBodyTooLarge": 239 err = PartTooBig{} 240 case "InvalidMetadata": 241 err = UnsupportedMetadata{} 242 case "BlobAccessTierNotSupportedForAccountType": 243 err = NotImplemented{} 244 case "OutOfRangeInput": 245 err = ObjectNameInvalid{ 246 Bucket: bucket, 247 Object: object, 248 } 249 default: 250 switch statusCode { 251 case http.StatusNotFound: 252 if object != "" { 253 err = ObjectNotFound{ 254 Bucket: bucket, 255 Object: object, 256 } 257 } else { 258 err = BucketNotFound{Bucket: bucket} 259 } 260 case http.StatusBadRequest: 261 err = BucketNameInvalid{Bucket: bucket} 262 } 263 } 264 return err 265 }