Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitaly.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJacob Vosmaer <jacob@gitlab.com>2018-11-27 17:30:54 +0300
committerJacob Vosmaer <jacob@gitlab.com>2018-11-27 17:30:54 +0300
commitd5b4072ea07f858aa4d7b9de012c4797039c3e0a (patch)
tree4936684e9bdee624079645e6569858c6df098d76
parent92cc07f67557c4719edd4d83dc2040b646716e4c (diff)
parentfbf23ca6f33e8a5f4531f1bb311a361377223d60 (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.go4
-rw-r--r--internal/git/command.go5
-rw-r--r--internal/git/objectpool/clone.go63
-rw-r--r--internal/git/objectpool/clone_test.go64
-rw-r--r--internal/git/objectpool/link.go17
-rw-r--r--internal/git/objectpool/path.go20
-rw-r--r--internal/git/objectpool/pool.go107
-rw-r--r--internal/git/objectpool/pool_test.go91
-rw-r--r--internal/git/objectpool/remote.go16
-rw-r--r--internal/git/objectpool/remote_test.go27
-rw-r--r--internal/git/repository/repository.go9
-rw-r--r--internal/helper/repo.go6
-rw-r--r--internal/helper/security.go11
-rw-r--r--internal/helper/security_test.go18
-rw-r--r--internal/service/objectpool/create.go59
-rw-r--r--internal/service/objectpool/create_test.go140
-rw-r--r--internal/service/objectpool/link.go16
-rw-r--r--internal/service/objectpool/server.go12
-rw-r--r--internal/service/objectpool/testhelper_test.go56
-rw-r--r--internal/service/register.go2
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())
}