diff options
Diffstat (limited to 'internal/gitaly/service')
-rw-r--r-- | internal/gitaly/service/operations/rebase.go | 45 | ||||
-rw-r--r-- | internal/gitaly/service/operations/rebase_test.go | 60 | ||||
-rw-r--r-- | internal/gitaly/service/repository/archive.go | 16 | ||||
-rw-r--r-- | internal/gitaly/service/smarthttp/server.go | 3 | ||||
-rw-r--r-- | internal/gitaly/service/ssh/receive_pack_test.go | 86 |
5 files changed, 140 insertions, 70 deletions
diff --git a/internal/gitaly/service/operations/rebase.go b/internal/gitaly/service/operations/rebase.go index 30c4438cb..e23a18fe9 100644 --- a/internal/gitaly/service/operations/rebase.go +++ b/internal/gitaly/service/operations/rebase.go @@ -9,6 +9,7 @@ import ( "gitlab.com/gitlab-org/gitaly/v14/internal/git/updateref" "gitlab.com/gitlab-org/gitaly/v14/internal/git2go" "gitlab.com/gitlab-org/gitaly/v14/internal/helper" + "gitlab.com/gitlab-org/gitaly/v14/internal/metadata/featureflag" "gitlab.com/gitlab-org/gitaly/v14/proto/go/gitalypb" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" @@ -67,6 +68,34 @@ func (s *Server) UserRebaseConfirmable(stream gitalypb.OperationService_UserReba SkipEmptyCommits: true, }) if err != nil { + if featureflag.UserRebaseConfirmableImprovedErrorHandling.IsEnabled(ctx) { + var conflictErr git2go.ConflictingFilesError + if errors.As(err, &conflictErr) { + conflictingFiles := make([][]byte, 0, len(conflictErr.ConflictingFiles)) + for _, conflictingFile := range conflictErr.ConflictingFiles { + conflictingFiles = append(conflictingFiles, []byte(conflictingFile)) + } + + detailedErr, err := helper.ErrWithDetails( + helper.ErrFailedPreconditionf("rebasing commits: %w", err), + &gitalypb.UserRebaseConfirmableError{ + Error: &gitalypb.UserRebaseConfirmableError_RebaseConflict{ + RebaseConflict: &gitalypb.MergeConflictError{ + ConflictingFiles: conflictingFiles, + }, + }, + }, + ) + if err != nil { + return helper.ErrInternalf("error details: %w", err) + } + + return detailedErr + } + + return helper.ErrInternalf("rebasing commits: %w", err) + } + return stream.Send(&gitalypb.UserRebaseConfirmableResponse{ GitError: err.Error(), }) @@ -100,6 +129,22 @@ func (s *Server) UserRebaseConfirmable(stream gitalypb.OperationService_UserReba header.GitPushOptions...); err != nil { switch { case errors.As(err, &updateref.HookError{}): + if featureflag.UserRebaseConfirmableImprovedErrorHandling.IsEnabled(ctx) { + detailedErr, err := helper.ErrWithDetails( + helper.ErrPermissionDeniedf("access check: %q", err), + &gitalypb.UserRebaseConfirmableError{ + Error: &gitalypb.UserRebaseConfirmableError_AccessCheck{ + AccessCheck: &gitalypb.AccessCheckError{ + ErrorMessage: err.Error(), + }, + }, + }, + ) + if err != nil { + return helper.ErrInternalf("error details: %w", err) + } + return detailedErr + } return stream.Send(&gitalypb.UserRebaseConfirmableResponse{ PreReceiveError: err.Error(), }) diff --git a/internal/gitaly/service/operations/rebase_test.go b/internal/gitaly/service/operations/rebase_test.go index 1d388ded7..a43f084f3 100644 --- a/internal/gitaly/service/operations/rebase_test.go +++ b/internal/gitaly/service/operations/rebase_test.go @@ -1,6 +1,8 @@ package operations import ( + "context" + "errors" "fmt" "io" "path/filepath" @@ -14,7 +16,9 @@ import ( "gitlab.com/gitlab-org/gitaly/v14/internal/git/localrepo" "gitlab.com/gitlab-org/gitaly/v14/internal/gitaly/config" "gitlab.com/gitlab-org/gitaly/v14/internal/gitaly/transaction" + "gitlab.com/gitlab-org/gitaly/v14/internal/helper" "gitlab.com/gitlab-org/gitaly/v14/internal/metadata" + "gitlab.com/gitlab-org/gitaly/v14/internal/metadata/featureflag" "gitlab.com/gitlab-org/gitaly/v14/internal/testhelper" "gitlab.com/gitlab-org/gitaly/v14/internal/testhelper/testserver" "gitlab.com/gitlab-org/gitaly/v14/internal/transaction/txinfo" @@ -507,8 +511,11 @@ func TestFailedUserRebaseConfirmableDueToApplyBeingFalse(t *testing.T) { func TestFailedUserRebaseConfirmableRequestDueToPreReceiveError(t *testing.T) { t.Parallel() - ctx := testhelper.Context(t) + testhelper.NewFeatureSets(featureflag.UserRebaseConfirmableImprovedErrorHandling). + Run(t, testFailedUserRebaseConfirmableRequestDueToPreReceiveError) +} +func testFailedUserRebaseConfirmableRequestDueToPreReceiveError(t *testing.T, ctx context.Context) { ctx, cfg, repoProto, repoPath, client := setupOperationsService(t, ctx) repo := localrepo.NewTestRepo(t, cfg, repoProto) @@ -541,11 +548,23 @@ func TestFailedUserRebaseConfirmableRequestDueToPreReceiveError(t *testing.T) { secondResponse, err := rebaseStream.Recv() - require.NoError(t, err, "receive second response") - require.Contains(t, secondResponse.PreReceiveError, "failure") - - _, err = rebaseStream.Recv() - require.Equal(t, io.EOF, err) + if featureflag.UserRebaseConfirmableImprovedErrorHandling.IsEnabled(ctx) { + testhelper.RequireGrpcError(t, errWithDetails(t, + helper.ErrPermissionDeniedf(`access check: "failure\n"`), + &gitalypb.UserRebaseConfirmableError{ + Error: &gitalypb.UserRebaseConfirmableError_AccessCheck{ + AccessCheck: &gitalypb.AccessCheckError{ + ErrorMessage: "failure\n", + }, + }, + }, + ), err) + } else { + require.NoError(t, err, "receive second response") + require.Contains(t, secondResponse.PreReceiveError, "failure") + _, err = rebaseStream.Recv() + require.Equal(t, io.EOF, err) + } _, err = repo.ReadCommit(ctx, git.Revision(firstResponse.GetRebaseSha())) if hookName == "pre-receive" { @@ -561,10 +580,13 @@ func TestFailedUserRebaseConfirmableRequestDueToPreReceiveError(t *testing.T) { } } -func TestFailedUserRebaseConfirmableDueToGitError(t *testing.T) { +func TestUserRebaseConfirmable_gitError(t *testing.T) { t.Parallel() - ctx := testhelper.Context(t) + testhelper.NewFeatureSets(featureflag.UserRebaseConfirmableImprovedErrorHandling). + Run(t, testFailedUserRebaseConfirmableDueToGitError) +} +func testFailedUserRebaseConfirmableDueToGitError(t *testing.T, ctx context.Context) { ctx, cfg, repoProto, repoPath, client := setupOperationsService(t, ctx) repoCopyProto, _ := gittest.CreateRepository(ctx, t, cfg, gittest.CreateRepositoryConfig{ @@ -578,14 +600,28 @@ func TestFailedUserRebaseConfirmableDueToGitError(t *testing.T) { require.NoError(t, err) headerRequest := buildHeaderRequest(repoProto, gittest.TestUser, "1", failedBranchName, branchSha, repoCopyProto, "master") + require.NoError(t, rebaseStream.Send(headerRequest), "send header") firstResponse, err := rebaseStream.Recv() - require.NoError(t, err, "receive first response") - require.Contains(t, firstResponse.GitError, "conflict") + if featureflag.UserRebaseConfirmableImprovedErrorHandling.IsEnabled(ctx) { + testhelper.RequireGrpcError(t, errWithDetails(t, + helper.ErrFailedPrecondition(errors.New(`rebasing commits: rebase: commit "eb8f5fb9523b868cef583e09d4bf70b99d2dd404": there are conflicting files`)), + &gitalypb.UserRebaseConfirmableError{ + Error: &gitalypb.UserRebaseConfirmableError_RebaseConflict{ + RebaseConflict: &gitalypb.MergeConflictError{ + ConflictingFiles: [][]byte{[]byte("README.md")}, + }, + }, + }, + ), err) + } else { + require.NoError(t, err, "receive first response") + require.Contains(t, firstResponse.GitError, "conflict") - _, err = rebaseStream.Recv() - require.Equal(t, io.EOF, err) + _, err = rebaseStream.Recv() + require.Equal(t, io.EOF, err) + } newBranchSha := getBranchSha(t, cfg, repoPath, failedBranchName) require.Equal(t, branchSha, newBranchSha, "branch should not change when the rebase fails due to GitError") diff --git a/internal/gitaly/service/repository/archive.go b/internal/gitaly/service/repository/archive.go index cffd4fb36..878572f91 100644 --- a/internal/gitaly/service/repository/archive.go +++ b/internal/gitaly/service/repository/archive.go @@ -2,6 +2,8 @@ package repository import ( "context" + "crypto/sha256" + "encoding/hex" "encoding/json" "fmt" "io" @@ -10,6 +12,7 @@ import ( "path/filepath" "strings" + "github.com/grpc-ecosystem/go-grpc-middleware/logging/logrus/ctxlogrus" "gitlab.com/gitlab-org/gitaly/v14/internal/command" "gitlab.com/gitlab-org/gitaly/v14/internal/git" "gitlab.com/gitlab-org/gitaly/v14/internal/git/catfile" @@ -19,6 +22,7 @@ import ( "gitlab.com/gitlab-org/gitaly/v14/proto/go/gitalypb" "gitlab.com/gitlab-org/gitaly/v14/streamio" "gitlab.com/gitlab-org/labkit/correlation" + "google.golang.org/protobuf/proto" ) type archiveParams struct { @@ -92,6 +96,8 @@ func (s *server) GetArchive(in *gitalypb.GetArchiveRequest, stream gitalypb.Repo return err } + ctxlogrus.Extract(ctx).WithField("request_hash", requestHash(in)).Info("request details") + return s.handleArchive(archiveParams{ ctx: ctx, writer: writer, @@ -243,3 +249,13 @@ func (s *server) handleArchive(p archiveParams) error { return archiveCommand.Wait() } + +func requestHash(req proto.Message) string { + reqBytes, err := proto.Marshal(req) + if err != nil { + return "failed to hash request" + } + + hash := sha256.Sum256(reqBytes) + return hex.EncodeToString(hash[:]) +} diff --git a/internal/gitaly/service/smarthttp/server.go b/internal/gitaly/service/smarthttp/server.go index 62134032a..e1a3c60d0 100644 --- a/internal/gitaly/service/smarthttp/server.go +++ b/internal/gitaly/service/smarthttp/server.go @@ -18,7 +18,8 @@ type server struct { // NewServer creates a new instance of a grpc SmartHTTPServer func NewServer(locator storage.Locator, gitCmdFactory git.CommandFactory, - cache cache.Streamer, serverOpts ...ServerOpt) gitalypb.SmartHTTPServiceServer { + cache cache.Streamer, serverOpts ...ServerOpt, +) gitalypb.SmartHTTPServiceServer { s := &server{ locator: locator, gitCmdFactory: gitCmdFactory, diff --git a/internal/gitaly/service/ssh/receive_pack_test.go b/internal/gitaly/service/ssh/receive_pack_test.go index 90797458d..a810d6766 100644 --- a/internal/gitaly/service/ssh/receive_pack_test.go +++ b/internal/gitaly/service/ssh/receive_pack_test.go @@ -2,6 +2,7 @@ package ssh import ( "bytes" + "context" "fmt" "io" "os" @@ -118,18 +119,16 @@ func TestReceivePackPushSuccess(t *testing.T) { // when deserializing the HooksPayload. By setting all flags to `true` explicitly, we both // verify that gitaly-ssh picks up feature flags correctly and fix the test to behave the // same with and without Praefect. - featureFlags := map[featureflag.FeatureFlag]bool{} for _, featureFlag := range featureflag.All { - featureFlags[featureFlag] = true + ctx = featureflag.ContextWithFeatureFlag(ctx, featureFlag, true) } - lHead, rHead, err := testCloneAndPush(t, cfg, cfg.SocketPath, repo, repoPath, pushParams{ + lHead, rHead, err := testCloneAndPush(ctx, t, cfg, cfg.SocketPath, repo, repoPath, pushParams{ storageName: cfg.Storages[0].Name, glID: "123", glUsername: "user", glRepository: glRepository, glProjectPath: glProjectPath, - featureFlags: featureFlags, }) require.NoError(t, err) require.Equal(t, lHead, rHead, "local and remote head not equal. push failed") @@ -188,7 +187,7 @@ func TestReceivePackPushSuccessWithGitProtocol(t *testing.T) { Seed: gittest.SeedGitLabTest, }) - lHead, rHead, err := testCloneAndPush(t, cfg, cfg.SocketPath, repo, repoPath, pushParams{ + lHead, rHead, err := testCloneAndPush(ctx, t, cfg, cfg.SocketPath, repo, repoPath, pushParams{ storageName: testhelper.DefaultStorageName, glRepository: "project-123", glID: "1", @@ -205,14 +204,16 @@ func TestReceivePackPushSuccessWithGitProtocol(t *testing.T) { func TestReceivePackPushFailure(t *testing.T) { t.Parallel() + ctx := testhelper.Context(t) + cfg, repo, repoPath := testcfg.BuildWithRepo(t) serverSocketPath := runSSHServer(t, cfg) - _, _, err := testCloneAndPush(t, cfg, serverSocketPath, repo, repoPath, pushParams{storageName: "foobar", glID: "1"}) + _, _, err := testCloneAndPush(ctx, t, cfg, serverSocketPath, repo, repoPath, pushParams{storageName: "foobar", glID: "1"}) require.Error(t, err, "local and remote head equal. push did not fail") - _, _, err = testCloneAndPush(t, cfg, serverSocketPath, repo, repoPath, pushParams{storageName: cfg.Storages[0].Name, glID: ""}) + _, _, err = testCloneAndPush(ctx, t, cfg, serverSocketPath, repo, repoPath, pushParams{storageName: cfg.Storages[0].Name, glID: ""}) require.Error(t, err, "local and remote head equal. push did not fail") } @@ -234,7 +235,7 @@ func TestReceivePackPushHookFailure(t *testing.T) { hookContent := []byte("#!/bin/sh\nexit 1") require.NoError(t, os.WriteFile(filepath.Join(gitCmdFactory.HooksPath(ctx), "pre-receive"), hookContent, 0o755)) - _, _, err := testCloneAndPush(t, cfg, cfg.SocketPath, repo, repoPath, pushParams{storageName: cfg.Storages[0].Name, glID: "1"}) + _, _, err := testCloneAndPush(ctx, t, cfg, cfg.SocketPath, repo, repoPath, pushParams{storageName: cfg.Storages[0].Name, glID: "1"}) require.Error(t, err) require.Contains(t, err.Error(), "(pre-receive hook declined)") } @@ -550,7 +551,7 @@ func TestSSHReceivePackToHooks(t *testing.T) { gittest.WriteCheckNewObjectExistsHook(t, cloneDetails.RemoteRepoPath) - lHead, rHead, err := sshPush(t, cfg, cloneDetails, cfg.SocketPath, pushParams{ + lHead, rHead, err := sshPush(ctx, t, cfg, cloneDetails, cfg.SocketPath, pushParams{ storageName: cfg.Storages[0].Name, glID: glID, glRepository: glRepository, @@ -572,18 +573,23 @@ type SSHCloneDetails struct { // setupSSHClone sets up a test clone func setupSSHClone(t *testing.T, cfg config.Cfg, remoteRepo *gitalypb.Repository, remoteRepoPath string) (SSHCloneDetails, func()) { - // Make a non-bare clone of the test repo to act as a local one - _, localRepoPath := gittest.CloneRepo(t, cfg, cfg.Storages[0], gittest.CloneRepoOpts{ - WithWorktree: true, - }) - - // We need git thinking we're pushing over SSH... - oldHead, newHead, success := makeCommit(t, cfg, localRepoPath) - require.True(t, success) + _, localRepoPath := gittest.CloneRepo(t, cfg, cfg.Storages[0]) + + oldHead := text.ChompBytes(gittest.Exec(t, cfg, "-C", localRepoPath, "rev-parse", "HEAD")) + newHead := gittest.WriteCommit(t, cfg, localRepoPath, + gittest.WithMessage(fmt.Sprintf("Testing ReceivePack RPC around %d", time.Now().Unix())), + gittest.WithTreeEntries(gittest.TreeEntry{ + Path: "foo.txt", + Mode: "100644", + Content: "foo bar", + }), + gittest.WithBranch("master"), + gittest.WithParents(git.ObjectID(oldHead)), + ) return SSHCloneDetails{ - OldHead: oldHead, - NewHead: newHead, + OldHead: []byte(oldHead), + NewHead: []byte(newHead.String()), LocalRepoPath: localRepoPath, RemoteRepoPath: remoteRepoPath, TempRepo: remoteRepo.GetRelativePath(), @@ -593,7 +599,7 @@ func setupSSHClone(t *testing.T, cfg config.Cfg, remoteRepo *gitalypb.Repository } } -func sshPush(t *testing.T, cfg config.Cfg, cloneDetails SSHCloneDetails, serverSocketPath string, params pushParams) (string, string, error) { +func sshPush(ctx context.Context, t *testing.T, cfg config.Cfg, cloneDetails SSHCloneDetails, serverSocketPath string, params pushParams) (string, string, error) { pbTempRepo := &gitalypb.Repository{ StorageName: params.storageName, RelativePath: cloneDetails.TempRepo, @@ -610,16 +616,11 @@ func sshPush(t *testing.T, cfg config.Cfg, cloneDetails SSHCloneDetails, serverS }) require.NoError(t, err) - featureFlags := []string{} - for flag, value := range params.featureFlags { - featureFlags = append(featureFlags, fmt.Sprintf("%s:%v", flag.Name, value)) - } - cmd := gittest.NewCommand(t, cfg, "-C", cloneDetails.LocalRepoPath, "push", "-v", "git@localhost:test/test.git", "master") cmd.Env = []string{ fmt.Sprintf("GITALY_PAYLOAD=%s", payload), fmt.Sprintf("GITALY_ADDRESS=%s", serverSocketPath), - fmt.Sprintf("GITALY_FEATUREFLAGS=%s", strings.Join(featureFlags, ",")), + fmt.Sprintf("GITALY_FEATUREFLAGS=%s", strings.Join(featureflag.AllFlags(ctx), ",")), fmt.Sprintf("GIT_SSH_COMMAND=%s receive-pack", filepath.Join(cfg.BinDir, "gitaly-ssh")), } @@ -638,39 +639,11 @@ func sshPush(t *testing.T, cfg config.Cfg, cloneDetails SSHCloneDetails, serverS return string(localHead), string(remoteHead), nil } -func testCloneAndPush(t *testing.T, cfg config.Cfg, serverSocketPath string, remoteRepo *gitalypb.Repository, remoteRepoPath string, params pushParams) (string, string, error) { +func testCloneAndPush(ctx context.Context, t *testing.T, cfg config.Cfg, serverSocketPath string, remoteRepo *gitalypb.Repository, remoteRepoPath string, params pushParams) (string, string, error) { cloneDetails, cleanup := setupSSHClone(t, cfg, remoteRepo, remoteRepoPath) defer cleanup() - return sshPush(t, cfg, cloneDetails, serverSocketPath, params) -} - -// makeCommit creates a new commit and returns oldHead, newHead, success -func makeCommit(t *testing.T, cfg config.Cfg, localRepoPath string) ([]byte, []byte, bool) { - commitMsg := fmt.Sprintf("Testing ReceivePack RPC around %d", time.Now().Unix()) - committerName := "Scrooge McDuck" - committerEmail := "scrooge@mcduck.com" - newFilePath := localRepoPath + "/foo.txt" - - // Create a tiny file and add it to the index - require.NoError(t, os.WriteFile(newFilePath, []byte("foo bar"), 0o644)) - gittest.Exec(t, cfg, "-C", localRepoPath, "add", ".") - - // The latest commit ID on the remote repo - oldHead := bytes.TrimSpace(gittest.Exec(t, cfg, "-C", localRepoPath, "rev-parse", "master")) - - gittest.Exec(t, cfg, "-C", localRepoPath, - "-c", fmt.Sprintf("user.name=%s", committerName), - "-c", fmt.Sprintf("user.email=%s", committerEmail), - "commit", "-m", commitMsg) - if t.Failed() { - return nil, nil, false - } - - // The commit ID we want to push to the remote repo - newHead := bytes.TrimSpace(gittest.Exec(t, cfg, "-C", localRepoPath, "rev-parse", "master")) - - return oldHead, newHead, true + return sshPush(ctx, t, cfg, cloneDetails, serverSocketPath, params) } func drainPostReceivePackResponse(stream gitalypb.SSHService_SSHReceivePackClient) error { @@ -689,5 +662,4 @@ type pushParams struct { glProjectPath string gitConfigOptions []string gitProtocol string - featureFlags map[featureflag.FeatureFlag]bool } |