github.com/pachyderm/pachyderm@v1.13.4/src/server/pkg/obj/testing/client_test.go (about) 1 package testing 2 3 import ( 4 "context" 5 "fmt" 6 "io" 7 "path" 8 "testing" 9 10 "github.com/pachyderm/pachyderm/src/client/pkg/errors" 11 "github.com/pachyderm/pachyderm/src/client/pkg/require" 12 "github.com/pachyderm/pachyderm/src/server/pkg/obj" 13 "github.com/pachyderm/pachyderm/src/server/pkg/testetcd" 14 tu "github.com/pachyderm/pachyderm/src/server/pkg/testutil" 15 16 "google.golang.org/api/option" 17 ) 18 19 // BackendType is used to tell the tests which backend is being tested so some 20 // testing can be skipped for backends that do not support certain behavior. 21 type BackendType string 22 23 const ( 24 AmazonBackend BackendType = "Amazon" 25 ECSBackend BackendType = "ECS" 26 GoogleBackend BackendType = "Google" 27 MicrosoftBackend BackendType = "Microsoft" 28 LocalBackend BackendType = "Local" 29 ) 30 31 // ClientType is used to tell the tests which client is being tested so some testing 32 // can be skipped for clients that do not support certain behavior. 33 type ClientType string 34 35 const ( 36 AmazonClient ClientType = "Amazon" 37 MinioClient ClientType = "ECS" 38 GoogleClient ClientType = "Google" 39 MicrosoftClient ClientType = "Microsoft" 40 LocalClient ClientType = "Local" 41 ) 42 43 // NOTE: these tests require object storage credentials to be loaded in your 44 // environment (see util.go for where they are loaded). 45 46 func requireExists(t *testing.T, client obj.Client, object string, expected bool) { 47 exists := client.Exists(context.Background(), object) 48 require.Equal(t, expected, exists) 49 } 50 51 func doWriteTest(t *testing.T, backendType BackendType, clientType ClientType, client obj.Client, object string, writes []string) { 52 requireExists(t, client, object, false) 53 defer requireExists(t, client, object, false) 54 55 w, err := client.Writer(context.Background(), object) 56 require.NoError(t, err) 57 for _, s := range writes { 58 size, err := w.Write([]byte(s)) 59 require.NoError(t, err) 60 require.Equal(t, len(s), size) 61 } 62 require.NoError(t, w.Close()) 63 64 defer func() { 65 require.NoError(t, client.Delete(context.Background(), object)) 66 }() 67 68 requireExists(t, client, object, true) 69 70 data := "" 71 for _, s := range writes { 72 data += s 73 } 74 75 doRead := func(offset uint64, length uint64) error { 76 expected := "" 77 if length == 0 || int(offset+length) > len(data) { 78 expected = data[offset:] 79 } else { 80 expected = data[offset : offset+length] 81 } 82 83 r, err := client.Reader(context.Background(), object, offset, length) 84 require.NoError(t, err) 85 86 buf := make([]byte, len(data)+1) 87 size, err := r.Read(buf) 88 require.Equal(t, len(expected), size) 89 if err != nil && !errors.Is(err, io.EOF) { 90 require.NoError(t, r.Close()) 91 return err 92 } 93 94 size, err = r.Read(buf) 95 require.Equal(t, 0, size) 96 if err != nil && !errors.Is(err, io.EOF) { 97 require.NoError(t, r.Close()) 98 return err 99 } 100 101 require.NoError(t, r.Close()) 102 return nil 103 } 104 105 doRead(0, 0) 106 107 if len(data) > 0 { 108 // TODO: this check is broken on the MicrosoftClient due to how the BlobRange is implemented 109 if clientType != MicrosoftClient { 110 // Read the first character 111 err := doRead(0, 1) 112 require.NoError(t, err) 113 } 114 115 // Read the last character 116 err = doRead(uint64(len(data)-1), 1) 117 require.NoError(t, err) 118 119 // Read through the end of the object 120 err = doRead(uint64(len(data)-1), 10) 121 require.YesError(t, err) 122 require.Matches(t, "read stream ended after the wrong length", err.Error()) 123 124 // Read the middle of the object 125 err = doRead(1, uint64(len(data)-2)) 126 require.NoError(t, err) 127 128 // Read past the end of the object 129 _, err = client.Reader(context.Background(), object, uint64(len(data)+1), 1) 130 require.YesError(t, err) // The precise error here varies across clients and providers 131 } 132 } 133 134 func runClientTests(t *testing.T, backendType BackendType, clientType ClientType, client obj.Client) { 135 t.Run("TestMissingObject", func(t *testing.T) { 136 t.Parallel() 137 object := tu.UniqueString("test-missing-object-") 138 requireExists(t, client, object, false) 139 140 r, err := client.Reader(context.Background(), object, 0, 0) 141 require.Nil(t, r) 142 require.YesError(t, err) 143 require.True(t, client.IsNotExist(err)) 144 }) 145 146 t.Run("TestEmptyWrite", func(t *testing.T) { 147 t.Parallel() 148 doWriteTest(t, backendType, clientType, client, tu.UniqueString("test-empty-write-"), []string{}) 149 }) 150 151 t.Run("TestSingleWrite", func(t *testing.T) { 152 t.Parallel() 153 doWriteTest(t, backendType, clientType, client, tu.UniqueString("test-single-write-"), []string{"foo"}) 154 }) 155 156 t.Run("TestMultiWrite", func(t *testing.T) { 157 t.Parallel() 158 doWriteTest(t, backendType, clientType, client, tu.UniqueString("test-multi-write-"), []string{"foo", "bar"}) 159 }) 160 161 t.Run("TestSubdirectory", func(t *testing.T) { 162 t.Parallel() 163 object := path.Join(tu.UniqueString("test-subdirectory-"), "object") 164 doWriteTest(t, backendType, clientType, client, object, []string{"foo", "bar"}) 165 }) 166 167 // TODO: implement walk test 168 169 t.Run("TestInterruption", func(t *testing.T) { 170 // Interruption is currently not implemented on the Amazon, Microsoft, and Minio clients 171 // Amazon client - use *WithContext methods 172 // Microsoft client - move to github.com/Azure/azure-storage-blob-go which supports contexts 173 // Minio client - upgrade to v7 which supports contexts in all APIs 174 // Local client - interruptible file operations are not a thing in the stdlib 175 if clientType == AmazonClient || clientType == MicrosoftClient || clientType == MinioClient || clientType == LocalClient { 176 message := fmt.Sprintf("Interruption is not currently supported for the %s object client", clientType) 177 t.Skip(message) 178 } 179 t.Parallel() 180 181 // Make a canceled context 182 ctx, cancel := context.WithCancel(context.Background()) 183 cancel() 184 185 object := tu.UniqueString("test-interruption-") 186 defer requireExists(t, client, object, false) 187 188 // Some clients return an error immediately and some when the stream is closed 189 w, err := client.Writer(ctx, object) 190 require.NoError(t, err) 191 if w != nil { 192 err = w.Close() 193 } 194 require.YesError(t, err) 195 require.True(t, errors.Is(err, context.Canceled)) 196 197 // Some clients return an error immediately and some when the stream is closed 198 r, err := client.Reader(ctx, object, 0, 0) 199 if r != nil { 200 err = r.Close() 201 } 202 require.YesError(t, err) 203 require.True(t, errors.Is(err, context.Canceled)) 204 205 err = client.Delete(ctx, object) 206 require.YesError(t, err) 207 require.True(t, errors.Is(err, context.Canceled)) 208 209 err = client.Walk(ctx, object, func(name string) error { 210 require.False(t, true) 211 return nil 212 }) 213 require.YesError(t, err) 214 require.True(t, errors.Is(err, context.Canceled)) 215 216 exists := client.Exists(ctx, object) 217 require.False(t, exists) 218 }) 219 } 220 221 func TestAmazonClient(t *testing.T) { 222 t.Parallel() 223 224 amazonTests := func(t *testing.T, backendType BackendType, id string, secret string, bucket string, region string, endpoint string) { 225 for _, reverse := range []bool{true, false} { 226 t.Run(fmt.Sprintf("reverse=%v", reverse), func(t *testing.T) { 227 t.Parallel() 228 creds := &obj.AmazonCreds{ID: id, Secret: secret} 229 client, err := obj.NewAmazonClient(region, bucket, creds, "", endpoint, reverse) 230 require.NoError(t, err) 231 runClientTests(t, backendType, AmazonClient, client) 232 }) 233 } 234 } 235 236 // Test the Amazon client against S3 237 t.Run("AmazonObjectStorage", func(t *testing.T) { 238 t.Parallel() 239 id, secret, bucket, region := LoadAmazonParameters(t) 240 amazonTests(t, AmazonBackend, id, secret, bucket, region, "") 241 }) 242 243 // Test the Amazon client against ECS 244 t.Run("ECSObjectStorage", func(t *testing.T) { 245 t.Parallel() 246 id, secret, bucket, region, endpoint := LoadECSParameters(t) 247 amazonTests(t, ECSBackend, id, secret, bucket, region, endpoint) 248 }) 249 250 // Test the Amazon client against GCS 251 t.Run("GoogleObjectStorage", func(t *testing.T) { 252 t.Skip("Amazon client gets 'InvalidArgument' errors when running against GCS") 253 t.Parallel() 254 id, secret, bucket, region, endpoint := LoadGoogleHMACParameters(t) 255 amazonTests(t, GoogleBackend, id, secret, bucket, region, endpoint) 256 }) 257 } 258 259 func TestMinioClient(t *testing.T) { 260 t.Parallel() 261 minioTests := func(t *testing.T, backend BackendType, endpoint string, bucket string, id string, secret string) { 262 t.Run("S3v2", func(t *testing.T) { 263 if backend == AmazonBackend || backend == ECSBackend { 264 t.Skip("Minio client running S3v2 does not handle empty writes properly on S3 and ECS") // try upgrading to minio-go/v7? 265 } 266 t.Parallel() 267 client, err := obj.NewMinioClient(endpoint, bucket, id, secret, true, true) 268 require.NoError(t, err) 269 runClientTests(t, backend, MinioClient, client) 270 }) 271 272 t.Run("S3v4", func(t *testing.T) { 273 t.Parallel() 274 client, err := obj.NewMinioClient(endpoint, bucket, id, secret, true, false) 275 require.NoError(t, err) 276 runClientTests(t, backend, MinioClient, client) 277 }) 278 } 279 280 // Test the Minio client against S3 using the S3v2 and S3v4 APIs 281 t.Run("AmazonObjectStorage", func(t *testing.T) { 282 t.Parallel() 283 id, secret, bucket, region := LoadAmazonParameters(t) 284 endpoint := fmt.Sprintf("s3.%s.amazonaws.com", region) // Note that not all AWS regions support both http/https or both S3v2/S3v4 285 minioTests(t, AmazonBackend, endpoint, bucket, id, secret) 286 }) 287 288 // Test the Minio client against ECS using the S3v2 and S3v4 APIs 289 t.Run("ECSObjectStorage", func(t *testing.T) { 290 t.Parallel() 291 id, secret, bucket, _, endpoint := LoadECSParameters(t) 292 minioTests(t, ECSBackend, endpoint, bucket, id, secret) 293 }) 294 295 // Test the Minio client against GCS using the S3v2 and S3v4 APIs 296 t.Run("GoogleObjectStorage", func(t *testing.T) { 297 t.Parallel() 298 id, secret, bucket, _, endpoint := LoadGoogleHMACParameters(t) 299 minioTests(t, GoogleBackend, endpoint, bucket, id, secret) 300 }) 301 } 302 303 func TestGoogleClient(t *testing.T) { 304 t.Parallel() 305 bucket, credData := LoadGoogleParameters(t) 306 opts := []option.ClientOption{option.WithCredentialsJSON([]byte(credData))} 307 client, err := obj.NewGoogleClient(bucket, opts) 308 require.NoError(t, err) 309 runClientTests(t, GoogleBackend, GoogleClient, client) 310 } 311 312 func TestMicrosoftClient(t *testing.T) { 313 t.Parallel() 314 id, secret, container := LoadMicrosoftParameters(t) 315 client, err := obj.NewMicrosoftClient(container, id, secret) 316 require.NoError(t, err) 317 runClientTests(t, MicrosoftBackend, MicrosoftClient, client) 318 } 319 320 func TestLocalClient(t *testing.T) { 321 t.Parallel() 322 // We don't actually need etcd, but this gives us a callback-scoped temp directory 323 err := testetcd.WithEnv(func(env *testetcd.Env) error { 324 client, err := obj.NewLocalClient(env.Directory) 325 require.NoError(t, err) 326 runClientTests(t, LocalBackend, LocalClient, client) 327 return nil 328 }) 329 require.NoError(t, err) 330 }