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  }