github.com/NVIDIA/aistore@v1.3.23-0.20240517131212-7df6609be51d/ais/test/s3_compat_test.go (about)

     1  // Package integration_test.
     2  /*
     3   * Copyright (c) 2018-2024, NVIDIA CORPORATION. All rights reserved.
     4   */
     5  package integration_test
     6  
     7  import (
     8  	"context"
     9  	"crypto/rand"
    10  	"crypto/tls"
    11  	"io"
    12  	"net"
    13  	"net/http"
    14  	"net/url"
    15  	"strings"
    16  	"testing"
    17  	"time"
    18  
    19  	"github.com/NVIDIA/aistore/api"
    20  	"github.com/NVIDIA/aistore/api/apc"
    21  	"github.com/NVIDIA/aistore/api/env"
    22  	"github.com/NVIDIA/aistore/cmn"
    23  	"github.com/NVIDIA/aistore/cmn/cos"
    24  	"github.com/NVIDIA/aistore/cmn/feat"
    25  	"github.com/NVIDIA/aistore/tools"
    26  	"github.com/NVIDIA/aistore/tools/tassert"
    27  	"github.com/aws/aws-sdk-go-v2/aws"
    28  	"github.com/aws/aws-sdk-go-v2/service/s3"
    29  	"github.com/aws/aws-sdk-go-v2/service/s3/types"
    30  )
    31  
    32  type customTransport struct {
    33  	rt http.RoundTripper
    34  }
    35  
    36  func (t *customTransport) RoundTrip(req *http.Request) (*http.Response, error) {
    37  	bucket := strings.Split(req.URL.Host, ".")[0]
    38  	u, _ := url.Parse(proxyURL)
    39  	req.URL.Host = u.Host
    40  	req.URL.Path = "/s3/" + bucket + req.URL.Path
    41  	return t.rt.RoundTrip(req)
    42  }
    43  
    44  func newCustomTransport() *customTransport {
    45  	dialer := &net.Dialer{
    46  		Timeout:   30 * time.Second,
    47  		KeepAlive: 30 * time.Second,
    48  	}
    49  
    50  	return &customTransport{
    51  		rt: &http.Transport{
    52  			Proxy:                 http.ProxyFromEnvironment,
    53  			DialContext:           dialer.DialContext,
    54  			MaxIdleConns:          100,
    55  			IdleConnTimeout:       90 * time.Second,
    56  			TLSHandshakeTimeout:   10 * time.Second,
    57  			ExpectContinueTimeout: 1 * time.Second,
    58  			TLSClientConfig: &tls.Config{
    59  				InsecureSkipVerify: true,
    60  			},
    61  		},
    62  	}
    63  }
    64  
    65  func newS3Client() *http.Client {
    66  	return &http.Client{
    67  		Transport: newCustomTransport(),
    68  	}
    69  }
    70  
    71  func setBucketFeatures(t *testing.T, bck cmn.Bck, bprops *cmn.Bprops, nf feat.Flags) {
    72  	if bprops.Features.IsSet(nf) {
    73  		return // nothing to do
    74  	}
    75  	props := &cmn.BpropsToSet{Features: &nf}
    76  	_, err := api.SetBucketProps(baseParams, bck, props)
    77  	tassert.CheckFatal(t, err)
    78  
    79  	t.Cleanup(func() {
    80  		// restore original feature flags
    81  		props := &cmn.BpropsToSet{Features: &bprops.Features}
    82  		_, err := api.SetBucketProps(baseParams, bck, props)
    83  		tassert.CheckFatal(t, err)
    84  	})
    85  }
    86  
    87  func TestS3PresignedPutGet(t *testing.T) {
    88  	tools.CheckSkip(t, &tools.SkipTestArgs{Bck: cliBck, RequiresTLS: true, RequiredCloudProvider: apc.AWS})
    89  
    90  	var (
    91  		bck     = cliBck
    92  		objName = "object.txt"
    93  	)
    94  	bprops, err := api.HeadBucket(baseParams, bck, false)
    95  	tassert.CheckFatal(t, err)
    96  
    97  	setBucketFeatures(t, bck, bprops, feat.S3PresignedRequest)
    98  
    99  	tools.SetClusterConfig(t, cos.StrKVs{"features": feat.S3ReverseProxy.String()})
   100  	t.Cleanup(func() {
   101  		tools.SetClusterConfig(t, cos.StrKVs{"features": "0"})
   102  	})
   103  
   104  	/* TODO -- FIXME: alternatively, use env vars AWS_PROFILE et al:
   105  	cfg, err := config.LoadDefaultConfig(
   106  		context.Background(),
   107  		config.WithSharedConfigProfile("default"),
   108  	)
   109  	tassert.CheckFatal(t, err)
   110  	cfg.HTTPClient = newS3Client()
   111  	s3Client := s3.NewFromConfig(cfg)
   112  	*/
   113  	s3Client := s3.New(s3.Options{HTTPClient: newS3Client(), Region: env.AwsDefaultRegion()})
   114  
   115  	putOutput, err := s3Client.PutObject(context.Background(), &s3.PutObjectInput{
   116  		Bucket: aws.String(bck.Name),
   117  		Key:    aws.String(objName),
   118  		Body:   io.LimitReader(rand.Reader, fileSize),
   119  	})
   120  	tassert.CheckFatal(t, err)
   121  	tassert.Errorf(t, putOutput.ETag != nil, "ETag for PUT operation was not set")
   122  	tassert.Errorf(t, *putOutput.ETag != "", "ETag for PUT operation is empty")
   123  
   124  	getOutput, err := s3Client.GetObject(context.Background(), &s3.GetObjectInput{
   125  		Bucket: aws.String(bck.Name),
   126  		Key:    aws.String(objName),
   127  	})
   128  	tassert.CheckFatal(t, err)
   129  	tassert.Fatalf(t, getOutput.ETag != nil, "ETag for PUT operation was not set")
   130  	tassert.Errorf(t, *getOutput.ETag != "", "ETag for PUT operation is empty")
   131  
   132  	cos.DrainReader(getOutput.Body)
   133  	getOutput.Body.Close()
   134  
   135  	tassert.Errorf(t, *putOutput.ETag == *getOutput.ETag, "ETag does not match between PUT and GET operation (%s != %s)", *putOutput.ETag, *getOutput.ETag)
   136  }
   137  
   138  func TestS3PresignedMultipart(t *testing.T) {
   139  	tools.CheckSkip(t, &tools.SkipTestArgs{Long: true, Bck: cliBck, RequiresTLS: true, RequiredCloudProvider: apc.AWS})
   140  
   141  	var (
   142  		bck     = cliBck
   143  		objName = "object.txt"
   144  	)
   145  	bprops, err := api.HeadBucket(baseParams, bck, false)
   146  	tassert.CheckFatal(t, err)
   147  
   148  	setBucketFeatures(t, bck, bprops, feat.S3PresignedRequest)
   149  
   150  	tools.SetClusterConfig(t, cos.StrKVs{"features": feat.S3ReverseProxy.String()})
   151  	t.Cleanup(func() {
   152  		tools.SetClusterConfig(t, cos.StrKVs{"features": "0"})
   153  	})
   154  
   155  	s3Client := s3.New(s3.Options{HTTPClient: newS3Client(), Region: env.AwsDefaultRegion()})
   156  
   157  	createMultipartUploadOutput, err := s3Client.CreateMultipartUpload(context.Background(), &s3.CreateMultipartUploadInput{
   158  		Bucket: aws.String(bck.Name),
   159  		Key:    aws.String(objName),
   160  	})
   161  	tassert.CheckFatal(t, err)
   162  	tassert.Errorf(t, createMultipartUploadOutput.UploadId != nil, "UploadId for CreateMultipartUpload operation was not set")
   163  	tassert.Errorf(t, *createMultipartUploadOutput.UploadId != "", "UploadId for CreateMultipartUpload operation is empty")
   164  
   165  	var parts []types.CompletedPart //nolint:prealloc // Not needed.
   166  	for i := 1; i <= 3; i++ {
   167  		uploadPartOutput, err := s3Client.UploadPart(context.Background(), &s3.UploadPartInput{
   168  			Bucket:        aws.String(bck.Name),
   169  			Key:           aws.String(objName),
   170  			PartNumber:    aws.Int32(int32(i)),
   171  			UploadId:      createMultipartUploadOutput.UploadId,
   172  			Body:          io.LimitReader(rand.Reader, 5*cos.MiB),
   173  			ContentLength: aws.Int64(5 * cos.MiB),
   174  		})
   175  		tassert.CheckFatal(t, err)
   176  		tassert.Errorf(t, uploadPartOutput.ETag != nil, "ETag for UploadPart operation was not set")
   177  
   178  		parts = append(parts, types.CompletedPart{
   179  			ETag:       uploadPartOutput.ETag,
   180  			PartNumber: aws.Int32(int32(i)),
   181  		})
   182  	}
   183  
   184  	completeMultipartUpload, err := s3Client.CompleteMultipartUpload(context.Background(), &s3.CompleteMultipartUploadInput{
   185  		Bucket:          aws.String(bck.Name),
   186  		Key:             aws.String(objName),
   187  		UploadId:        createMultipartUploadOutput.UploadId,
   188  		MultipartUpload: &types.CompletedMultipartUpload{Parts: parts},
   189  	})
   190  	tassert.CheckFatal(t, err)
   191  	tassert.Errorf(t, completeMultipartUpload.ETag != nil, "ETag for CreateMultipartUpload was not set")
   192  
   193  	getOutput, err := s3Client.GetObject(context.Background(), &s3.GetObjectInput{
   194  		Bucket: aws.String(bck.Name),
   195  		Key:    aws.String(objName),
   196  	})
   197  	tassert.CheckFatal(t, err)
   198  	tassert.Fatalf(t, getOutput.ETag != nil, "ETag for GET operation was not set")
   199  	tassert.Errorf(t, *getOutput.ETag != "", "ETag for GET operation is empty")
   200  
   201  	cos.DrainReader(getOutput.Body)
   202  	getOutput.Body.Close()
   203  
   204  	tassert.Errorf(t,
   205  		*completeMultipartUpload.ETag == *getOutput.ETag,
   206  		"ETag does not match between multipart upload and GET operation (%s != %s)",
   207  		*completeMultipartUpload.ETag, *getOutput.ETag,
   208  	)
   209  }
   210  
   211  // This tests checks that when there is no object locally in the AIStore, we
   212  // won't get it from S3.
   213  func TestDisableColdGet(t *testing.T) {
   214  	tools.CheckSkip(t, &tools.SkipTestArgs{Bck: cliBck, RequiresTLS: true, RequiredCloudProvider: apc.AWS})
   215  
   216  	var (
   217  		bck     = cliBck
   218  		objName = "object.txt"
   219  	)
   220  
   221  	bprops, err := api.HeadBucket(baseParams, bck, false)
   222  	tassert.CheckFatal(t, err)
   223  
   224  	setBucketFeatures(t, bck, bprops, feat.S3PresignedRequest|feat.DisableColdGET)
   225  
   226  	tools.SetClusterConfig(t, cos.StrKVs{"features": feat.S3ReverseProxy.String()})
   227  	t.Cleanup(func() {
   228  		tools.SetClusterConfig(t, cos.StrKVs{"features": "0"})
   229  	})
   230  
   231  	s3Client := s3.New(s3.Options{HTTPClient: newS3Client(), Region: env.AwsDefaultRegion()})
   232  
   233  	putOutput, err := s3Client.PutObject(context.Background(), &s3.PutObjectInput{
   234  		Bucket: aws.String(bck.Name),
   235  		Key:    aws.String(objName),
   236  		Body:   io.LimitReader(rand.Reader, fileSize),
   237  	})
   238  	tassert.CheckFatal(t, err)
   239  	tassert.Errorf(t, putOutput.ETag != nil, "ETag for PUT operation was not set")
   240  	tassert.Errorf(t, *putOutput.ETag != "", "ETag for PUT operation is empty")
   241  
   242  	err = api.EvictRemoteBucket(baseParams, bck, true)
   243  	tassert.CheckFatal(t, err)
   244  
   245  	_, err = s3Client.GetObject(context.Background(), &s3.GetObjectInput{
   246  		Bucket: aws.String(bck.Name),
   247  		Key:    aws.String(objName),
   248  	})
   249  	tassert.Fatalf(t, err != nil, "Expected GET to fail %v", err)
   250  }