diff options
author | John Cai <jcai@gitlab.com> | 2023-11-13 22:14:32 +0300 |
---|---|---|
committer | John Cai <jcai@gitlab.com> | 2023-11-16 21:19:23 +0300 |
commit | 848723cc52d0eaec6d992a27bfe58754bc4e5847 (patch) | |
tree | cb456ae9ce8470ab08ed70d83c97d6cf95940f7c | |
parent | 40b162d825dbc7e1f8c88246b6af47580769e2d6 (diff) |
internalgitaly: Implement RunCommand RPCjc/run-command-rpc
Implement the RunCommand RPC, which can be used to execute an arbitrary
Git command on a given repository.
4 files changed, 180 insertions, 64 deletions
diff --git a/internal/gitaly/service/internalgitaly/execute_command.go b/internal/gitaly/service/internalgitaly/execute_command.go deleted file mode 100644 index 278de303e..000000000 --- a/internal/gitaly/service/internalgitaly/execute_command.go +++ /dev/null @@ -1,58 +0,0 @@ -package internalgitaly - -import ( - "bytes" - "context" - - "gitlab.com/gitlab-org/gitaly/v16/internal/command" - "gitlab.com/gitlab-org/gitaly/v16/internal/structerr" - "gitlab.com/gitlab-org/gitaly/v16/proto/go/gitalypb" -) - -func (s *server) ExecuteCommand( - ctx context.Context, - req *gitalypb.ExecuteCommandRequest, -) (*gitalypb.ExecuteCommandResponse, error) { - repo := req.GetRepository() - - if repo == nil { - return nil, structerr.NewInvalidArgument("repository cannot be empty") - } - - repoPath, err := s.locator.GetRepoPath(repo) - if err != nil { - return nil, structerr.NewInternal("error getting repo path %w", err) - } - - var stdout, stderr bytes.Buffer - - cmd, err := command.New( - ctx, - s.logger, - req.GetArgs(), - command.WithStdout(&stdout), - command.WithStderr(&stderr), - command.WithDir(repoPath), - ) - if err != nil { - return nil, structerr.NewInternal("error creating command: %w", err) - } - - if err := cmd.Wait(); err != nil { - exitCode, found := command.ExitStatus(err) - if found { - return &gitalypb.ExecuteCommandResponse{ - ReturnCode: int32(exitCode), - Output: stdout.Bytes(), - ErrorOutput: stderr.Bytes(), - }, nil - } - - return nil, structerr.NewInternal("error running command: %w", err) - } - - return &gitalypb.ExecuteCommandResponse{ - Output: stdout.Bytes(), - ErrorOutput: stderr.Bytes(), - }, nil -} diff --git a/internal/gitaly/service/internalgitaly/run_command.go b/internal/gitaly/service/internalgitaly/run_command.go new file mode 100644 index 000000000..abea2c6ec --- /dev/null +++ b/internal/gitaly/service/internalgitaly/run_command.go @@ -0,0 +1,62 @@ +package internalgitaly + +import ( + "bytes" + "context" + + "gitlab.com/gitlab-org/gitaly/v16/internal/command" + "gitlab.com/gitlab-org/gitaly/v16/internal/git" + "gitlab.com/gitlab-org/gitaly/v16/internal/helper/text" + "gitlab.com/gitlab-org/gitaly/v16/internal/structerr" + "gitlab.com/gitlab-org/gitaly/v16/proto/go/gitalypb" +) + +func (s *server) RunCommand( + ctx context.Context, + req *gitalypb.RunCommandRequest, +) (*gitalypb.RunCommandResponse, error) { + repo := req.GetRepository() + gitCmdParams := req.GetGitCommand() + + if repo == nil { + return nil, structerr.NewInvalidArgument("repository cannot be empty") + } + + var flags []git.Option + + for _, flag := range gitCmdParams.GetFlags() { + flags = append(flags, &git.Flag{Name: flag}) + } + + var stdout, stderr bytes.Buffer + + cmd, err := s.gitCmdFactory.New(ctx, repo, git.Command{ + Name: gitCmdParams.GetName(), + Action: gitCmdParams.GetAction(), + Flags: flags, + Args: gitCmdParams.GetArgs(), + PostSepArgs: gitCmdParams.GetPostSeparatorArgs(), + }, git.WithStdout(&stdout), git.WithStderr(&stderr), + ) + if err != nil { + return nil, structerr.NewInternal("error creating command: %w", err) + } + + if err := cmd.Wait(); err != nil { + exitCode, found := command.ExitStatus(err) + if found { + return &gitalypb.RunCommandResponse{ + ReturnCode: int32(exitCode), + Output: stdout.Bytes(), + ErrorOutput: stderr.Bytes(), + }, nil + } + + return nil, structerr.NewInternal("error running command: %w", err) + } + + return &gitalypb.RunCommandResponse{ + Output: []byte(text.ChompBytes(stdout.Bytes())), + ErrorOutput: []byte(text.ChompBytes(stderr.Bytes())), + }, nil +} diff --git a/internal/gitaly/service/internalgitaly/run_command_test.go b/internal/gitaly/service/internalgitaly/run_command_test.go new file mode 100644 index 000000000..6e56ee7d4 --- /dev/null +++ b/internal/gitaly/service/internalgitaly/run_command_test.go @@ -0,0 +1,109 @@ +package internalgitaly + +import ( + "testing" + + "github.com/stretchr/testify/require" + "gitlab.com/gitlab-org/gitaly/v16/internal/git" + "gitlab.com/gitlab-org/gitaly/v16/internal/git/gittest" + "gitlab.com/gitlab-org/gitaly/v16/internal/git/localrepo" + "gitlab.com/gitlab-org/gitaly/v16/internal/gitaly/config" + "gitlab.com/gitlab-org/gitaly/v16/internal/gitaly/service" + "gitlab.com/gitlab-org/gitaly/v16/internal/testhelper" + "gitlab.com/gitlab-org/gitaly/v16/internal/testhelper/testcfg" + "gitlab.com/gitlab-org/gitaly/v16/proto/go/gitalypb" +) + +func TestRunCommand(t *testing.T) { + t.Parallel() + + ctx := testhelper.Context(t) + cfg := testcfg.Build(t) + + testRepo, testRepoPath := gittest.CreateRepository(t, ctx, cfg, gittest.CreateRepositoryConfig{ + SkipCreationViaService: true, + RelativePath: "a", + }) + + testLocalRepo := localrepo.NewTestRepo(t, cfg, testRepo) + + commitID := gittest.WriteCommit( + t, + cfg, + testRepoPath, + gittest.WithMessage("hello world"), + gittest.WithBranch(git.DefaultBranch), + gittest.WithTreeEntries( + gittest.TreeEntry{ + Mode: "100644", + Path: ".gitattributes", + Content: "a/b/c foo=bar", + }, + ), + ) + commitData, err := testLocalRepo.ReadObject(ctx, commitID) + require.NoError(t, err) + + srv := NewServer(&service.Dependencies{ + Logger: testhelper.SharedLogger(t), + Cfg: cfg, + StorageLocator: config.NewLocator(cfg), + GitCmdFactory: gittest.NewCommandFactory(t, cfg), + }) + + client := setupInternalGitalyService(t, cfg, srv) + + testCases := []struct { + desc string + req *gitalypb.RunCommandRequest + expectedExitStatus int32 + expectedOutput []byte + }{ + { + desc: "git cat-file", + req: &gitalypb.RunCommandRequest{ + Repository: testRepo, + GitCommand: &gitalypb.GitCommand{ + Name: "cat-file", + Flags: []string{"-p"}, + Args: []string{git.DefaultBranch}, + }, + }, + expectedExitStatus: 0, + expectedOutput: commitData, + }, + { + desc: "reading a non-existent object", + req: &gitalypb.RunCommandRequest{ + Repository: testRepo, + GitCommand: &gitalypb.GitCommand{ + Name: "cat-file", + Flags: []string{"-p"}, + Args: []string{"does-not-exist"}, + }, + }, + expectedExitStatus: 128, + }, + { + desc: "attributes", + req: &gitalypb.RunCommandRequest{ + Repository: testRepo, + GitCommand: &gitalypb.GitCommand{ + Name: "check-attr", + Flags: []string{"--source=HEAD"}, + Args: []string{"foo"}, + PostSeparatorArgs: []string{"a/b/c"}, + }, + }, + expectedExitStatus: 0, + expectedOutput: []byte("a/b/c: foo: bar"), + }, + } + + for _, tc := range testCases { + resp, err := client.RunCommand(ctx, tc.req) + require.NoError(t, err) + require.Equal(t, tc.expectedExitStatus, resp.ReturnCode) + require.Equal(t, tc.expectedOutput, resp.Output) + } +} diff --git a/internal/gitaly/service/internalgitaly/server.go b/internal/gitaly/service/internalgitaly/server.go index a275ae252..78cf9cb2e 100644 --- a/internal/gitaly/service/internalgitaly/server.go +++ b/internal/gitaly/service/internalgitaly/server.go @@ -1,6 +1,7 @@ package internalgitaly import ( + "gitlab.com/gitlab-org/gitaly/v16/internal/git" "gitlab.com/gitlab-org/gitaly/v16/internal/gitaly/config" "gitlab.com/gitlab-org/gitaly/v16/internal/gitaly/service" "gitlab.com/gitlab-org/gitaly/v16/internal/gitaly/storage" @@ -10,16 +11,18 @@ import ( type server struct { gitalypb.UnimplementedInternalGitalyServer - logger log.Logger - storages []config.Storage - locator storage.Locator + logger log.Logger + storages []config.Storage + locator storage.Locator + gitCmdFactory git.CommandFactory } // NewServer return an instance of the Gitaly service. func NewServer(deps *service.Dependencies) gitalypb.InternalGitalyServer { return &server{ - logger: deps.GetLogger(), - storages: deps.GetCfg().Storages, - locator: deps.GetLocator(), + logger: deps.GetLogger(), + storages: deps.GetCfg().Storages, + locator: deps.GetLocator(), + gitCmdFactory: deps.GetGitCmdFactory(), } } |