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 }