diff options
author | Jacob Vosmaer <jacob@gitlab.com> | 2018-11-27 17:30:54 +0300 |
---|---|---|
committer | Jacob Vosmaer <jacob@gitlab.com> | 2018-11-27 17:30:54 +0300 |
commit | d5b4072ea07f858aa4d7b9de012c4797039c3e0a (patch) | |
tree | 4936684e9bdee624079645e6569858c6df098d76 | |
parent | 92cc07f67557c4719edd4d83dc2040b646716e4c (diff) | |
parent | fbf23ca6f33e8a5f4531f1bb311a361377223d60 (diff) |
Merge branch 'zj-objectpool-structure' into 'master'
Create and Remove object pools
Closes #1344
See merge request gitlab-org/gitaly!966
-rw-r--r-- | internal/git/alternates/alternates.go | 4 | ||||
-rw-r--r-- | internal/git/command.go | 5 | ||||
-rw-r--r-- | internal/git/objectpool/clone.go | 63 | ||||
-rw-r--r-- | internal/git/objectpool/clone_test.go | 64 | ||||
-rw-r--r-- | internal/git/objectpool/link.go | 17 | ||||
-rw-r--r-- | internal/git/objectpool/path.go | 20 | ||||
-rw-r--r-- | internal/git/objectpool/pool.go | 107 | ||||
-rw-r--r-- | internal/git/objectpool/pool_test.go | 91 | ||||
-rw-r--r-- | internal/git/objectpool/remote.go | 16 | ||||
-rw-r--r-- | internal/git/objectpool/remote_test.go | 27 | ||||
-rw-r--r-- | internal/git/repository/repository.go | 9 | ||||
-rw-r--r-- | internal/helper/repo.go | 6 | ||||
-rw-r--r-- | internal/helper/security.go | 11 | ||||
-rw-r--r-- | internal/helper/security_test.go | 18 | ||||
-rw-r--r-- | internal/service/objectpool/create.go | 59 | ||||
-rw-r--r-- | internal/service/objectpool/create_test.go | 140 | ||||
-rw-r--r-- | internal/service/objectpool/link.go | 16 | ||||
-rw-r--r-- | internal/service/objectpool/server.go | 12 | ||||
-rw-r--r-- | internal/service/objectpool/testhelper_test.go | 56 | ||||
-rw-r--r-- | internal/service/register.go | 2 |
20 files changed, 735 insertions, 8 deletions
diff --git a/internal/git/alternates/alternates.go b/internal/git/alternates/alternates.go index 25ac0cf3e..29ba66b8f 100644 --- a/internal/git/alternates/alternates.go +++ b/internal/git/alternates/alternates.go @@ -5,14 +5,14 @@ import ( "path" "strings" - "gitlab.com/gitlab-org/gitaly-proto/go/gitalypb" + "gitlab.com/gitlab-org/gitaly/internal/git/repository" "gitlab.com/gitlab-org/gitaly/internal/helper" ) // PathAndEnv finds the disk path to a repository, and returns the // alternate object directory environment variables encoded in the // gitalypb.Repository instance. -func PathAndEnv(repo *gitalypb.Repository) (string, []string, error) { +func PathAndEnv(repo repository.GitRepo) (string, []string, error) { repoPath, err := helper.GetRepoPath(repo) if err != nil { return "", nil, err diff --git a/internal/git/command.go b/internal/git/command.go index a552e5ddc..012a3f895 100644 --- a/internal/git/command.go +++ b/internal/git/command.go @@ -5,15 +5,14 @@ import ( "io" "os/exec" - "gitlab.com/gitlab-org/gitaly-proto/go/gitalypb" "gitlab.com/gitlab-org/gitaly/internal/command" "gitlab.com/gitlab-org/gitaly/internal/git/alternates" + "gitlab.com/gitlab-org/gitaly/internal/git/repository" ) // Command creates a git.Command with the given args and Repository -func Command(ctx context.Context, repo *gitalypb.Repository, args ...string) (*command.Command, error) { +func Command(ctx context.Context, repo repository.GitRepo, args ...string) (*command.Command, error) { repoPath, env, err := alternates.PathAndEnv(repo) - if err != nil { return nil, err } diff --git a/internal/git/objectpool/clone.go b/internal/git/objectpool/clone.go new file mode 100644 index 000000000..19795a662 --- /dev/null +++ b/internal/git/objectpool/clone.go @@ -0,0 +1,63 @@ +package objectpool + +import ( + "context" + "io" + "os" + "path" + + "gitlab.com/gitlab-org/gitaly-proto/go/gitalypb" + "gitlab.com/gitlab-org/gitaly/internal/git" + "gitlab.com/gitlab-org/gitaly/internal/helper" +) + +// Clone a repository to a pool, without setting the alternates, is not the +// resposibility of this function. +func (o *ObjectPool) clone(ctx context.Context, repo *gitalypb.Repository) error { + repoPath, err := helper.GetRepoPath(repo) + if err != nil { + return err + } + + targetDir := o.FullPath() + targetName := path.Base(targetDir) + if err != nil { + return err + } + + cloneArgs := []string{"-C", path.Dir(targetDir), "clone", "--quiet", "--bare", "--local", repoPath, targetName} + cmd, err := git.CommandWithoutRepo(ctx, cloneArgs...) + if err != nil { + return err + } + + return cmd.Wait() +} + +func (o *ObjectPool) removeRefs(ctx context.Context) error { + pipeReader, pipeWriter := io.Pipe() + defer pipeReader.Close() + defer pipeWriter.Close() + + cmd, err := git.BareCommand(ctx, nil, pipeWriter, os.Stderr, nil, "--git-dir", o.FullPath(), "for-each-ref", "--format=delete %(refname)") + if err != nil { + return err + } + + updateRefCmd, err := git.BareCommand(ctx, pipeReader, nil, os.Stderr, nil, "-C", o.FullPath(), "update-ref", "--stdin") + if err != nil { + return err + } + + if err := cmd.Wait(); err != nil { + return err + } + + pipeWriter.Close() + + return updateRefCmd.Wait() +} + +func (o *ObjectPool) removeHooksDir() error { + return os.RemoveAll(path.Join(o.FullPath(), "hooks")) +} diff --git a/internal/git/objectpool/clone_test.go b/internal/git/objectpool/clone_test.go new file mode 100644 index 000000000..0c2a2b1d7 --- /dev/null +++ b/internal/git/objectpool/clone_test.go @@ -0,0 +1,64 @@ +package objectpool + +import ( + "path" + "testing" + + "github.com/stretchr/testify/require" + "gitlab.com/gitlab-org/gitaly/internal/testhelper" +) + +func TestClone(t *testing.T) { + ctx, cancel := testhelper.Context() + defer cancel() + + testRepo, _, cleanupFn := testhelper.NewTestRepo(t) + defer cleanupFn() + + pool, err := NewObjectPool(testRepo.GetStorageName(), "@pools"+t.Name()) + require.NoError(t, err) + + err = pool.clone(ctx, testRepo) + require.NoError(t, err) + defer pool.Remove(ctx) + + require.DirExists(t, pool.FullPath()) + require.DirExists(t, path.Join(pool.FullPath(), "objects")) +} + +func TestRemoveRefs(t *testing.T) { + ctx, cancel := testhelper.Context() + defer cancel() + + testRepo, _, cleanupFn := testhelper.NewTestRepo(t) + defer cleanupFn() + + pool, err := NewObjectPool(testRepo.GetStorageName(), t.Name()) + require.NoError(t, err) + defer pool.Remove(ctx) + + require.NoError(t, pool.clone(ctx, testRepo)) + require.NoError(t, pool.removeRefs(ctx)) + + out := testhelper.MustRunCommand(t, nil, "git", "-C", pool.FullPath(), "for-each-ref") + require.Len(t, out, 0) +} + +func TestCloneExistingPool(t *testing.T) { + ctx, cancel := testhelper.Context() + defer cancel() + + testRepo, _, cleanupFn := testhelper.NewTestRepo(t) + defer cleanupFn() + + pool, err := NewObjectPool(testRepo.GetStorageName(), t.Name()) + require.NoError(t, err) + + err = pool.clone(ctx, testRepo) + require.NoError(t, err) + defer pool.Remove(ctx) + + // Reclone on the directory + err = pool.clone(ctx, testRepo) + require.Error(t, err) +} diff --git a/internal/git/objectpool/link.go b/internal/git/objectpool/link.go new file mode 100644 index 000000000..106fc56b3 --- /dev/null +++ b/internal/git/objectpool/link.go @@ -0,0 +1,17 @@ +package objectpool + +import ( + "context" + + "gitlab.com/gitlab-org/gitaly/internal/git/repository" +) + +// Link writes the alternate file +func Link(ctx context.Context, pool, repository repository.GitRepo) error { + return nil +} + +// Unlink removes the alternate file +func Unlink(ctx context.Context, pool, repository repository.GitRepo) error { + return nil +} diff --git a/internal/git/objectpool/path.go b/internal/git/objectpool/path.go new file mode 100644 index 000000000..4f538c5a5 --- /dev/null +++ b/internal/git/objectpool/path.go @@ -0,0 +1,20 @@ +package objectpool + +import "path/filepath" + +// GetRelativePath will create the relative path to the ObjectPool from the +// storage path. +func (o *ObjectPool) GetRelativePath() string { + return o.relativePath +} + +// GetStorageName exposes the shard name, to satisfy the repository.GitRepo +// interface +func (o *ObjectPool) GetStorageName() string { + return o.storageName +} + +// FullPath on disk, depending on the storage path, and the pools relative path +func (o *ObjectPool) FullPath() string { + return filepath.Join(o.storagePath, o.GetRelativePath()) +} diff --git a/internal/git/objectpool/pool.go b/internal/git/objectpool/pool.go new file mode 100644 index 000000000..7da743132 --- /dev/null +++ b/internal/git/objectpool/pool.go @@ -0,0 +1,107 @@ +package objectpool + +import ( + "context" + "os" + "path" + + "gitlab.com/gitlab-org/gitaly-proto/go/gitalypb" + "gitlab.com/gitlab-org/gitaly/internal/helper" +) + +// ObjectPool are a way to dedup objects between repositories, where the objects +// live in a pool in a distinct repository which is used as an alternate object +// store for other repositories. +type ObjectPool struct { + storageName string + storagePath string + + relativePath string +} + +// NewObjectPool will initialize the object with the required data on the storage +// shard. If the shard cannot be found, this function returns an error +func NewObjectPool(storageName, relativePath string) (pool *ObjectPool, err error) { + storagePath, err := helper.GetStorageByName(storageName) + if err != nil { + return nil, err + } + + return &ObjectPool{storageName, storagePath, relativePath}, nil +} + +// GetGitAlternateObjectDirectories for object pools are empty, given pools are +// never a member of another pool, nor do they share Alternate objects with other +// repositories which the pool doesn't contain itself +func (o *ObjectPool) GetGitAlternateObjectDirectories() []string { + return []string{} +} + +// GetGitObjectDirectory satisfies the repository.GitRepo interface, but is not +// used for ObjectPools +func (o *ObjectPool) GetGitObjectDirectory() string { + return "" +} + +// Exists will return true if the pool path exists and is a directory +func (o *ObjectPool) Exists() bool { + fi, err := os.Stat(o.FullPath()) + if os.IsNotExist(err) || err != nil { + return false + } + + return fi.IsDir() +} + +// IsValid checks if a repository exists, and if its valid. +func (o *ObjectPool) IsValid() bool { + if !o.Exists() { + return false + } + + return helper.IsGitDirectory(o.FullPath()) +} + +// Create will create a pool for a repository and pull the required data to this +// pool. `repo` that is passed also joins the repository. +func (o *ObjectPool) Create(ctx context.Context, repo *gitalypb.Repository) (err error) { + if err := os.MkdirAll(path.Dir(o.FullPath()), 0755); err != nil { + return err + } + + if err := o.clone(ctx, repo); err != nil { + return err + } + + if err := o.removeHooksDir(); err != nil { + return err + } + + if err := o.removeRemote(ctx, "origin"); err != nil { + return err + } + + if err := o.removeRefs(ctx); err != nil { + return err + } + + return nil +} + +// Remove will remove the pool, and all its contents without preparing and/or +// updating the repositories depending on this object pool +// Subdirectories will remain to exist, and will never be cleaned up, even when +// these are empty. +func (o *ObjectPool) Remove(ctx context.Context) (err error) { + return os.RemoveAll(o.FullPath()) +} + +// ToProto returns a new struct that is the protobuf definition of the ObjectPool +func (o *ObjectPool) ToProto() *gitalypb.ObjectPool { + return &gitalypb.ObjectPool{ + Repository: &gitalypb.Repository{ + StorageName: o.GetStorageName(), + RelativePath: o.GetRelativePath(), + }, + } +} diff --git a/internal/git/objectpool/pool_test.go b/internal/git/objectpool/pool_test.go new file mode 100644 index 000000000..d1fe05ba6 --- /dev/null +++ b/internal/git/objectpool/pool_test.go @@ -0,0 +1,91 @@ +package objectpool + +import ( + "os" + "path" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gitlab.com/gitlab-org/gitaly/internal/testhelper" +) + +func TestNewObjectPool(t *testing.T) { + _, err := NewObjectPool("default", t.Name()) + require.NoError(t, err) + + _, err = NewObjectPool("mepmep", t.Name()) + require.Error(t, err) +} + +func TestCreate(t *testing.T) { + ctx, cancel := testhelper.Context() + defer cancel() + + testRepo, _, cleanupFn := testhelper.NewTestRepo(t) + defer cleanupFn() + + pool, err := NewObjectPool(testRepo.GetStorageName(), t.Name()) + require.NoError(t, err) + + err = pool.Create(ctx, testRepo) + require.NoError(t, err) + defer pool.Remove(ctx) + + require.True(t, pool.IsValid()) + + // No refs + out := testhelper.MustRunCommand(t, nil, "git", "-C", pool.FullPath(), "for-each-ref") + assert.Len(t, out, 0) + + // No remotes + out = testhelper.MustRunCommand(t, nil, "git", "-C", pool.FullPath(), "remote") + assert.Len(t, out, 0) + + // No hooks + _, err = os.Stat(path.Join(pool.FullPath(), "hooks")) + assert.True(t, os.IsNotExist(err)) + + // No problems + out = testhelper.MustRunCommand(t, nil, "git", "-C", pool.FullPath(), "cat-file", "-s", "55bc176024cfa3baaceb71db584c7e5df900ea65") + assert.Equal(t, "282\n", string(out)) +} + +func TestCreateSubDirsExist(t *testing.T) { + ctx, cancel := testhelper.Context() + defer cancel() + + testRepo, _, cleanupFn := testhelper.NewTestRepo(t) + defer cleanupFn() + + pool, err := NewObjectPool(testRepo.GetStorageName(), t.Name()) + defer pool.Remove(ctx) + require.NoError(t, err) + + err = pool.Create(ctx, testRepo) + require.NoError(t, err) + + pool.Remove(ctx) + + // Recreate pool so the subdirs exist already + err = pool.Create(ctx, testRepo) + require.NoError(t, err) +} + +func TestRemove(t *testing.T) { + ctx, cancel := testhelper.Context() + defer cancel() + + testRepo, _, cleanupFn := testhelper.NewTestRepo(t) + defer cleanupFn() + + pool, err := NewObjectPool(testRepo.GetStorageName(), t.Name()) + require.NoError(t, err) + + err = pool.Create(ctx, testRepo) + require.NoError(t, err) + + require.True(t, pool.Exists()) + require.NoError(t, pool.Remove(ctx)) + require.False(t, pool.Exists()) +} diff --git a/internal/git/objectpool/remote.go b/internal/git/objectpool/remote.go new file mode 100644 index 000000000..589808c9c --- /dev/null +++ b/internal/git/objectpool/remote.go @@ -0,0 +1,16 @@ +package objectpool + +import ( + "context" + + "gitlab.com/gitlab-org/gitaly/internal/git" +) + +func (o *ObjectPool) removeRemote(ctx context.Context, name string) error { + cmd, err := git.Command(ctx, o, "remote", "remove", name) + if err != nil { + return err + } + + return cmd.Wait() +} diff --git a/internal/git/objectpool/remote_test.go b/internal/git/objectpool/remote_test.go new file mode 100644 index 000000000..ef07e02fd --- /dev/null +++ b/internal/git/objectpool/remote_test.go @@ -0,0 +1,27 @@ +package objectpool + +import ( + "testing" + + "github.com/stretchr/testify/require" + "gitlab.com/gitlab-org/gitaly/internal/testhelper" +) + +func TestRemoveRemote(t *testing.T) { + ctx, cancel := testhelper.Context() + defer cancel() + + testRepo, _, cleanupFn := testhelper.NewTestRepo(t) + defer cleanupFn() + + pool, err := NewObjectPool(testRepo.GetStorageName(), t.Name()) + require.NoError(t, err) + + require.NoError(t, pool.clone(ctx, testRepo)) + defer pool.Remove(ctx) + + require.NoError(t, pool.removeRemote(ctx, "origin")) + + out := testhelper.MustRunCommand(t, nil, "git", "-C", pool.FullPath(), "remote") + require.Len(t, out, 0) +} diff --git a/internal/git/repository/repository.go b/internal/git/repository/repository.go new file mode 100644 index 000000000..3a200d7a8 --- /dev/null +++ b/internal/git/repository/repository.go @@ -0,0 +1,9 @@ +package repository + +// GitRepo supplies an interface for executing `git.Command`s +type GitRepo interface { + GetStorageName() string + GetRelativePath() string + GetGitObjectDirectory() string + GetGitAlternateObjectDirectories() []string +} diff --git a/internal/helper/repo.go b/internal/helper/repo.go index 5397b2fca..4a5ba852b 100644 --- a/internal/helper/repo.go +++ b/internal/helper/repo.go @@ -4,8 +4,8 @@ import ( "os" "path" - "gitlab.com/gitlab-org/gitaly-proto/go/gitalypb" "gitlab.com/gitlab-org/gitaly/internal/config" + "gitlab.com/gitlab-org/gitaly/internal/git/repository" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) @@ -14,7 +14,7 @@ import ( // RPC Repository message. The errors returned are gRPC errors with // relevant error codes and should be passed back to gRPC without further // decoration. -func GetRepoPath(repo *gitalypb.Repository) (string, error) { +func GetRepoPath(repo repository.GitRepo) (string, error) { repoPath, err := GetPath(repo) if err != nil { return "", err @@ -34,7 +34,7 @@ func GetRepoPath(repo *gitalypb.Repository) (string, error) { // GetPath returns the path of the repo passed as first argument. An error is // returned when either the storage can't be found or the path includes // constructs trying to perform directory traversal. -func GetPath(repo *gitalypb.Repository) (string, error) { +func GetPath(repo repository.GitRepo) (string, error) { storagePath, err := GetStorageByName(repo.GetStorageName()) if err != nil { return "", err diff --git a/internal/helper/security.go b/internal/helper/security.go index 8dae4f9c5..afbcced9d 100644 --- a/internal/helper/security.go +++ b/internal/helper/security.go @@ -2,6 +2,7 @@ package helper import ( "os" + "regexp" "strings" ) @@ -13,3 +14,13 @@ func ContainsPathTraversal(path string) bool { strings.Contains(path, separator+".."+separator) || strings.HasSuffix(path, separator+"..") } + +// Pattern taken from Regular Expressions Cookbook, slightly modified though +// |Scheme |User |Named/IPv4 host|IPv6+ host +var hostPattern = regexp.MustCompile(`(?i)([a-z][a-z0-9+\-.]*://)([a-z0-9\-._~%!$&'()*+,;=:]+@)([a-z0-9\-._~%]+|\[[a-z0-9\-._~%!$&'()*+,;=:]+\])`) + +// SanitizeString will clean password and tokens from URLs, and replace them +// with [FILTERED]. +func SanitizeString(str string) string { + return hostPattern.ReplaceAllString(str, "$1[FILTERED]@$3$4") +} diff --git a/internal/helper/security_test.go b/internal/helper/security_test.go index cd31d9f73..9a8125dac 100644 --- a/internal/helper/security_test.go +++ b/internal/helper/security_test.go @@ -22,3 +22,21 @@ func TestContainsPathTraversal(t *testing.T) { assert.Equal(t, tc.containsTraversal, ContainsPathTraversal(tc.path)) } } + +func TestSanitizeString(t *testing.T) { + testCases := []struct { + input string + output string + }{ + {"https://foo_the_user@gitlab.com/foo/bar", "https://[FILTERED]@gitlab.com/foo/bar"}, + {"https://foo_the_user:hUntEr1@gitlab.com/foo/bar", "https://[FILTERED]@gitlab.com/foo/bar"}, + {"proto://user:password@gitlab.com", "proto://[FILTERED]@gitlab.com"}, + {"some message proto://user:password@gitlab.com", "some message proto://[FILTERED]@gitlab.com"}, + {"test", "test"}, + {"ssh://@gitlab.com", "ssh://@gitlab.com"}, + } + + for _, tc := range testCases { + assert.Equal(t, tc.output, SanitizeString(tc.input)) + } +} diff --git a/internal/service/objectpool/create.go b/internal/service/objectpool/create.go new file mode 100644 index 000000000..f6997378a --- /dev/null +++ b/internal/service/objectpool/create.go @@ -0,0 +1,59 @@ +package objectpool + +import ( + "context" + + "gitlab.com/gitlab-org/gitaly-proto/go/gitalypb" + "gitlab.com/gitlab-org/gitaly/internal/git/objectpool" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +func (s *server) CreateObjectPool(ctx context.Context, in *gitalypb.CreateObjectPoolRequest) (*gitalypb.CreateObjectPoolResponse, error) { + if in.GetOrigin() == nil { + return nil, status.Errorf(codes.InvalidArgument, "no origin repository") + } + + pool, err := poolForRequest(in) + if err != nil { + return nil, err + } + + if pool.Exists() { + return nil, status.Errorf(codes.FailedPrecondition, "pool already exists at: %v", pool.GetRelativePath()) + } + + if err := pool.Create(ctx, in.GetOrigin()); err != nil { + return nil, err + } + + return &gitalypb.CreateObjectPoolResponse{}, nil +} + +func (s *server) DeleteObjectPool(ctx context.Context, in *gitalypb.DeleteObjectPoolRequest) (*gitalypb.DeleteObjectPoolResponse, error) { + pool, err := poolForRequest(in) + if err != nil { + return nil, err + } + + if err := pool.Remove(ctx); err != nil { + return nil, err + } + + return &gitalypb.DeleteObjectPoolResponse{}, nil +} + +type poolRequest interface { + GetObjectPool() *gitalypb.ObjectPool +} + +func poolForRequest(req poolRequest) (*objectpool.ObjectPool, error) { + reqPool := req.GetObjectPool() + + poolRepo := reqPool.GetRepository() + if poolRepo == nil { + return nil, status.Errorf(codes.InvalidArgument, "no object pool repository") + } + + return objectpool.NewObjectPool(poolRepo.GetStorageName(), poolRepo.GetRelativePath()) +} diff --git a/internal/service/objectpool/create_test.go b/internal/service/objectpool/create_test.go new file mode 100644 index 000000000..41d069607 --- /dev/null +++ b/internal/service/objectpool/create_test.go @@ -0,0 +1,140 @@ +package objectpool + +import ( + "os" + "path" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gitlab.com/gitlab-org/gitaly-proto/go/gitalypb" + "gitlab.com/gitlab-org/gitaly/internal/git/objectpool" + "gitlab.com/gitlab-org/gitaly/internal/helper" + "gitlab.com/gitlab-org/gitaly/internal/testhelper" + "google.golang.org/grpc/codes" +) + +func TestCreate(t *testing.T) { + server, serverSocketPath := runObjectPoolServer(t) + defer server.Stop() + + client, conn := newObjectPoolClient(t, serverSocketPath) + defer conn.Close() + + ctx, cancel := testhelper.Context() + defer cancel() + + testRepo, _, cleanupFn := testhelper.NewTestRepo(t) + defer cleanupFn() + + pool, err := objectpool.NewObjectPool("default", t.Name()) + require.NoError(t, err) + + poolReq := &gitalypb.CreateObjectPoolRequest{ + ObjectPool: pool.ToProto(), + Origin: testRepo, + } + + _, err = client.CreateObjectPool(ctx, poolReq) + require.NoError(t, err) + defer pool.Remove(ctx) + + // Checks if the underlying repository is valid + require.True(t, pool.IsValid()) + + // No refs + out := testhelper.MustRunCommand(t, nil, "git", "-C", pool.FullPath(), "for-each-ref") + assert.Len(t, out, 0) + + // No remotes + out = testhelper.MustRunCommand(t, nil, "git", "-C", pool.FullPath(), "remote") + assert.Len(t, out, 0) + + // No hooks + _, err = os.Stat(path.Join(pool.FullPath(), "hooks")) + assert.True(t, os.IsNotExist(err)) + + // No problems + out = testhelper.MustRunCommand(t, nil, "git", "-C", pool.FullPath(), "cat-file", "-s", "55bc176024cfa3baaceb71db584c7e5df900ea65") + assert.Equal(t, "282\n", string(out)) + + // Making the same request twice, should result in an error + _, err = client.CreateObjectPool(ctx, poolReq) + require.Error(t, err) + require.True(t, pool.IsValid()) +} + +func TestUnsuccessfulCreate(t *testing.T) { + server, serverSocketPath := runObjectPoolServer(t) + defer server.Stop() + + client, conn := newObjectPoolClient(t, serverSocketPath) + defer conn.Close() + + ctx, cancel := testhelper.Context() + defer cancel() + + testRepo, _, cleanupFn := testhelper.NewTestRepo(t) + defer cleanupFn() + + pool, err := objectpool.NewObjectPool("default", t.Name()) + require.NoError(t, err) + defer pool.Remove(ctx) + + testCases := []struct { + desc string + request *gitalypb.CreateObjectPoolRequest + code codes.Code + }{ + { + desc: "no origin repository", + request: &gitalypb.CreateObjectPoolRequest{ + ObjectPool: pool.ToProto(), + }, + code: codes.InvalidArgument, + }, + { + desc: "no object pool", + request: &gitalypb.CreateObjectPoolRequest{ + Origin: testRepo, + }, + code: codes.InvalidArgument, + }, + } + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + _, err := client.CreateObjectPool(ctx, tc.request) + require.Equal(t, tc.code, helper.GrpcCode(err)) + }) + } +} + +func TestRemove(t *testing.T) { + server, serverSocketPath := runObjectPoolServer(t) + defer server.Stop() + + client, conn := newObjectPoolClient(t, serverSocketPath) + defer conn.Close() + + ctx, cancel := testhelper.Context() + defer cancel() + + testRepo, _, cleanupFn := testhelper.NewTestRepo(t) + defer cleanupFn() + + pool, err := objectpool.NewObjectPool("default", t.Name()) + require.NoError(t, err) + require.NoError(t, pool.Create(ctx, testRepo)) + + req := &gitalypb.DeleteObjectPoolRequest{ + pool.ToProto(), + } + + _, err = client.DeleteObjectPool(ctx, req) + require.NoError(t, err) + + // Removing again should be possible + _, err = client.DeleteObjectPool(ctx, req) + require.NoError(t, err) +} diff --git a/internal/service/objectpool/link.go b/internal/service/objectpool/link.go new file mode 100644 index 000000000..20f0ef9d0 --- /dev/null +++ b/internal/service/objectpool/link.go @@ -0,0 +1,16 @@ +package objectpool + +import ( + "context" + + "gitlab.com/gitlab-org/gitaly-proto/go/gitalypb" + "gitlab.com/gitlab-org/gitaly/internal/helper" +) + +func (s *server) LinkRepositoryToObjectPool(ctx context.Context, req *gitalypb.LinkRepositoryToObjectPoolRequest) (*gitalypb.LinkRepositoryToObjectPoolResponse, error) { + return nil, helper.Unimplemented +} + +func (s *server) UnlinkRepositoryFromObjectPool(ctx context.Context, req *gitalypb.UnlinkRepositoryFromObjectPoolRequest) (*gitalypb.UnlinkRepositoryFromObjectPoolResponse, error) { + return nil, helper.Unimplemented +} diff --git a/internal/service/objectpool/server.go b/internal/service/objectpool/server.go new file mode 100644 index 000000000..e6dbb1c6b --- /dev/null +++ b/internal/service/objectpool/server.go @@ -0,0 +1,12 @@ +package objectpool + +import ( + "gitlab.com/gitlab-org/gitaly-proto/go/gitalypb" +) + +type server struct{} + +// NewServer creates a new instance of a gRPC repo server +func NewServer() gitalypb.ObjectPoolServiceServer { + return &server{} +} diff --git a/internal/service/objectpool/testhelper_test.go b/internal/service/objectpool/testhelper_test.go new file mode 100644 index 000000000..943fd1942 --- /dev/null +++ b/internal/service/objectpool/testhelper_test.go @@ -0,0 +1,56 @@ +package objectpool + +import ( + "net" + "os" + "testing" + "time" + + "gitlab.com/gitlab-org/gitaly-proto/go/gitalypb" + "gitlab.com/gitlab-org/gitaly/internal/testhelper" + "google.golang.org/grpc" + "google.golang.org/grpc/reflection" +) + +func TestMain(m *testing.M) { + os.Exit(testMain(m)) +} + +func testMain(m *testing.M) int { + defer testhelper.MustHaveNoChildProcess() + + return m.Run() +} + +func runObjectPoolServer(t *testing.T) (*grpc.Server, string) { + server := testhelper.NewTestGrpcServer(t, nil, nil) + + serverSocketPath := testhelper.GetTemporaryGitalySocketFileName() + listener, err := net.Listen("unix", serverSocketPath) + if err != nil { + t.Fatal(err) + } + + gitalypb.RegisterObjectPoolServiceServer(server, NewServer()) + reflection.Register(server) + + go server.Serve(listener) + + return server, serverSocketPath +} + +func newObjectPoolClient(t *testing.T, serverSocketPath string) (gitalypb.ObjectPoolServiceClient, *grpc.ClientConn) { + connOpts := []grpc.DialOption{ + grpc.WithInsecure(), + grpc.WithDialer(func(addr string, timeout time.Duration) (net.Conn, error) { + return net.DialTimeout("unix", addr, timeout) + }), + } + + conn, err := grpc.Dial(serverSocketPath, connOpts...) + if err != nil { + t.Fatal(err) + } + + return gitalypb.NewObjectPoolServiceClient(conn), conn +} diff --git a/internal/service/register.go b/internal/service/register.go index 49190bc2b..5314496df 100644 --- a/internal/service/register.go +++ b/internal/service/register.go @@ -8,6 +8,7 @@ import ( "gitlab.com/gitlab-org/gitaly/internal/service/conflicts" "gitlab.com/gitlab-org/gitaly/internal/service/diff" "gitlab.com/gitlab-org/gitaly/internal/service/namespace" + "gitlab.com/gitlab-org/gitaly/internal/service/objectpool" "gitlab.com/gitlab-org/gitaly/internal/service/operations" "gitlab.com/gitlab-org/gitaly/internal/service/ref" "gitlab.com/gitlab-org/gitaly/internal/service/remote" @@ -39,6 +40,7 @@ func RegisterAll(grpcServer *grpc.Server, rubyServer *rubyserver.Server) { gitalypb.RegisterRemoteServiceServer(grpcServer, remote.NewServer(rubyServer)) gitalypb.RegisterServerServiceServer(grpcServer, server.NewServer()) gitalypb.RegisterStorageServiceServer(grpcServer, storage.NewServer()) + gitalypb.RegisterObjectPoolServiceServer(grpcServer, objectpool.NewServer()) healthpb.RegisterHealthServer(grpcServer, health.NewServer()) } |