github.com/percona/percona-xtradb-cluster-operator@v1.14.0/pkg/pxc/backup/storage/storage.go (about)

     1  package storage
     2  
     3  import (
     4  	"context"
     5  	"crypto/tls"
     6  	"fmt"
     7  	"io"
     8  	"net/http"
     9  	"path"
    10  	"strings"
    11  
    12  	"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
    13  	"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/bloberror"
    14  	"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/container"
    15  	"github.com/minio/minio-go/v7"
    16  	"github.com/minio/minio-go/v7/pkg/credentials"
    17  	"github.com/pkg/errors"
    18  
    19  	api "github.com/percona/percona-xtradb-cluster-operator/pkg/apis/pxc/v1"
    20  )
    21  
    22  var ErrObjectNotFound = errors.New("object not found")
    23  
    24  type Storage interface {
    25  	GetObject(ctx context.Context, objectName string) (io.ReadCloser, error)
    26  	PutObject(ctx context.Context, name string, data io.Reader, size int64) error
    27  	ListObjects(ctx context.Context, prefix string) ([]string, error)
    28  	DeleteObject(ctx context.Context, objectName string) error
    29  	SetPrefix(prefix string)
    30  	GetPrefix() string
    31  }
    32  
    33  type NewClientFunc func(context.Context, Options) (Storage, error)
    34  
    35  func NewClient(ctx context.Context, opts Options) (Storage, error) {
    36  	switch opts.Type() {
    37  	case api.BackupStorageS3:
    38  		opts, ok := opts.(*S3Options)
    39  		if !ok {
    40  			return nil, errors.New("invalid options type")
    41  		}
    42  		return NewS3(ctx, opts.Endpoint, opts.AccessKeyID, opts.SecretAccessKey, opts.BucketName, opts.Prefix, opts.Region, opts.VerifyTLS)
    43  	case api.BackupStorageAzure:
    44  		opts, ok := opts.(*AzureOptions)
    45  		if !ok {
    46  			return nil, errors.New("invalid options type")
    47  		}
    48  		return NewAzure(opts.StorageAccount, opts.AccessKey, opts.Endpoint, opts.Container, opts.Prefix)
    49  	}
    50  	return nil, errors.New("invalid storage type")
    51  }
    52  
    53  // S3 is a type for working with S3 storages
    54  type S3 struct {
    55  	client     *minio.Client // minio client for work with storage
    56  	bucketName string        // S3 bucket name where binlogs will be stored
    57  	prefix     string        // prefix for S3 requests
    58  }
    59  
    60  // NewS3 return new Manager, useSSL using ssl for connection with storage
    61  func NewS3(ctx context.Context, endpoint, accessKeyID, secretAccessKey, bucketName, prefix, region string, verifyTLS bool) (Storage, error) {
    62  	if endpoint == "" {
    63  		endpoint = "https://s3.amazonaws.com"
    64  		// We can't use default endpoint if region is not us-east-1
    65  		// More info: https://docs.aws.amazon.com/general/latest/gr/s3.html
    66  		if region != "" && region != "us-east-1" {
    67  			endpoint = fmt.Sprintf("https://s3.%s.amazonaws.com", region)
    68  		}
    69  	}
    70  	useSSL := strings.Contains(endpoint, "https")
    71  	endpoint = strings.TrimPrefix(strings.TrimPrefix(endpoint, "https://"), "http://")
    72  	transport := http.DefaultTransport
    73  	transport.(*http.Transport).TLSClientConfig = &tls.Config{
    74  		InsecureSkipVerify: !verifyTLS,
    75  	}
    76  	minioClient, err := minio.New(strings.TrimRight(endpoint, "/"), &minio.Options{
    77  		Creds:     credentials.NewStaticV4(accessKeyID, secretAccessKey, ""),
    78  		Secure:    useSSL,
    79  		Region:    region,
    80  		Transport: transport,
    81  	})
    82  	if err != nil {
    83  		return nil, errors.Wrap(err, "new minio client")
    84  	}
    85  
    86  	bucketExists, err := minioClient.BucketExists(ctx, bucketName)
    87  	if err != nil {
    88  		if merr, ok := err.(minio.ErrorResponse); ok && merr.Code == "301 Moved Permanently" {
    89  			return nil, errors.Errorf("%s region: %s bucket: %s", merr.Code, merr.Region, merr.BucketName)
    90  		}
    91  		return nil, errors.Wrap(err, "failed to check if bucket exists")
    92  	}
    93  	if !bucketExists {
    94  		return nil, errors.Errorf("bucket %s does not exist", bucketName)
    95  	}
    96  
    97  	return &S3{
    98  		client:     minioClient,
    99  		bucketName: bucketName,
   100  		prefix:     prefix,
   101  	}, nil
   102  }
   103  
   104  // GetObject return content by given object name
   105  func (s *S3) GetObject(ctx context.Context, objectName string) (io.ReadCloser, error) {
   106  	objPath := path.Join(s.prefix, objectName)
   107  	oldObj, err := s.client.GetObject(ctx, s.bucketName, objPath, minio.GetObjectOptions{})
   108  	if err != nil {
   109  		return nil, errors.Wrapf(err, "get object %s", objPath)
   110  	}
   111  
   112  	// minio client returns error only on Read() method, so we need to call it to see if object exists
   113  	_, err = oldObj.Read([]byte{})
   114  	if err != nil {
   115  		if minio.ToErrorResponse(errors.Cause(err)).Code == "NoSuchKey" {
   116  			return nil, ErrObjectNotFound
   117  		}
   118  		return nil, errors.Wrapf(err, "read object %s", objPath)
   119  	}
   120  
   121  	_, err = oldObj.Seek(0, 0)
   122  	if err != nil {
   123  		return nil, errors.Wrapf(err, "seek object %s", objPath)
   124  	}
   125  
   126  	return oldObj, nil
   127  }
   128  
   129  // PutObject puts new object to storage with given name and content
   130  func (s *S3) PutObject(ctx context.Context, name string, data io.Reader, size int64) error {
   131  	objPath := path.Join(s.prefix, name)
   132  	_, err := s.client.PutObject(ctx, s.bucketName, objPath, data, size, minio.PutObjectOptions{})
   133  	if err != nil {
   134  		return errors.Wrapf(err, "put object %s", objPath)
   135  	}
   136  
   137  	return nil
   138  }
   139  
   140  func (s *S3) ListObjects(ctx context.Context, prefix string) ([]string, error) {
   141  	opts := minio.ListObjectsOptions{
   142  		UseV1:     true,
   143  		Recursive: true,
   144  		Prefix:    s.prefix + prefix,
   145  	}
   146  	list := []string{}
   147  
   148  	var err error
   149  	for object := range s.client.ListObjects(ctx, s.bucketName, opts) {
   150  		// From `(c *Client) ListObjects` method docs:
   151  		//  `caller must drain the channel entirely and wait until channel is closed before proceeding,
   152  		//   without waiting on the channel to be closed completely you might leak goroutines`
   153  		// So we should save the error and drain the channel.
   154  		if err != nil {
   155  			continue
   156  		}
   157  		if object.Err != nil {
   158  			err = errors.Wrapf(object.Err, "list object %s", object.Key)
   159  		}
   160  		list = append(list, strings.TrimPrefix(object.Key, s.prefix))
   161  	}
   162  	if err != nil {
   163  		return nil, err
   164  	}
   165  
   166  	return list, nil
   167  }
   168  
   169  func (s *S3) SetPrefix(prefix string) {
   170  	s.prefix = prefix
   171  }
   172  
   173  func (s *S3) GetPrefix() string {
   174  	return s.prefix
   175  }
   176  
   177  func (s *S3) DeleteObject(ctx context.Context, objectName string) error {
   178  	objPath := path.Join(s.prefix, objectName)
   179  	err := s.client.RemoveObject(ctx, s.bucketName, objPath, minio.RemoveObjectOptions{})
   180  	if err != nil {
   181  		if minio.ToErrorResponse(errors.Cause(err)).Code == "NoSuchKey" {
   182  			return ErrObjectNotFound
   183  		}
   184  		return errors.Wrapf(err, "failed to remove object %s", objectName)
   185  	}
   186  	return nil
   187  }
   188  
   189  // Azure is a type for working with Azure Blob storages
   190  type Azure struct {
   191  	client    *azblob.Client // azure client for work with storage
   192  	container string
   193  	prefix    string
   194  }
   195  
   196  func NewAzure(storageAccount, accessKey, endpoint, container, prefix string) (Storage, error) {
   197  	credential, err := azblob.NewSharedKeyCredential(storageAccount, accessKey)
   198  	if err != nil {
   199  		return nil, errors.Wrap(err, "new credentials")
   200  	}
   201  	if endpoint == "" {
   202  		endpoint = fmt.Sprintf("https://%s.blob.core.windows.net/", storageAccount)
   203  	}
   204  	cli, err := azblob.NewClientWithSharedKeyCredential(endpoint, credential, nil)
   205  	if err != nil {
   206  		return nil, errors.Wrap(err, "new client")
   207  	}
   208  
   209  	return &Azure{
   210  		client:    cli,
   211  		container: container,
   212  		prefix:    prefix,
   213  	}, nil
   214  }
   215  
   216  func (a *Azure) GetObject(ctx context.Context, name string) (io.ReadCloser, error) {
   217  	objPath := path.Join(a.prefix, name)
   218  	resp, err := a.client.DownloadStream(ctx, a.container, objPath, &azblob.DownloadStreamOptions{})
   219  	if err != nil {
   220  		if bloberror.HasCode(errors.Cause(err), bloberror.BlobNotFound) {
   221  			return nil, ErrObjectNotFound
   222  		}
   223  		return nil, errors.Wrapf(err, "download stream: %s", objPath)
   224  	}
   225  	return resp.Body, nil
   226  }
   227  
   228  func (a *Azure) PutObject(ctx context.Context, name string, data io.Reader, _ int64) error {
   229  	objPath := path.Join(a.prefix, name)
   230  	_, err := a.client.UploadStream(ctx, a.container, objPath, data, nil)
   231  	if err != nil {
   232  		return errors.Wrapf(err, "upload stream: %s", objPath)
   233  	}
   234  	return nil
   235  }
   236  
   237  func (a *Azure) ListObjects(ctx context.Context, prefix string) ([]string, error) {
   238  	listPrefix := path.Join(a.prefix, prefix) + "/"
   239  	pg := a.client.NewListBlobsFlatPager(a.container, &container.ListBlobsFlatOptions{
   240  		Prefix: &listPrefix,
   241  	})
   242  	var blobs []string
   243  	for pg.More() {
   244  		resp, err := pg.NextPage(ctx)
   245  		if err != nil {
   246  			return nil, errors.Wrapf(err, "next page: %s", prefix)
   247  		}
   248  		if resp.Segment != nil {
   249  			for _, item := range resp.Segment.BlobItems {
   250  				if item != nil && item.Name != nil {
   251  					name := strings.TrimPrefix(*item.Name, a.prefix)
   252  					blobs = append(blobs, name)
   253  				}
   254  			}
   255  		}
   256  	}
   257  	return blobs, nil
   258  }
   259  
   260  func (a *Azure) SetPrefix(prefix string) {
   261  	a.prefix = prefix
   262  }
   263  
   264  func (a *Azure) GetPrefix() string {
   265  	return a.prefix
   266  }
   267  
   268  func (a *Azure) DeleteObject(ctx context.Context, objectName string) error {
   269  	objPath := path.Join(a.prefix, objectName)
   270  	_, err := a.client.DeleteBlob(ctx, a.container, objPath, nil)
   271  	if err != nil {
   272  		if bloberror.HasCode(errors.Cause(err), bloberror.BlobNotFound) {
   273  			return ErrObjectNotFound
   274  		}
   275  		return errors.Wrapf(err, "delete blob %s", objPath)
   276  	}
   277  	return nil
   278  }