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  }