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:
authorJohn Cai <jcai@gitlab.com>2020-06-17 00:03:25 +0300
committerJohn Cai <jcai@gitlab.com>2020-07-16 00:32:07 +0300
commit0d9c6f245938ea92c8de107fb51ad480ac5683fc (patch)
treea4ba70f7007294c4725cc937040b172c0bcda1df
parent1f8d26cf17170ece575270e65f298a03cd14d7ae (diff)
PostReceiveHook in Go
-rw-r--r--changelogs/unreleased/jc-go-post-receive.yml5
-rw-r--r--cmd/gitaly-hooks/hooks.go8
-rw-r--r--cmd/gitaly-hooks/hooks_test.go4
-rw-r--r--cmd/gitaly-ssh/auth_test.go2
-rw-r--r--internal/git/receivepack.go1
-rw-r--r--internal/metadata/featureflag/feature_flags.go8
-rw-r--r--internal/praefect/replicator_test.go3
-rw-r--r--internal/service/hook/access.go67
-rw-r--r--internal/service/hook/post_receive.go177
-rw-r--r--internal/service/hook/post_receive_test.go173
-rw-r--r--internal/service/hook/testhelper_test.go2
-rw-r--r--internal/service/operations/rebase_test.go11
-rw-r--r--internal/service/operations/tags_test.go10
-rw-r--r--internal/service/operations/testhelper_test.go2
-rw-r--r--internal/service/smarthttp/receive_pack_test.go21
-rw-r--r--internal/testhelper/testhelper.go15
-rw-r--r--internal/testhelper/testserver.go50
-rw-r--r--ruby/lib/gitlab/git/hook.rb3
18 files changed, 525 insertions, 37 deletions
diff --git a/changelogs/unreleased/jc-go-post-receive.yml b/changelogs/unreleased/jc-go-post-receive.yml
new file mode 100644
index 000000000..8caf53e22
--- /dev/null
+++ b/changelogs/unreleased/jc-go-post-receive.yml
@@ -0,0 +1,5 @@
+---
+title: PostReceiveHook in Go
+merge_request: 2290
+author:
+type: added
diff --git a/cmd/gitaly-hooks/hooks.go b/cmd/gitaly-hooks/hooks.go
index 0d59f334d..306fa8bd5 100644
--- a/cmd/gitaly-hooks/hooks.go
+++ b/cmd/gitaly-hooks/hooks.go
@@ -138,9 +138,15 @@ func main() {
logger.Fatalf("error when getting stream client for %q: %v", subCmd, err)
}
+ environment := glValues()
+
+ if os.Getenv(featureflag.GoPostReceiveHookEnvVar) == "true" {
+ environment = append(environment, fmt.Sprintf("%s=true", featureflag.GoPostReceiveHookEnvVar))
+ }
+
if err := postReceiveHookStream.Send(&gitalypb.PostReceiveHookRequest{
Repository: repository,
- EnvironmentVariables: glValues(),
+ EnvironmentVariables: environment,
GitPushOptions: gitPushOptions(),
}); err != nil {
logger.Fatalf("error when sending request for %q: %v", subCmd, err)
diff --git a/cmd/gitaly-hooks/hooks_test.go b/cmd/gitaly-hooks/hooks_test.go
index 58fb0e6d5..157685dfe 100644
--- a/cmd/gitaly-hooks/hooks_test.go
+++ b/cmd/gitaly-hooks/hooks_test.go
@@ -117,7 +117,7 @@ func TestHooksPrePostReceive(t *testing.T) {
hookNames := []string{"pre-receive", "post-receive"}
- featureSets, err := testhelper.NewFeatureSets([]featureflag.FeatureFlag{featureflag.GoPreReceiveHook})
+ featureSets, err := testhelper.NewFeatureSets([]featureflag.FeatureFlag{featureflag.GoPreReceiveHook, featureflag.GoPostReceiveHook})
require.NoError(t, err)
for _, hookName := range hookNames {
@@ -562,7 +562,7 @@ func TestCheckBadCreds(t *testing.T) {
}
func runHookServiceServer(t *testing.T, token string) (string, func()) {
- return runHookServiceServerWithAPI(t, token, testhelper.GitlabAPIStub)
+ return runHookServiceServerWithAPI(t, token, hook.GitlabAPIStub)
}
func runHookServiceServerWithAPI(t *testing.T, token string, gitlabAPI hook.GitlabAPI) (string, func()) {
diff --git a/cmd/gitaly-ssh/auth_test.go b/cmd/gitaly-ssh/auth_test.go
index b4d10ffaa..6206aede4 100644
--- a/cmd/gitaly-ssh/auth_test.go
+++ b/cmd/gitaly-ssh/auth_test.go
@@ -118,7 +118,7 @@ func TestConnectivity(t *testing.T) {
}
func runServer(t *testing.T, newServer func(rubyServer *rubyserver.Server, gitlabAPI hook.GitlabAPI, cfg config.Cfg) *grpc.Server, cfg config.Cfg, connectionType string, addr string) (*grpc.Server, int) {
- srv := newServer(nil, testhelper.GitlabAPIStub, cfg)
+ srv := newServer(nil, hook.GitlabAPIStub, cfg)
listener, err := net.Listen(connectionType, addr)
require.NoError(t, err)
diff --git a/internal/git/receivepack.go b/internal/git/receivepack.go
index 376c940dc..b1e8b54ab 100644
--- a/internal/git/receivepack.go
+++ b/internal/git/receivepack.go
@@ -49,6 +49,7 @@ func ReceivePackHookEnv(ctx context.Context, req ReceivePackRequest) ([]string,
fmt.Sprintf("GITALY_TOKEN=%s", config.Config.Auth.Token),
fmt.Sprintf("%s=%s", featureflag.GoUpdateHookEnvVar, strconv.FormatBool(featureflag.IsEnabled(ctx, featureflag.GoUpdateHook))),
fmt.Sprintf("%s=%s", featureflag.GoPreReceiveHookEnvVar, strconv.FormatBool(featureflag.IsEnabled(ctx, featureflag.GoPreReceiveHook))),
+ fmt.Sprintf("%s=%s", featureflag.GoPostReceiveHookEnvVar, strconv.FormatBool(featureflag.IsEnabled(ctx, featureflag.GoPostReceiveHook))),
}, gitlabshellEnv...)
transaction, err := metadata.TransactionFromContext(ctx)
diff --git a/internal/metadata/featureflag/feature_flags.go b/internal/metadata/featureflag/feature_flags.go
index 51e6e60c1..19f703759 100644
--- a/internal/metadata/featureflag/feature_flags.go
+++ b/internal/metadata/featureflag/feature_flags.go
@@ -16,7 +16,8 @@ var (
DistributedReads = FeatureFlag{Name: "distributed_reads", OnByDefault: false}
// GoPreReceiveHook will bypass the ruby pre-receive hook and use the go implementation
GoPreReceiveHook = FeatureFlag{Name: "go_prereceive_hook", OnByDefault: true}
-
+ // GoPostReceiveHook will bypass the ruby post-receive hook and use the go implementation
+ GoPostReceiveHook = FeatureFlag{Name: "go_postreceive_hook", OnByDefault: false}
// ReferenceTransactions will handle Git reference updates via the transaction service for strong consistency
ReferenceTransactions = FeatureFlag{Name: "reference_transactions", OnByDefault: false}
// ReferenceTransactionsOperationService will enable reference transactions for the OperationService
@@ -28,6 +29,7 @@ var (
)
const (
- GoUpdateHookEnvVar = "GITALY_GO_UPDATE"
- GoPreReceiveHookEnvVar = "GITALY_GO_PRERECEIVE"
+ GoUpdateHookEnvVar = "GITALY_GO_UPDATE"
+ GoPreReceiveHookEnvVar = "GITALY_GO_PRERECEIVE"
+ GoPostReceiveHookEnvVar = "GITALY_GO_POSTRECEIVE"
)
diff --git a/internal/praefect/replicator_test.go b/internal/praefect/replicator_test.go
index 8a8af2b30..542d16970 100644
--- a/internal/praefect/replicator_test.go
+++ b/internal/praefect/replicator_test.go
@@ -29,6 +29,7 @@ import (
"gitlab.com/gitlab-org/gitaly/internal/praefect/transactions"
"gitlab.com/gitlab-org/gitaly/internal/rubyserver"
serverPkg "gitlab.com/gitlab-org/gitaly/internal/server"
+ "gitlab.com/gitlab-org/gitaly/internal/service/hook"
objectpoolservice "gitlab.com/gitlab-org/gitaly/internal/service/objectpool"
"gitlab.com/gitlab-org/gitaly/internal/service/ref"
"gitlab.com/gitlab-org/gitaly/internal/service/remote"
@@ -766,7 +767,7 @@ func TestBackoff(t *testing.T) {
}
func runFullGitalyServer(t *testing.T) (*grpc.Server, string) {
- server := serverPkg.NewInsecure(RubyServer, testhelper.GitlabAPIStub, gitaly_config.Config)
+ server := serverPkg.NewInsecure(RubyServer, hook.GitlabAPIStub, gitaly_config.Config)
serverSocketPath := testhelper.GetTemporaryGitalySocketFileName()
diff --git a/internal/service/hook/access.go b/internal/service/hook/access.go
index 2eec8f617..5ba74dafd 100644
--- a/internal/service/hook/access.go
+++ b/internal/service/hook/access.go
@@ -57,6 +57,8 @@ type GitlabAPI interface {
Allowed(repo *gitalypb.Repository, glRepository, glID, glProtocol, changes string) (bool, string, error)
// PreReceive queries the gitlab internal api /pre_receive to increase the reference counter
PreReceive(glRepository string) (bool, error)
+ // PostReceive queries the gitlab internal api /post_receive to decrease the reference counter
+ PostReceive(glRepository, glID, changes string, pushOptions ...string) (bool, []PostReceiveMessage, error)
}
// gitlabAPI is a wrapper around client.GitlabNetClient with API methods for gitlab git receive hooks
@@ -196,6 +198,52 @@ func (a *gitlabAPI) PreReceive(glRepository string) (bool, error) {
return result.ReferenceCounterIncreased, nil
}
+// PostReceiveResponse is the response the GitLab internal api provides on a successful /post_receive call
+type PostReceiveResponse struct {
+ ReferenceCounterDecreased bool `json:"reference_counter_decreased"`
+ Messages []PostReceiveMessage `json:"messages"`
+}
+
+// PostReceiveMessage encapsulates a message from the /post_receive endpoint that gets printed to stdout
+type PostReceiveMessage struct {
+ Message string `json:"message"`
+ Type string `json:"type"`
+}
+
+// PostReceive decreases the reference counter for a push for a given gl_repository through the gitlab internal API /post_receive endpoint
+func (a *gitlabAPI) PostReceive(glRepository, glID, changes string, pushOptions ...string) (bool, []PostReceiveMessage, error) {
+ resp, err := a.client.Post("/post_receive", map[string]interface{}{"gl_repository": glRepository, "identifier": glID, "changes": changes, "push_options[]": pushOptions})
+ if err != nil {
+ return false, nil, fmt.Errorf("http post to gitlab api /post_receive endpoint: %w", err)
+ }
+
+ defer func() {
+ io.Copy(ioutil.Discard, resp.Body)
+ resp.Body.Close()
+ }()
+
+ if resp.StatusCode != http.StatusOK {
+ return false, nil, fmt.Errorf("post-receive call failed with status: %d", resp.StatusCode)
+ }
+
+ mtype, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
+ if err != nil {
+ return false, nil, fmt.Errorf("/post_receive endpoint respond with invalid content type: %w", err)
+ }
+
+ if mtype != "application/json" {
+ return false, nil, fmt.Errorf("/post_receive endpoint respond with unsupported content type: %s", mtype)
+ }
+
+ var result PostReceiveResponse
+
+ if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
+ return false, nil, fmt.Errorf("decoding response from /post_receive endpoint: %w", err)
+ }
+
+ return result.ReferenceCounterDecreased, result.Messages, nil
+}
+
var glIDRegex = regexp.MustCompile(`\A[0-9]+\z`)
func (a *AllowedRequest) parseAndSetGLID(glID string) error {
@@ -219,3 +267,22 @@ func (a *AllowedRequest) parseAndSetGLID(glID string) error {
return nil
}
+
+// mockAPI is a noop gitlab API client
+type mockAPI struct {
+}
+
+func (m *mockAPI) Allowed(repo *gitalypb.Repository, glRepository, glID, glProtocol, changes string) (bool, string, error) {
+ return true, "", nil
+}
+
+func (m *mockAPI) PreReceive(glRepository string) (bool, error) {
+ return true, nil
+}
+
+func (m *mockAPI) PostReceive(glRepository, glID, changes string, gitPushOptions ...string) (bool, []PostReceiveMessage, error) {
+ return true, nil, nil
+}
+
+// GitlabAPIStub is a global mock that can be used in testing
+var GitlabAPIStub = &mockAPI{}
diff --git a/internal/service/hook/post_receive.go b/internal/service/hook/post_receive.go
index afae9bb1b..6c6158fe9 100644
--- a/internal/service/hook/post_receive.go
+++ b/internal/service/hook/post_receive.go
@@ -1,17 +1,126 @@
package hook
import (
+ "bytes"
"errors"
+ "fmt"
"io"
"io/ioutil"
+ "math"
"os/exec"
+ "strings"
"gitlab.com/gitlab-org/gitaly/internal/git/hooks"
"gitlab.com/gitlab-org/gitaly/internal/helper"
+ "gitlab.com/gitlab-org/gitaly/internal/metadata/featureflag"
"gitlab.com/gitlab-org/gitaly/proto/go/gitalypb"
"gitlab.com/gitlab-org/gitaly/streamio"
)
+func isGoPostReceiveHookUsed(env []string) bool {
+ return getEnvVar(featureflag.GoPostReceiveHookEnvVar, env) == "true"
+}
+
+func postReceiveHookResponse(stream gitalypb.HookService_PostReceiveHookServer, code int32, stderr string) error {
+ if err := stream.Send(&gitalypb.PostReceiveHookResponse{
+ ExitStatus: &gitalypb.ExitStatus{Value: code},
+ Stderr: []byte(stderr),
+ }); err != nil {
+ return helper.ErrInternalf("sending response: %v", err)
+ }
+
+ return nil
+}
+
+const (
+ // A standard terminal window is (at least) 80 characters wide.
+ terminalWidth = 80
+ gitRemoteMessagePrefixLength = len("remote: ")
+ terminalMessagePadding = 2
+
+ // Git prefixes remote messages with "remote: ", so this width is subtracted
+ // from the width available to us.
+ maxMessageWidth = terminalWidth - gitRemoteMessagePrefixLength
+
+ // Our centered text shouldn't start or end right at the edge of the window,
+ // so we add some horizontal padding: 2 chars on either side.
+ maxMessageTextWidth = maxMessageWidth - 2*terminalMessagePadding
+)
+
+func printMessages(messages []PostReceiveMessage, w io.Writer) error {
+ for _, message := range messages {
+ if _, err := w.Write([]byte("\n")); err != nil {
+ return err
+ }
+
+ switch message.Type {
+ case "basic":
+ if _, err := w.Write([]byte(message.Message)); err != nil {
+ return err
+ }
+ case "alert":
+ if err := printAlert(message, w); err != nil {
+ return err
+ }
+ default:
+ return fmt.Errorf("invalid message type: %v", message.Type)
+ }
+
+ if _, err := w.Write([]byte("\n\n")); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func centerLine(b []byte) []byte {
+ b = bytes.TrimSpace(b)
+ linePadding := int(math.Max((float64(maxMessageWidth)-float64(len(b)))/2, 0))
+ return append(bytes.Repeat([]byte(" "), linePadding), b...)
+}
+
+func printAlert(m PostReceiveMessage, w io.Writer) error {
+ if _, err := w.Write(bytes.Repeat([]byte("="), maxMessageWidth)); err != nil {
+ return err
+ }
+
+ if _, err := w.Write([]byte("\n\n")); err != nil {
+ return err
+ }
+
+ words := strings.Fields(m.Message)
+
+ line := bytes.NewBufferString("")
+
+ for _, word := range words {
+ if line.Len()+1+len(word) > maxMessageTextWidth {
+ if _, err := w.Write(append(centerLine(line.Bytes()), '\n')); err != nil {
+ return err
+ }
+ line.Reset()
+ }
+
+ if _, err := line.WriteString(word + " "); err != nil {
+ return err
+ }
+ }
+
+ if _, err := w.Write(centerLine(line.Bytes())); err != nil {
+ return err
+ }
+
+ if _, err := w.Write([]byte("\n\n")); err != nil {
+ return err
+ }
+
+ if _, err := w.Write(bytes.Repeat([]byte("="), maxMessageWidth)); err != nil {
+ return err
+ }
+
+ return nil
+}
+
func (s *server) PostReceiveHook(stream gitalypb.HookService_PostReceiveHookServer) error {
firstRequest, err := stream.Recv()
if err != nil {
@@ -22,6 +131,74 @@ func (s *server) PostReceiveHook(stream gitalypb.HookService_PostReceiveHookServ
return helper.ErrInvalidArgument(err)
}
+ if !isGoPostReceiveHookUsed(firstRequest.GetEnvironmentVariables()) {
+ return postReceiveHookRuby(firstRequest, stream)
+ }
+
+ hookEnv, err := hookRequestEnv(firstRequest)
+ if err != nil {
+ return helper.ErrInternal(err)
+ }
+
+ hookEnv = append(hookEnv, hooks.GitPushOptions(firstRequest.GetGitPushOptions())...)
+
+ stdin := streamio.NewReader(func() ([]byte, error) {
+ req, err := stream.Recv()
+ return req.GetStdin(), err
+ })
+ stdout := streamio.NewWriter(func(p []byte) error { return stream.Send(&gitalypb.PostReceiveHookResponse{Stdout: p}) })
+ stderr := streamio.NewWriter(func(p []byte) error { return stream.Send(&gitalypb.PostReceiveHookResponse{Stderr: p}) })
+
+ changes, err := ioutil.ReadAll(stdin)
+ if err != nil {
+ return helper.ErrInternalf("reading stdin from request: %w", err)
+ }
+
+ glID, glRepo := getEnvVar("GL_ID", hookEnv), getEnvVar("GL_REPOSITORY", hookEnv)
+
+ ok, messages, err := s.gitlabAPI.PostReceive(glRepo, glID, string(changes), firstRequest.GetGitPushOptions()...)
+ if err != nil {
+ return postReceiveHookResponse(stream, 1, fmt.Sprintf("GitLab: %v", err))
+ }
+
+ if err := printMessages(messages, stdout); err != nil {
+ return helper.ErrInternalf("error writing messages to stream: %v", err)
+ }
+
+ if !ok {
+ return postReceiveHookResponse(stream, 1, "")
+ }
+
+ // custom hooks execution
+ repoPath, err := helper.GetRepoPath(firstRequest.GetRepository())
+ if err != nil {
+ return err
+ }
+ executor, err := newCustomHooksExecutor(repoPath, s.hooksConfig.CustomHooksDir, "post-receive")
+ if err != nil {
+ return helper.ErrInternalf("creating custom hooks executor: %v", err)
+ }
+
+ if err = executor(
+ stream.Context(),
+ nil,
+ hookEnv,
+ bytes.NewReader(changes),
+ stdout,
+ stderr,
+ ); err != nil {
+ var exitError *exec.ExitError
+ if errors.As(err, &exitError) {
+ return postReceiveHookResponse(stream, int32(exitError.ExitCode()), "")
+ }
+
+ return helper.ErrInternalf("executing custom hooks: %v", err)
+ }
+
+ return postReceiveHookResponse(stream, 0, "")
+}
+
+func postReceiveHookRuby(firstRequest *gitalypb.PostReceiveHookRequest, stream gitalypb.HookService_PostReceiveHookServer) error {
hookEnv, err := hookRequestEnv(firstRequest)
if err != nil {
return helper.ErrInternal(err)
diff --git a/internal/service/hook/post_receive_test.go b/internal/service/hook/post_receive_test.go
index 73b6fb428..3c9bb1255 100644
--- a/internal/service/hook/post_receive_test.go
+++ b/internal/service/hook/post_receive_test.go
@@ -2,6 +2,7 @@ package hook
import (
"bytes"
+ "fmt"
"io"
"os"
"path/filepath"
@@ -236,3 +237,175 @@ func TestPostReceive(t *testing.T) {
})
}
}
+
+func TestPostReceiveMessages(t *testing.T) {
+ testCases := []struct {
+ desc string
+ basicMessages, alertMessages []string
+ expectedStdout string
+ }{
+ {
+ desc: "basic MR message",
+ basicMessages: []string{"To create a merge request for okay, visit:\n http://localhost/project/-/merge_requests/new?merge_request"},
+ expectedStdout: `
+To create a merge request for okay, visit:
+ http://localhost/project/-/merge_requests/new?merge_request
+`,
+ },
+ {
+ desc: "alert",
+ alertMessages: []string{"something went very wrong"},
+ expectedStdout: `
+========================================================================
+
+ something went very wrong
+
+========================================================================
+`,
+ },
+ }
+
+ testRepo, testRepoPath, cleanupFn := testhelper.NewTestRepo(t)
+ defer cleanupFn()
+
+ secretToken := "secret token"
+ user, password := "user", "password"
+
+ tempDir, cleanup := testhelper.CreateTemporaryGitlabShellDir(t)
+ defer cleanup()
+ testhelper.WriteShellSecretFile(t, tempDir, secretToken)
+
+ for _, tc := range testCases {
+ for _, useGoPreReceive := range []bool{true, false} {
+ t.Run(fmt.Sprintf("%s:use_go_pre_receive:%v", tc.desc, useGoPreReceive), func(t *testing.T) {
+ c := testhelper.GitlabTestServerOptions{
+ User: user,
+ Password: password,
+ SecretToken: secretToken,
+ GLID: "key_id",
+ GLRepository: "repository",
+ Changes: "changes",
+ PostReceiveCounterDecreased: true,
+ PostReceiveMessages: tc.basicMessages,
+ PostReceiveAlerts: tc.alertMessages,
+ Protocol: "protocol",
+ RepoPath: testRepoPath,
+ }
+
+ ts := testhelper.NewGitlabTestServer(c)
+ defer ts.Close()
+
+ gitlabConfig := config.Gitlab{
+ SecretFile: filepath.Join(tempDir, ".gitlab_shell_secret"),
+ URL: ts.URL,
+ HTTPSettings: config.HTTPSettings{
+ User: user,
+ Password: password,
+ },
+ }
+
+ defer func(cfg config.Cfg) {
+ config.Config = cfg
+ }(config.Config)
+
+ config.Config.Gitlab = gitlabConfig
+
+ api, err := NewGitlabAPI(gitlabConfig)
+ require.NoError(t, err)
+
+ serverSocketPath, stop := runHooksServerWithAPI(t, api, config.Config.Hooks)
+ defer stop()
+
+ client, conn := newHooksClient(t, serverSocketPath)
+ defer conn.Close()
+
+ ctx, cancel := testhelper.Context()
+ defer cancel()
+
+ stream, err := client.PostReceiveHook(ctx)
+ require.NoError(t, err)
+
+ envVars := []string{
+ "GL_ID=key_id",
+ "GL_USERNAME=username",
+ "GL_PROTOCOL=protocol",
+ "GL_REPOSITORY=repository"}
+ if useGoPreReceive {
+ envVars = append(envVars, "GITALY_GO_PRERECEIVE=true")
+ }
+
+ require.NoError(t, stream.Send(&gitalypb.PostReceiveHookRequest{
+ Repository: testRepo,
+ EnvironmentVariables: envVars}))
+
+ go func() {
+ writer := streamio.NewWriter(func(p []byte) error {
+ return stream.Send(&gitalypb.PostReceiveHookRequest{Stdin: p})
+ })
+ _, err := writer.Write([]byte("changes"))
+ require.NoError(t, err)
+ require.NoError(t, stream.CloseSend(), "close send")
+ }()
+
+ var status int32
+ var stdout, stderr bytes.Buffer
+ for {
+ resp, err := stream.Recv()
+ if err == io.EOF {
+ break
+ }
+
+ _, err = stdout.Write(resp.GetStdout())
+ require.NoError(t, err)
+ stderr.Write(resp.GetStderr())
+ status = resp.GetExitStatus().GetValue()
+ }
+
+ assert.Equal(t, int32(0), status)
+ assert.Equal(t, "", text.ChompBytes(stderr.Bytes()), "hook stderr")
+ assert.Equal(t, tc.expectedStdout, text.ChompBytes(stdout.Bytes()), "hook stdout")
+ })
+ }
+ }
+}
+
+func TestPrintAlert(t *testing.T) {
+ testCases := []struct {
+ message string
+ expected string
+ }{
+ {
+ message: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur nec mi lectus. Fusce eu ligula in odio hendrerit posuere. Ut semper neque vitae maximus accumsan. In malesuada justo nec leo congue egestas. Vivamus interdum nec libero ac convallis. Praesent euismod et nunc vitae vulputate. Mauris tincidunt ligula urna, bibendum vestibulum sapien luctus eu. Donec sed justo in erat dictum semper. Ut porttitor augue in felis gravida scelerisque. Morbi dolor justo, accumsan et nulla vitae, luctus consectetur est. Donec aliquet erat pellentesque suscipit elementum. Cras posuere eros ipsum, a tincidunt tortor laoreet quis. Mauris varius nulla vitae placerat imperdiet. Vivamus ut ligula odio. Cras nec euismod ligula.",
+ expected: `========================================================================
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur
+ nec mi lectus. Fusce eu ligula in odio hendrerit posuere. Ut semper
+ neque vitae maximus accumsan. In malesuada justo nec leo congue
+ egestas. Vivamus interdum nec libero ac convallis. Praesent euismod
+ et nunc vitae vulputate. Mauris tincidunt ligula urna, bibendum
+ vestibulum sapien luctus eu. Donec sed justo in erat dictum semper.
+ Ut porttitor augue in felis gravida scelerisque. Morbi dolor justo,
+ accumsan et nulla vitae, luctus consectetur est. Donec aliquet erat
+ pellentesque suscipit elementum. Cras posuere eros ipsum, a
+ tincidunt tortor laoreet quis. Mauris varius nulla vitae placerat
+ imperdiet. Vivamus ut ligula odio. Cras nec euismod ligula.
+
+========================================================================`,
+ },
+ {
+ message: "Lorem ipsum dolor sit amet, consectetur",
+ expected: `========================================================================
+
+ Lorem ipsum dolor sit amet, consectetur
+
+========================================================================`,
+ },
+ }
+
+ for _, tc := range testCases {
+ var result bytes.Buffer
+
+ require.NoError(t, printAlert(PostReceiveMessage{Message: tc.message}, &result))
+ assert.Equal(t, tc.expected, result.String())
+ }
+}
diff --git a/internal/service/hook/testhelper_test.go b/internal/service/hook/testhelper_test.go
index a59596269..4bfb2fc28 100644
--- a/internal/service/hook/testhelper_test.go
+++ b/internal/service/hook/testhelper_test.go
@@ -32,7 +32,7 @@ func newHooksClient(t *testing.T, serverSocketPath string) (gitalypb.HookService
}
func runHooksServer(t *testing.T, hooksCfg config.Hooks) (string, func()) {
- return runHooksServerWithAPI(t, testhelper.GitlabAPIStub, hooksCfg)
+ return runHooksServerWithAPI(t, GitlabAPIStub, hooksCfg)
}
func runHooksServerWithAPI(t *testing.T, gitlabAPI GitlabAPI, hooksCfg config.Hooks) (string, func()) {
diff --git a/internal/service/operations/rebase_test.go b/internal/service/operations/rebase_test.go
index a7d0aeb46..2c6cecbdb 100644
--- a/internal/service/operations/rebase_test.go
+++ b/internal/service/operations/rebase_test.go
@@ -11,6 +11,7 @@ import (
"github.com/stretchr/testify/require"
gitlog "gitlab.com/gitlab-org/gitaly/internal/git/log"
+ "gitlab.com/gitlab-org/gitaly/internal/metadata/featureflag"
"gitlab.com/gitlab-org/gitaly/internal/rubyserver"
"gitlab.com/gitlab-org/gitaly/internal/testhelper"
"gitlab.com/gitlab-org/gitaly/proto/go/gitalypb"
@@ -23,6 +24,9 @@ var (
)
func TestSuccessfulUserRebaseConfirmableRequest(t *testing.T) {
+ featureSets, err := testhelper.NewFeatureSets(nil, featureflag.GoPostReceiveHook)
+ require.NoError(t, err)
+
ctx, cancel := testhelper.Context()
defer cancel()
@@ -38,7 +42,12 @@ func TestSuccessfulUserRebaseConfirmableRequest(t *testing.T) {
serverSocketPath, stop := runOperationServiceServerWithRubyServer(t, &ruby)
defer stop()
- testSuccessfulUserRebaseConfirmableRequest(t, ctx, serverSocketPath, pushOptions)
+ for _, featureSet := range featureSets {
+ t.Run(featureSet.String(), func(t *testing.T) {
+ ctx := featureSet.WithParent(ctx)
+ testSuccessfulUserRebaseConfirmableRequest(t, ctx, serverSocketPath, pushOptions)
+ })
+ }
}
func testSuccessfulUserRebaseConfirmableRequest(t *testing.T, ctxOuter context.Context, serverSocketPath string, pushOptions []string) {
diff --git a/internal/service/operations/tags_test.go b/internal/service/operations/tags_test.go
index 620ba537b..bc06a693c 100644
--- a/internal/service/operations/tags_test.go
+++ b/internal/service/operations/tags_test.go
@@ -62,7 +62,15 @@ func TestSuccessfulGitHooksForUserDeleteTagRequest(t *testing.T) {
ctx, cancel := testhelper.Context()
defer cancel()
- testSuccessfulGitHooksForUserDeleteTagRequest(t, ctx)
+ featureSets, err := testhelper.NewFeatureSets(nil, featureflag.GoPostReceiveHook)
+ require.NoError(t, err)
+
+ for _, featureSet := range featureSets {
+ t.Run(featureSet.String(), func(t *testing.T) {
+ ctx := featureSet.WithParent(ctx)
+ testSuccessfulGitHooksForUserDeleteTagRequest(t, ctx)
+ })
+ }
}
func testSuccessfulGitHooksForUserDeleteTagRequest(t *testing.T, ctx context.Context) {
diff --git a/internal/service/operations/testhelper_test.go b/internal/service/operations/testhelper_test.go
index 028cdd327..6edcb2762 100644
--- a/internal/service/operations/testhelper_test.go
+++ b/internal/service/operations/testhelper_test.go
@@ -87,7 +87,7 @@ func runOperationServiceServerWithRubyServer(t *testing.T, ruby *rubyserver.Serv
require.NoError(t, err)
gitalypb.RegisterOperationServiceServer(srv.GrpcServer(), &server{ruby: ruby})
- gitalypb.RegisterHookServiceServer(srv.GrpcServer(), hook.NewServer(testhelper.GitlabAPIStub, config.Config.Hooks))
+ gitalypb.RegisterHookServiceServer(srv.GrpcServer(), hook.NewServer(hook.GitlabAPIStub, config.Config.Hooks))
gitalypb.RegisterRepositoryServiceServer(srv.GrpcServer(), repository.NewServer(ruby, config.NewLocator(config.Config), internalSocket))
gitalypb.RegisterRefServiceServer(srv.GrpcServer(), ref.NewServer())
gitalypb.RegisterCommitServiceServer(srv.GrpcServer(), commit.NewServer())
diff --git a/internal/service/smarthttp/receive_pack_test.go b/internal/service/smarthttp/receive_pack_test.go
index 17f3d7f14..f7c162c39 100644
--- a/internal/service/smarthttp/receive_pack_test.go
+++ b/internal/service/smarthttp/receive_pack_test.go
@@ -350,6 +350,20 @@ func TestPostReceivePackToHooks(t *testing.T) {
config.Config = cfg
}(config.Config)
+ features, err := testhelper.NewFeatureSets([]featureflag.FeatureFlag{featureflag.GoPostReceiveHook})
+ require.NoError(t, err)
+
+ ctx, cancel := testhelper.Context()
+ defer cancel()
+
+ for _, feature := range features {
+ t.Run(feature.String(), func(t *testing.T) {
+ testPostReceivePackToHooks(t, feature.WithParent(ctx))
+ })
+ }
+}
+
+func testPostReceivePackToHooks(t *testing.T, ctx context.Context) {
secretToken := "secret token"
glRepository := "some_repo"
glID := "key-123"
@@ -409,9 +423,6 @@ func TestPostReceivePackToHooks(t *testing.T) {
hookDir := filepath.Join(cwd, "../../../ruby", "git-hooks")
hooks.Override = hookDir
- ctx, cancel := testhelper.Context()
- defer cancel()
-
stream, err := client.PostReceivePack(ctx)
require.NoError(t, err)
@@ -442,7 +453,7 @@ func runSmartHTTPHookServiceServer(t *testing.T) (*grpc.Server, string) {
}
gitalypb.RegisterSmartHTTPServiceServer(server, NewServer())
- gitalypb.RegisterHookServiceServer(server, hook.NewServer(testhelper.GitlabAPIStub, config.Config.Hooks))
+ gitalypb.RegisterHookServiceServer(server, hook.NewServer(hook.GitlabAPIStub, config.Config.Hooks))
reflection.Register(server)
go server.Serve(listener)
@@ -509,7 +520,7 @@ func TestPostReceiveWithTransactions(t *testing.T) {
gitalyServer := testhelper.NewServerWithAuth(t, nil, nil, config.Config.Auth.Token)
gitalypb.RegisterSmartHTTPServiceServer(gitalyServer.GrpcServer(), NewServer())
- gitalypb.RegisterHookServiceServer(gitalyServer.GrpcServer(), hook.NewServer(testhelper.GitlabAPIStub, config.Config.Hooks))
+ gitalypb.RegisterHookServiceServer(gitalyServer.GrpcServer(), hook.NewServer(hook.GitlabAPIStub, config.Config.Hooks))
reflection.Register(gitalyServer.GrpcServer())
require.NoError(t, gitalyServer.Start())
defer gitalyServer.Stop()
diff --git a/internal/testhelper/testhelper.go b/internal/testhelper/testhelper.go
index 3245c52d9..72d803176 100644
--- a/internal/testhelper/testhelper.go
+++ b/internal/testhelper/testhelper.go
@@ -808,21 +808,6 @@ func NewFeatureSets(goFeatures []featureflag.FeatureFlag, rubyFeatures ...featur
return f, nil
}
-// mockAPI is a noop gitlab API client
-type mockAPI struct {
-}
-
-func (m *mockAPI) Allowed(repo *gitalypb.Repository, glRepository, glID, glProtocol, changes string) (bool, string, error) {
- return true, "", nil
-}
-
-func (m *mockAPI) PreReceive(glRepository string) (bool, error) {
- return true, nil
-}
-
-// GitlabAPIStub is a global mock that can be used in testing
-var GitlabAPIStub = &mockAPI{}
-
// ModifyEnvironment will change an environment variable and return a func suitable
// for `defer` to change the value back.
func ModifyEnvironment(t testing.TB, key string, value string) func() {
diff --git a/internal/testhelper/testserver.go b/internal/testhelper/testserver.go
index 390b7e6f4..c743f93b4 100644
--- a/internal/testhelper/testserver.go
+++ b/internal/testhelper/testserver.go
@@ -589,9 +589,18 @@ func handlePostReceive(options GitlabTestServerOptions) func(w http.ResponseWrit
}
if params["secret_token"] != options.SecretToken {
- http.Error(w, "secret_token is invalid", http.StatusUnauthorized)
- return
+ decodedSecret, err := base64.StdEncoding.DecodeString(r.Header.Get("Gitlab-Shared-Secret"))
+ if err != nil {
+ http.Error(w, "secret_token is invalid", http.StatusUnauthorized)
+ return
+ }
+
+ if string(decodedSecret) != options.SecretToken {
+ http.Error(w, "secret_token is invalid", http.StatusUnauthorized)
+ return
+ }
}
+
if params["identifier"] == "" {
http.Error(w, "identifier is empty", http.StatusUnauthorized)
return
@@ -615,6 +624,7 @@ func handlePostReceive(options GitlabTestServerOptions) func(w http.ResponseWrit
return
}
}
+
if len(options.GitPushOptions) > 0 {
pushOptions := params["push_options"].([]string)
if len(pushOptions) != len(options.GitPushOptions) {
@@ -630,12 +640,42 @@ func handlePostReceive(options GitlabTestServerOptions) func(w http.ResponseWrit
}
}
- w.WriteHeader(http.StatusOK)
+ response := postReceiveResponse{
+ ReferenceCounterDecreased: options.PostReceiveCounterDecreased,
+ }
+
+ for _, basicMessage := range options.PostReceiveMessages {
+ response.Messages = append(response.Messages, postReceiveMessage{
+ Message: basicMessage,
+ Type: "basic",
+ })
+ }
+
+ for _, alertMessage := range options.PostReceiveAlerts {
+ response.Messages = append(response.Messages, postReceiveMessage{
+ Message: alertMessage,
+ Type: "alert",
+ })
+ }
+
w.Header().Set("Content-Type", "application/json")
- fmt.Fprintf(w, `{"reference_counter_decreased": %v}`, options.PostReceiveCounterDecreased)
+ w.WriteHeader(http.StatusOK)
+ if err := json.NewEncoder(w).Encode(&response); err != nil {
+ log.Fatal(err)
+ }
}
}
+type postReceiveResponse struct {
+ ReferenceCounterDecreased bool `json:"reference_counter_decreased"`
+ Messages []postReceiveMessage `json:"messages"`
+}
+
+type postReceiveMessage struct {
+ Message string `json:"message"`
+ Type string `json:"type"`
+}
+
func handleCheck(options GitlabTestServerOptions) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
u, p, ok := r.BasicAuth()
@@ -656,6 +696,8 @@ type GitlabTestServerOptions struct {
GLRepository string
Changes string
PostReceiveCounterDecreased bool
+ PostReceiveMessages []string
+ PostReceiveAlerts []string
Protocol string
GitPushOptions []string
GitObjectDir string
diff --git a/ruby/lib/gitlab/git/hook.rb b/ruby/lib/gitlab/git/hook.rb
index 9fb54506e..6711aaae3 100644
--- a/ruby/lib/gitlab/git/hook.rb
+++ b/ruby/lib/gitlab/git/hook.rb
@@ -121,7 +121,8 @@ module Gitlab
'GITALY_REPO' => repository.gitaly_repository.to_json,
'GITALY_SOCKET' => Gitlab.config.gitaly.internal_socket,
'GITALY_GO_UPDATE' => repository.feature_enabled?('go-update-hook', on_by_default: true).to_s,
- 'GITALY_GO_PRERECEIVE' => repository.feature_enabled?('go-prereceive-hook', on_by_default: true).to_s
+ 'GITALY_GO_PRERECEIVE' => repository.feature_enabled?('go-prereceive-hook', on_by_default: true).to_s,
+ 'GITALY_GO_POSTRECEIVE' => repository.feature_enabled?('go-postreceive-hook').to_s
}
end
end