diff options
author | Kim Carlbäcker <kim.carlbacker@gmail.com> | 2017-09-12 18:43:28 +0300 |
---|---|---|
committer | Kim Carlbäcker <kim.carlbacker@gmail.com> | 2017-09-12 18:43:28 +0300 |
commit | e3a261ed820c2d407229ecf2b8aeb97a2087f6fb (patch) | |
tree | 158b388cd476fa01830c7b14185dfd080efad83c | |
parent | afd5929cad5a99628ba7c667eaf21876d5ee9b88 (diff) | |
parent | f731bd65927487d1a71d5518664915b45f251c56 (diff) |
Merge branch 'zj-namespace-crud' into 'master'
Namespace CRUD methods
Closes #552
See merge request !340
-rw-r--r-- | internal/helper/repo.go | 17 | ||||
-rw-r--r-- | internal/service/namespace/namespace.go | 100 | ||||
-rw-r--r-- | internal/service/namespace/namespace_test.go | 284 | ||||
-rw-r--r-- | internal/service/namespace/server.go | 12 | ||||
-rw-r--r-- | internal/service/namespace/testhelper_test.go | 47 | ||||
-rw-r--r-- | internal/service/register.go | 4 |
6 files changed, 461 insertions, 3 deletions
diff --git a/internal/helper/repo.go b/internal/helper/repo.go index 9e4999248..d0badd3be 100644 --- a/internal/helper/repo.go +++ b/internal/helper/repo.go @@ -40,9 +40,9 @@ func GetRepoPath(repo *pb.Repository) (string, error) { // returned when either the storage can't be found or the path includes // constructs trying to perform directory traversal. func GetPath(repo *pb.Repository) (string, error) { - storagePath, ok := config.StoragePath(repo.GetStorageName()) - if !ok { - return "", grpc.Errorf(codes.InvalidArgument, "GetPath: invalid storage name '%s'", repo.GetStorageName()) + storagePath, err := GetStorageByName(repo.GetStorageName()) + if err != nil { + return "", err } if _, err := os.Stat(storagePath); err != nil { @@ -66,6 +66,17 @@ func GetPath(repo *pb.Repository) (string, error) { return path.Join(storagePath, relativePath), nil } +// GetStorageByName will return the path for the storage, which is fetched by +// its key. An error is return if it cannot be found. +func GetStorageByName(storageName string) (string, error) { + storagePath, ok := config.StoragePath(storageName) + if !ok { + return "", grpc.Errorf(codes.InvalidArgument, "Storage can not be found by name '%s'", storageName) + } + + return storagePath, nil +} + // IsGitDirectory checks if the directory passed as first argument looks like // a valid git directory. func IsGitDirectory(dir string) bool { diff --git a/internal/service/namespace/namespace.go b/internal/service/namespace/namespace.go new file mode 100644 index 000000000..0f8c6d645 --- /dev/null +++ b/internal/service/namespace/namespace.go @@ -0,0 +1,100 @@ +package namespace + +import ( + "os" + "path" + + pb "gitlab.com/gitlab-org/gitaly-proto/go" + "gitlab.com/gitlab-org/gitaly/internal/helper" + "golang.org/x/net/context" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" +) + +var noNameError = grpc.Errorf(codes.InvalidArgument, "Name: cannot be empty") + +func (s *server) NamespaceExists(ctx context.Context, in *pb.NamespaceExistsRequest) (*pb.NamespaceExistsResponse, error) { + storagePath, err := helper.GetStorageByName(in.GetStorageName()) + if err != nil { + return nil, err + } + + // This case should return an error, as else we'd actually say the path exists as the + // storage exists + if in.GetName() == "" { + return nil, noNameError + } + + if fi, err := os.Stat(namespacePath(storagePath, in.GetName())); os.IsNotExist(err) { + return &pb.NamespaceExistsResponse{Exists: false}, nil + } else if err != nil { + return nil, grpc.Errorf(codes.Internal, "could not stat the directory: %v", err) + } else { + return &pb.NamespaceExistsResponse{Exists: fi.IsDir()}, nil + } +} + +func (s *server) AddNamespace(ctx context.Context, in *pb.AddNamespaceRequest) (*pb.AddNamespaceResponse, error) { + storagePath, err := helper.GetStorageByName(in.GetStorageName()) + if err != nil { + return nil, err + } + + // Make idempotent, as it's called through Sidekiq + // Exists check will return an err if in.GetName() == "" + existsRequest := &pb.NamespaceExistsRequest{StorageName: in.StorageName, Name: in.Name} + if exists, err := s.NamespaceExists(ctx, existsRequest); err != nil { + return nil, err + } else if exists.Exists { + return &pb.AddNamespaceResponse{}, nil + } + + if err = os.MkdirAll(namespacePath(storagePath, in.GetName()), 0770); err != nil { + return nil, grpc.Errorf(codes.Internal, "create directory: %v", err) + } + + return &pb.AddNamespaceResponse{}, nil +} + +func (s *server) RenameNamespace(ctx context.Context, in *pb.RenameNamespaceRequest) (*pb.RenameNamespaceResponse, error) { + storagePath, err := helper.GetStorageByName(in.GetStorageName()) + if err != nil { + return nil, err + } + + if in.GetFrom() == "" || in.GetTo() == "" { + return nil, grpc.Errorf(codes.InvalidArgument, "from and to cannot be empty") + } + + err = os.Rename(namespacePath(storagePath, in.GetFrom()), namespacePath(storagePath, in.GetTo())) + if _, ok := err.(*os.LinkError); ok { + return nil, grpc.Errorf(codes.InvalidArgument, "from directory not found") + } else if err != nil { + return nil, grpc.Errorf(codes.Internal, "rename: %v", err) + } + + return &pb.RenameNamespaceResponse{}, nil +} + +func (s *server) RemoveNamespace(ctx context.Context, in *pb.RemoveNamespaceRequest) (*pb.RemoveNamespaceResponse, error) { + storagePath, err := helper.GetStorageByName(in.GetStorageName()) + if err != nil { + return nil, err + } + + // Needed as else we might destroy the whole storage + if in.GetName() == "" { + return nil, noNameError + } + + // os.RemoveAll is idempotent by itself + // No need to check if the directory exists, or not + if err = os.RemoveAll(namespacePath(storagePath, in.GetName())); err != nil { + return nil, grpc.Errorf(codes.Internal, "removal: %v", err) + } + return &pb.RemoveNamespaceResponse{}, nil +} + +func namespacePath(storage, ns string) string { + return path.Join(storage, ns) +} diff --git a/internal/service/namespace/namespace_test.go b/internal/service/namespace/namespace_test.go new file mode 100644 index 000000000..ca8875ddc --- /dev/null +++ b/internal/service/namespace/namespace_test.go @@ -0,0 +1,284 @@ +package namespace + +import ( + "io/ioutil" + "os" + "testing" + + "github.com/stretchr/testify/require" + pb "gitlab.com/gitlab-org/gitaly-proto/go" + "gitlab.com/gitlab-org/gitaly/internal/config" + "gitlab.com/gitlab-org/gitaly/internal/testhelper" + "golang.org/x/net/context" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" +) + +func TestMain(m *testing.M) { + os.Exit(testMain(m)) +} + +func testMain(m *testing.M) int { + storageOtherDir, _ := ioutil.TempDir("", "gitaly-repository-exists-test") + defer os.Remove(storageOtherDir) + + oldStorages := config.Config.Storages + config.Config.Storages = []config.Storage{ + {Name: "default", Path: testhelper.GitlabTestStoragePath()}, + {Name: "other", Path: storageOtherDir}, + } + defer func() { config.Config.Storages = oldStorages }() + + return m.Run() +} + +func TestNamespaceExists(t *testing.T) { + server := runNamespaceServer(t) + defer server.Stop() + + client, conn := newNamespaceClient(t) + defer conn.Close() + + // Create one namespace for testing it exists + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + _, err := client.AddNamespace(ctx, &pb.AddNamespaceRequest{StorageName: "default", Name: "existing"}) + require.NoError(t, err, "Namespace creation failed") + defer client.RemoveNamespace(ctx, &pb.RemoveNamespaceRequest{StorageName: "default", Name: "existing"}) + + queries := []struct { + desc string + request *pb.NamespaceExistsRequest + errorCode codes.Code + exists bool + }{ + { + desc: "empty name", + request: &pb.NamespaceExistsRequest{ + StorageName: "default", + Name: "", + }, + errorCode: codes.InvalidArgument, + }, + { + desc: "Namespace doesn't exists", + request: &pb.NamespaceExistsRequest{ + StorageName: "default", + Name: "not-existing", + }, + errorCode: codes.OK, + exists: false, + }, + { + desc: "Wrong storage path", + request: &pb.NamespaceExistsRequest{ + StorageName: "other", + Name: "existing", + }, + errorCode: codes.OK, + exists: false, + }, + { + desc: "Namespace exists", + request: &pb.NamespaceExistsRequest{ + StorageName: "default", + Name: "existing", + }, + errorCode: codes.OK, + exists: true, + }, + } + + for _, tc := range queries { + t.Run(tc.desc, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + response, err := client.NamespaceExists(ctx, tc.request) + + require.Equal(t, tc.errorCode, grpc.Code(err)) + + if tc.errorCode == codes.OK { + require.Equal(t, tc.exists, response.Exists) + } + }) + } +} + +func TestAddNamespace(t *testing.T) { + server := runNamespaceServer(t) + defer server.Stop() + + client, conn := newNamespaceClient(t) + defer conn.Close() + + queries := []struct { + desc string + request *pb.AddNamespaceRequest + errorCode codes.Code + }{ + { + desc: "No name", + request: &pb.AddNamespaceRequest{ + StorageName: "default", + Name: "", + }, + errorCode: codes.InvalidArgument, + }, + { + desc: "Namespace is successfully created", + request: &pb.AddNamespaceRequest{ + StorageName: "default", + Name: "create-me", + }, + errorCode: codes.OK, + }, + { + desc: "Idempotent on creation", + request: &pb.AddNamespaceRequest{ + StorageName: "default", + Name: "create-me", + }, + errorCode: codes.OK, + }, + { + desc: "no storage", + request: &pb.AddNamespaceRequest{ + StorageName: "", + Name: "mepmep", + }, + errorCode: codes.InvalidArgument, + }, + } + + for _, tc := range queries { + t.Run(tc.desc, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + _, err := client.AddNamespace(ctx, tc.request) + + require.Equal(t, tc.errorCode, grpc.Code(err)) + + // Clean up + if tc.errorCode == codes.OK { + client.RemoveNamespace(ctx, &pb.RemoveNamespaceRequest{ + StorageName: tc.request.StorageName, + Name: tc.request.Name, + }) + } + }) + } +} + +func TestRemoveNamespace(t *testing.T) { + server := runNamespaceServer(t) + defer server.Stop() + + client, conn := newNamespaceClient(t) + defer conn.Close() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + queries := []struct { + desc string + request *pb.RemoveNamespaceRequest + errorCode codes.Code + }{ + { + desc: "Namespace is successfully removed", + request: &pb.RemoveNamespaceRequest{ + StorageName: "default", + Name: "created", + }, + errorCode: codes.OK, + }, + { + desc: "Idempotent on deletion", + request: &pb.RemoveNamespaceRequest{ + StorageName: "default", + Name: "not-there", + }, + errorCode: codes.OK, + }, + { + desc: "no storage", + request: &pb.RemoveNamespaceRequest{ + StorageName: "", + Name: "mepmep", + }, + errorCode: codes.InvalidArgument, + }, + } + + for _, tc := range queries { + t.Run(tc.desc, func(t *testing.T) { + _, err := client.AddNamespace(ctx, &pb.AddNamespaceRequest{StorageName: "default", Name: "created"}) + require.NoError(t, err, "setup failed") + + _, err = client.RemoveNamespace(ctx, tc.request) + require.Equal(t, tc.errorCode, grpc.Code(err)) + }) + } +} + +func TestRenameNamespace(t *testing.T) { + server := runNamespaceServer(t) + defer server.Stop() + + client, conn := newNamespaceClient(t) + defer conn.Close() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + queries := []struct { + desc string + request *pb.RenameNamespaceRequest + errorCode codes.Code + }{ + { + desc: "Renaming an existing namespace", + request: &pb.RenameNamespaceRequest{ + From: "existing", + To: "new-path", + StorageName: "default", + }, + errorCode: codes.OK, + }, + { + desc: "No from given", + request: &pb.RenameNamespaceRequest{ + From: "", + To: "new-path", + StorageName: "default", + }, + errorCode: codes.InvalidArgument, + }, + { + desc: "non-existing namespace", + request: &pb.RenameNamespaceRequest{ + From: "non-existing", + To: "new-path", + StorageName: "default", + }, + errorCode: codes.InvalidArgument, + }, + } + + _, err := client.AddNamespace(ctx, &pb.AddNamespaceRequest{"default", "existing"}) + require.NoError(t, err) + + for _, tc := range queries { + t.Run(tc.desc, func(t *testing.T) { + _, err := client.RenameNamespace(ctx, tc.request) + + require.Equal(t, tc.errorCode, grpc.Code(err)) + + if tc.errorCode == codes.OK { + client.RemoveNamespace(ctx, &pb.RemoveNamespaceRequest{"default", "new-path"}) + } + }) + } +} diff --git a/internal/service/namespace/server.go b/internal/service/namespace/server.go new file mode 100644 index 000000000..5735feb37 --- /dev/null +++ b/internal/service/namespace/server.go @@ -0,0 +1,12 @@ +package namespace + +import ( + pb "gitlab.com/gitlab-org/gitaly-proto/go" +) + +type server struct{} + +// NewServer creates a new instance of a gRPC namespace server +func NewServer() pb.NamespaceServiceServer { + return &server{} +} diff --git a/internal/service/namespace/testhelper_test.go b/internal/service/namespace/testhelper_test.go new file mode 100644 index 000000000..a276c533b --- /dev/null +++ b/internal/service/namespace/testhelper_test.go @@ -0,0 +1,47 @@ +package namespace + +import ( + "net" + "testing" + "time" + + pb "gitlab.com/gitlab-org/gitaly-proto/go" + "gitlab.com/gitlab-org/gitaly/internal/testhelper" + + "google.golang.org/grpc" + "google.golang.org/grpc/reflection" +) + +var ( + serverSocketPath = testhelper.GetTemporaryGitalySocketFileName() +) + +func runNamespaceServer(t *testing.T) *grpc.Server { + server := testhelper.NewTestGrpcServer(t, nil, nil) + listener, err := net.Listen("unix", serverSocketPath) + if err != nil { + t.Fatal(err) + } + + pb.RegisterNamespaceServiceServer(server, NewServer()) + reflection.Register(server) + + go server.Serve(listener) + + return server +} + +func newNamespaceClient(t *testing.T) (pb.NamespaceServiceClient, *grpc.ClientConn) { + connOpts := []grpc.DialOption{ + grpc.WithInsecure(), + grpc.WithDialer(func(addr string, _ time.Duration) (net.Conn, error) { + return net.Dial("unix", addr) + }), + } + conn, err := grpc.Dial(serverSocketPath, connOpts...) + if err != nil { + t.Fatal(err) + } + + return pb.NewNamespaceServiceClient(conn), conn +} diff --git a/internal/service/register.go b/internal/service/register.go index 140bf5113..6c708215c 100644 --- a/internal/service/register.go +++ b/internal/service/register.go @@ -6,6 +6,7 @@ import ( "gitlab.com/gitlab-org/gitaly/internal/service/blob" "gitlab.com/gitlab-org/gitaly/internal/service/commit" "gitlab.com/gitlab-org/gitaly/internal/service/diff" + "gitlab.com/gitlab-org/gitaly/internal/service/namespace" "gitlab.com/gitlab-org/gitaly/internal/service/notifications" "gitlab.com/gitlab-org/gitaly/internal/service/ref" "gitlab.com/gitlab-org/gitaly/internal/service/renameadapter" @@ -45,6 +46,9 @@ func RegisterAll(grpcServer *grpc.Server, rubyServer *rubyserver.Server) { repositoryService := repository.NewServer() pb.RegisterRepositoryServiceServer(grpcServer, repositoryService) + namespaceService := namespace.NewServer() + pb.RegisterNamespaceServiceServer(grpcServer, namespaceService) + // Deprecated Services pb.RegisterNotificationsServer(grpcServer, renameadapter.NewNotificationAdapter(notificationsService)) pb.RegisterRefServer(grpcServer, renameadapter.NewRefAdapter(refService)) |