github.com/minio/minio@v0.0.0-20240328213742-3f72439b8a27/cmd/warm-backend-s3.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  	"errors"
    23  	"fmt"
    24  	"io"
    25  	"net/http"
    26  	"net/url"
    27  	"strings"
    28  	"sync"
    29  	"time"
    30  
    31  	"github.com/minio/madmin-go/v3"
    32  	"github.com/minio/minio-go/v7"
    33  	"github.com/minio/minio-go/v7/pkg/credentials"
    34  )
    35  
    36  // getRemoteTierTargetInstanceTransport contains a singleton roundtripper.
    37  var (
    38  	getRemoteTierTargetInstanceTransport     http.RoundTripper
    39  	getRemoteTierTargetInstanceTransportOnce sync.Once
    40  )
    41  
    42  type warmBackendS3 struct {
    43  	client       *minio.Client
    44  	core         *minio.Core
    45  	Bucket       string
    46  	Prefix       string
    47  	StorageClass string
    48  }
    49  
    50  func (s3 *warmBackendS3) ToObjectError(err error, params ...string) error {
    51  	object := ""
    52  	if len(params) >= 1 {
    53  		object = params[0]
    54  	}
    55  
    56  	return ErrorRespToObjectError(err, s3.Bucket, s3.getDest(object))
    57  }
    58  
    59  func (s3 *warmBackendS3) getDest(object string) string {
    60  	destObj := object
    61  	if s3.Prefix != "" {
    62  		destObj = fmt.Sprintf("%s/%s", s3.Prefix, object)
    63  	}
    64  	return destObj
    65  }
    66  
    67  func (s3 *warmBackendS3) Put(ctx context.Context, object string, r io.Reader, length int64) (remoteVersionID, error) {
    68  	res, err := s3.client.PutObject(ctx, s3.Bucket, s3.getDest(object), r, length, minio.PutObjectOptions{
    69  		SendContentMd5: true,
    70  		StorageClass:   s3.StorageClass,
    71  	})
    72  	return remoteVersionID(res.VersionID), s3.ToObjectError(err, object)
    73  }
    74  
    75  func (s3 *warmBackendS3) Get(ctx context.Context, object string, rv remoteVersionID, opts WarmBackendGetOpts) (io.ReadCloser, error) {
    76  	gopts := minio.GetObjectOptions{}
    77  
    78  	if rv != "" {
    79  		gopts.VersionID = string(rv)
    80  	}
    81  	if opts.startOffset >= 0 && opts.length > 0 {
    82  		if err := gopts.SetRange(opts.startOffset, opts.startOffset+opts.length-1); err != nil {
    83  			return nil, s3.ToObjectError(err, object)
    84  		}
    85  	}
    86  	c := &minio.Core{Client: s3.client}
    87  	// Important to use core primitives here to pass range get options as is.
    88  	r, _, _, err := c.GetObject(ctx, s3.Bucket, s3.getDest(object), gopts)
    89  	if err != nil {
    90  		return nil, s3.ToObjectError(err, object)
    91  	}
    92  	return r, nil
    93  }
    94  
    95  func (s3 *warmBackendS3) Remove(ctx context.Context, object string, rv remoteVersionID) error {
    96  	ropts := minio.RemoveObjectOptions{}
    97  	if rv != "" {
    98  		ropts.VersionID = string(rv)
    99  	}
   100  	err := s3.client.RemoveObject(ctx, s3.Bucket, s3.getDest(object), ropts)
   101  	return s3.ToObjectError(err, object)
   102  }
   103  
   104  func (s3 *warmBackendS3) InUse(ctx context.Context) (bool, error) {
   105  	result, err := s3.core.ListObjectsV2(s3.Bucket, s3.Prefix, "", "", slashSeparator, 1)
   106  	if err != nil {
   107  		return false, s3.ToObjectError(err)
   108  	}
   109  	return len(result.CommonPrefixes) > 0 || len(result.Contents) > 0, nil
   110  }
   111  
   112  func newWarmBackendS3(conf madmin.TierS3, tier string) (*warmBackendS3, error) {
   113  	u, err := url.Parse(conf.Endpoint)
   114  	if err != nil {
   115  		return nil, err
   116  	}
   117  
   118  	// Validation code
   119  	switch {
   120  	case conf.AWSRoleWebIdentityTokenFile == "" && conf.AWSRoleARN != "" || conf.AWSRoleWebIdentityTokenFile != "" && conf.AWSRoleARN == "":
   121  		return nil, errors.New("both the token file and the role ARN are required")
   122  	case conf.AccessKey == "" && conf.SecretKey != "" || conf.AccessKey != "" && conf.SecretKey == "":
   123  		return nil, errors.New("both the access and secret keys are required")
   124  	case conf.AWSRole && (conf.AWSRoleWebIdentityTokenFile != "" || conf.AWSRoleARN != "" || conf.AccessKey != "" || conf.SecretKey != ""):
   125  		return nil, errors.New("AWS Role cannot be activated with static credentials or the web identity token file")
   126  	case conf.Bucket == "":
   127  		return nil, errors.New("no bucket name was provided")
   128  	}
   129  
   130  	// Credentials initialization
   131  	var creds *credentials.Credentials
   132  	switch {
   133  	case conf.AWSRole:
   134  		creds = credentials.New(&credentials.IAM{
   135  			Client: &http.Client{
   136  				Transport: NewHTTPTransport(),
   137  			},
   138  		})
   139  	case conf.AWSRoleWebIdentityTokenFile != "" && conf.AWSRoleARN != "":
   140  		sessionName := conf.AWSRoleSessionName
   141  		if sessionName == "" {
   142  			// RoleSessionName has a limited set of characters (https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html)
   143  			sessionName = "minio-tier-" + mustGetUUID()
   144  		}
   145  		s3WebIdentityIAM := credentials.IAM{
   146  			Client: &http.Client{
   147  				Transport: NewHTTPTransport(),
   148  			},
   149  			EKSIdentity: struct {
   150  				TokenFile       string
   151  				RoleARN         string
   152  				RoleSessionName string
   153  			}{
   154  				conf.AWSRoleWebIdentityTokenFile,
   155  				conf.AWSRoleARN,
   156  				sessionName,
   157  			},
   158  		}
   159  		creds = credentials.New(&s3WebIdentityIAM)
   160  	case conf.AccessKey != "" && conf.SecretKey != "":
   161  		creds = credentials.NewStaticV4(conf.AccessKey, conf.SecretKey, "")
   162  	default:
   163  		return nil, errors.New("insufficient parameters for S3 backend authentication")
   164  	}
   165  	getRemoteTierTargetInstanceTransportOnce.Do(func() {
   166  		getRemoteTierTargetInstanceTransport = NewHTTPTransportWithTimeout(10 * time.Minute)
   167  	})
   168  	opts := &minio.Options{
   169  		Creds:     creds,
   170  		Secure:    u.Scheme == "https",
   171  		Transport: getRemoteTierTargetInstanceTransport,
   172  	}
   173  	client, err := minio.New(u.Host, opts)
   174  	if err != nil {
   175  		return nil, err
   176  	}
   177  	client.SetAppInfo(fmt.Sprintf("s3-tier-%s", tier), ReleaseTag)
   178  
   179  	core := &minio.Core{Client: client}
   180  	return &warmBackendS3{
   181  		client:       client,
   182  		core:         core,
   183  		Bucket:       conf.Bucket,
   184  		Prefix:       strings.TrimSuffix(conf.Prefix, slashSeparator),
   185  		StorageClass: conf.StorageClass,
   186  	}, nil
   187  }