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 }