diff options
-rw-r--r-- | internal/gitaly/service/repository/create_repository_from_bundle_test.go | 302 |
1 files changed, 146 insertions, 156 deletions
diff --git a/internal/gitaly/service/repository/create_repository_from_bundle_test.go b/internal/gitaly/service/repository/create_repository_from_bundle_test.go index 060534544..4f6ce3d66 100644 --- a/internal/gitaly/service/repository/create_repository_from_bundle_test.go +++ b/internal/gitaly/service/repository/create_repository_from_bundle_test.go @@ -19,87 +19,158 @@ import ( "gitlab.com/gitlab-org/gitaly/v16/internal/gitaly/storage" "gitlab.com/gitlab-org/gitaly/v16/internal/gitaly/transaction" "gitlab.com/gitlab-org/gitaly/v16/internal/grpc/metadata" - "gitlab.com/gitlab-org/gitaly/v16/internal/helper/text" "gitlab.com/gitlab-org/gitaly/v16/internal/structerr" - "gitlab.com/gitlab-org/gitaly/v16/internal/tempdir" "gitlab.com/gitlab-org/gitaly/v16/internal/testhelper" "gitlab.com/gitlab-org/gitaly/v16/internal/testhelper/testserver" "gitlab.com/gitlab-org/gitaly/v16/internal/transaction/txinfo" "gitlab.com/gitlab-org/gitaly/v16/internal/transaction/voting" "gitlab.com/gitlab-org/gitaly/v16/proto/go/gitalypb" "gitlab.com/gitlab-org/gitaly/v16/streamio" - "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) -func TestCreateRepositoryFromBundle_successful(t *testing.T) { +func TestCreateRepositoryFromBundle(t *testing.T) { t.Parallel() - ctx := testhelper.Context(t) - - cfg, repo, repoPath, client := setupRepositoryService(t, ctx) - - locator := config.NewLocator(cfg) - tmpdir, err := tempdir.New(ctx, repo.GetStorageName(), locator) - require.NoError(t, err) - bundlePath := filepath.Join(tmpdir.Path(), "original.bundle") - - gittest.Exec(t, cfg, "-C", repoPath, "update-ref", "refs/custom-refs/ref1", "HEAD") - // A user may use a default branch other than "main" or "master" - const wantDefaultBranch = "refs/heads/markdown" - gittest.Exec(t, cfg, "-C", repoPath, "symbolic-ref", "HEAD", wantDefaultBranch) - - gittest.Exec(t, cfg, "-C", repoPath, "bundle", "create", bundlePath, "--all") - defer func() { require.NoError(t, os.RemoveAll(bundlePath)) }() - - stream, err := client.CreateRepositoryFromBundle(ctx) - require.NoError(t, err) + ctx := testhelper.Context(t) + cfg, repoClient := setupRepositoryServiceWithoutRepo(t) - importedRepoProto := &gitalypb.Repository{ - StorageName: repo.GetStorageName(), - RelativePath: "a-repo-from-bundle", + type setupData struct { + repoProto *gitalypb.Repository + bundleData []byte + expectedRefs []git.Reference + expectedErr error } - request := &gitalypb.CreateRepositoryFromBundleRequest{Repository: importedRepoProto} - writer := streamio.NewWriter(func(p []byte) error { - request.Data = p - - if err := stream.Send(request); err != nil { - return err - } - - request = &gitalypb.CreateRepositoryFromBundleRequest{} - - return nil - }) - - file, err := os.Open(bundlePath) - require.NoError(t, err) - defer file.Close() - - _, err = io.Copy(writer, file) - require.NoError(t, err) - - _, err = stream.CloseAndRecv() - require.NoError(t, err) - - importedRepo := localrepo.NewTestRepo(t, cfg, importedRepoProto) - importedRepoPath, err := locator.GetRepoPath(gittest.RewrittenRepository(t, ctx, cfg, importedRepoProto), storage.WithRepositoryVerificationSkipped()) - require.NoError(t, err) - defer func() { require.NoError(t, os.RemoveAll(importedRepoPath)) }() - - gittest.Exec(t, cfg, "-C", importedRepoPath, "fsck") - - _, err = os.Lstat(filepath.Join(importedRepoPath, "hooks")) - require.True(t, os.IsNotExist(err), "hooks directory should not have been created") - - commit, err := importedRepo.ReadCommit(ctx, "refs/custom-refs/ref1") - require.NoError(t, err) - require.NotNil(t, commit) - - gotDefaultBranch, err := importedRepo.HeadReference(ctx) - require.NoError(t, err) - require.Equal(t, wantDefaultBranch, gotDefaultBranch.String()) + for _, tc := range []struct { + desc string + setup func(t *testing.T) setupData + }{ + { + desc: "create repository from bundle", + setup: func(t *testing.T) setupData { + _, bundleRepoPath := gittest.CreateRepository(t, ctx, cfg) + + // Create objects and references that will be used to validate the repository. + oldRef := "refs/heads/old" + newRef := "refs/heads/new" + commitID1 := gittest.WriteCommit(t, cfg, bundleRepoPath, gittest.WithReference(oldRef)) + commitID2 := gittest.WriteCommit(t, cfg, bundleRepoPath, gittest.WithReference(newRef)) + + // Change HEAD to validate the created repository will use the same reference. + gittest.Exec(t, cfg, "-C", bundleRepoPath, "symbolic-ref", "HEAD", newRef) + + // Generate a Git bundle that will be used to create a repository from. + bundleData := gittest.Exec(t, cfg, "-C", bundleRepoPath, "bundle", "create", "-", "--all") + + return setupData{ + repoProto: &gitalypb.Repository{ + StorageName: cfg.Storages[0].Name, + RelativePath: gittest.NewRepositoryName(t), + }, + bundleData: bundleData, + expectedRefs: []git.Reference{ + git.NewSymbolicReference("HEAD", git.ReferenceName(newRef)), + git.NewReference(git.ReferenceName(oldRef), commitID1), + git.NewReference(git.ReferenceName(newRef), commitID2), + }, + } + }, + }, + { + desc: "invalid bundle", + setup: func(t *testing.T) setupData { + return setupData{ + // If an invalid Git bundle is transmitted, the RPC returns an error. + repoProto: &gitalypb.Repository{ + StorageName: cfg.Storages[0].Name, + RelativePath: gittest.NewRepositoryName(t), + }, + bundleData: []byte("not-a-bundle"), + expectedErr: structerr.NewInternal("fatal: invalid gitfile format:"), + } + }, + }, + { + desc: "invalid argument", + setup: func(t *testing.T) setupData { + // If the repository is not specified in the RPC request, an error is returned. + return setupData{ + repoProto: nil, + expectedErr: structerr.NewInvalidArgument("%w", storage.ErrRepositoryNotSet), + } + }, + }, + { + desc: "repo already exists", + setup: func(t *testing.T) setupData { + // If the specified repository already exists a new one can not be created. + repoProto, _ := gittest.CreateRepository(t, ctx, cfg) + + return setupData{ + repoProto: repoProto, + expectedErr: testhelper.GitalyOrPraefect( + structerr.NewAlreadyExists("creating repository: repository exists already"), + structerr.NewAlreadyExists("route repository creation: reserve repository id: repository already exists"), + ), + } + }, + }, + } { + tc := tc + t.Run(tc.desc, func(t *testing.T) { + t.Parallel() + + setup := tc.setup(t) + + stream, err := repoClient.CreateRepositoryFromBundle(ctx) + require.NoError(t, err) + + writer := streamio.NewWriter(func(p []byte) error { + if err := stream.Send(&gitalypb.CreateRepositoryFromBundleRequest{Data: p}); err != nil { + return err + } + return nil + }) + + // Some test cases will not transmit any bundle data. To ensure the first request with + // repository information is received, explicitly send the first request. + require.NoError(t, stream.Send(&gitalypb.CreateRepositoryFromBundleRequest{Repository: setup.repoProto})) + + // If the test case is transmitting Git bundle data, write the content to the stream. + _, err = writer.Write(setup.bundleData) + require.NoError(t, err) + + _, err = stream.CloseAndRecv() + + // It is possible for the returned error to contain metadata embedded in its message + // that makes it difficult to assert equivalency. For this reason, the status code is + // verified, but the error message only asserts it contains a specified substring. + if setup.expectedErr != nil { + testhelper.RequireGrpcCode(t, err, status.Code(setup.expectedErr)) + require.ErrorContains(t, err, setup.expectedErr.Error()) + return + } + require.NoError(t, err) + + repo := localrepo.NewTestRepo(t, cfg, setup.repoProto) + repoPath, err := repo.Path() + require.NoError(t, err) + + // Verify connectivity and validity of the repository objects. + gittest.Exec(t, cfg, "-C", repoPath, "fsck") + + refs, err := repo.GetReferences(ctx) + require.NoError(t, err) + + headRef, err := repo.HeadReference(ctx) + require.NoError(t, err) + head := git.NewSymbolicReference("HEAD", headRef) + + // Verify repository contains references from the bundle. + require.ElementsMatch(t, setup.expectedRefs, append(refs, head)) + }) + } } func TestCreateRepositoryFromBundle_transactional(t *testing.T) { @@ -107,20 +178,21 @@ func TestCreateRepositoryFromBundle_transactional(t *testing.T) { ctx := testhelper.Context(t) txManager := transaction.NewTrackingManager() + cfg, client := setupRepositoryServiceWithoutRepo(t, testserver.WithTransactionManager(txManager)) - cfg, repoProto, repoPath, client := setupRepositoryService(t, ctx, testserver.WithTransactionManager(txManager)) + repoProto, repoPath := gittest.CreateRepository(t, ctx, cfg) // Reset the votes casted while creating the test repository. txManager.Reset() - masterOID := text.ChompBytes(gittest.Exec(t, cfg, "-C", repoPath, "rev-parse", "refs/heads/master")) - featureOID := text.ChompBytes(gittest.Exec(t, cfg, "-C", repoPath, "rev-parse", "refs/heads/feature")) + masterOID := gittest.WriteCommit(t, cfg, repoPath, gittest.WithBranch("master")) + featureOID := gittest.WriteCommit(t, cfg, repoPath, gittest.WithBranch("feature")) // keep-around refs are not cloned in the initial step, but are added via the second call to // git-fetch(1). We thus create some of them to exercise their behaviour with regards to // transactional voting. for _, keepAroundRef := range []string{"refs/keep-around/1", "refs/keep-around/2"} { - gittest.Exec(t, cfg, "-C", repoPath, "update-ref", keepAroundRef, masterOID) + gittest.Exec(t, cfg, "-C", repoPath, "update-ref", keepAroundRef, masterOID.String()) } ctx, err := txinfo.InjectTransaction(ctx, 1, "primary", true) @@ -141,7 +213,6 @@ func TestCreateRepositoryFromBundle_transactional(t *testing.T) { bundle := gittest.Exec(t, cfg, "-C", repoPath, "bundle", "create", "-", "refs/heads/master", "refs/heads/feature", "refs/keep-around/1", "refs/keep-around/2") - require.Greater(t, len(bundle), 100*1024) _, err = io.Copy(streamio.NewWriter(func(p []byte) error { require.NoError(t, stream.Send(&gitalypb.CreateRepositoryFromBundleRequest{ @@ -164,10 +235,10 @@ func TestCreateRepositoryFromBundle_transactional(t *testing.T) { require.NoError(t, err) refsVote := voting.VoteFromData([]byte(strings.Join([]string{ - fmt.Sprintf("%s %s refs/keep-around/2", git.ObjectHashSHA1.ZeroOID, masterOID), - fmt.Sprintf("%s %s refs/keep-around/1", git.ObjectHashSHA1.ZeroOID, masterOID), - fmt.Sprintf("%s %s refs/heads/feature", git.ObjectHashSHA1.ZeroOID, featureOID), - fmt.Sprintf("%s %s refs/heads/master", git.ObjectHashSHA1.ZeroOID, masterOID), + fmt.Sprintf("%s %s refs/keep-around/2", gittest.DefaultObjectHash.ZeroOID, masterOID), + fmt.Sprintf("%s %s refs/keep-around/1", gittest.DefaultObjectHash.ZeroOID, masterOID), + fmt.Sprintf("%s %s refs/heads/feature", gittest.DefaultObjectHash.ZeroOID, featureOID), + fmt.Sprintf("%s %s refs/heads/master", gittest.DefaultObjectHash.ZeroOID, masterOID), }, "\n") + "\n")) // Compute the second vote hash to assert that we really hash exactly the files that we @@ -203,84 +274,3 @@ func TestCreateRepositoryFromBundle_transactional(t *testing.T) { createVote(filesVote.String(), voting.Committed), }, txManager.Votes()) } - -func TestCreateRepositoryFromBundle_invalidBundle(t *testing.T) { - t.Parallel() - - ctx := testhelper.Context(t) - cfg, client := setupRepositoryServiceWithoutRepo(t) - - stream, err := client.CreateRepositoryFromBundle(ctx) - require.NoError(t, err) - - importedRepo := &gitalypb.Repository{ - StorageName: cfg.Storages[0].Name, - RelativePath: "a-repo-from-bundle", - } - importedRepoPath := filepath.Join(cfg.Storages[0].Path, importedRepo.GetRelativePath()) - defer func() { require.NoError(t, os.RemoveAll(importedRepoPath)) }() - - request := &gitalypb.CreateRepositoryFromBundleRequest{Repository: importedRepo} - writer := streamio.NewWriter(func(p []byte) error { - request.Data = p - - if err := stream.Send(request); err != nil { - return err - } - - request = &gitalypb.CreateRepositoryFromBundleRequest{} - - return nil - }) - - _, err = io.Copy(writer, bytes.NewBufferString("not-a-bundle")) - require.NoError(t, err) - - _, err = stream.CloseAndRecv() - require.Error(t, err) - require.Contains(t, err.Error(), "invalid gitfile format") -} - -func TestCreateRepositoryFromBundle_invalidArgument(t *testing.T) { - t.Parallel() - - ctx := testhelper.Context(t) - _, client := setupRepositoryServiceWithoutRepo(t) - - stream, err := client.CreateRepositoryFromBundle(ctx) - require.NoError(t, err) - - require.NoError(t, stream.Send(&gitalypb.CreateRepositoryFromBundleRequest{})) - - _, err = stream.CloseAndRecv() - testhelper.RequireGrpcError(t, structerr.NewInvalidArgument("%w", storage.ErrRepositoryNotSet), err) -} - -func TestCreateRepositoryFromBundle_existingRepository(t *testing.T) { - t.Parallel() - - ctx := testhelper.Context(t) - cfg, client := setupRepositoryServiceWithoutRepo(t) - - // The above test creates the second repository on the server. As this test can run with Praefect in front of it, - // we'll use the next replica path Praefect will assign in order to ensure this repository creation conflicts even - // with Praefect in front of it. - repo, _ := gittest.CreateRepository(t, ctx, cfg, gittest.CreateRepositoryConfig{ - RelativePath: storage.DeriveReplicaPath(1), - Seed: gittest.SeedGitLabTest, - }) - - stream, err := client.CreateRepositoryFromBundle(ctx) - require.NoError(t, err) - - require.NoError(t, stream.Send(&gitalypb.CreateRepositoryFromBundleRequest{ - Repository: repo, - })) - - _, err = stream.CloseAndRecv() - if testhelper.IsPraefectEnabled() { - testhelper.ProtoEqual(t, status.Error(codes.AlreadyExists, "route repository creation: reserve repository id: repository already exists"), err) - } else { - testhelper.ProtoEqual(t, status.Error(codes.AlreadyExists, "creating repository: repository exists already"), err) - } -} |