diff options
author | Jacob Vosmaer <jacob@gitlab.com> | 2019-10-01 19:16:38 +0300 |
---|---|---|
committer | Jacob Vosmaer <jacob@gitlab.com> | 2019-10-01 19:16:38 +0300 |
commit | 950b34432b20a1059d29b46801b0c9233f9769dd (patch) | |
tree | 03196ba934737cd39204c1a3ced9b250fac0618a | |
parent | 210c94447d0d8d4613a4ab9a86d606bbc46d3f2f (diff) | |
parent | b676876889ce332b26efb93ddd8d5d9649da2776 (diff) |
Merge branch 'jc-hook-binary' into 'master'
Create go binary to execute hooks
Closes #1859
See merge request gitlab-org/gitaly!1328
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | NOTICE | 253 | ||||
-rw-r--r-- | changelogs/unreleased/jc-hook-binary.yml | 5 | ||||
-rw-r--r-- | cmd/gitaly-hooks/.gitignore | 2 | ||||
-rw-r--r-- | cmd/gitaly-hooks/hooks.go | 61 | ||||
-rw-r--r-- | cmd/gitaly-hooks/hooks_test.go | 300 | ||||
-rwxr-xr-x | cmd/gitaly-hooks/testdata/update | 4 | ||||
-rw-r--r-- | go.mod | 4 | ||||
-rw-r--r-- | go.sum | 7 | ||||
-rw-r--r-- | internal/git/receivepack.go | 2 | ||||
-rw-r--r-- | internal/gitlabshell/env.go | 2 | ||||
-rw-r--r-- | internal/log/hook.go | 42 | ||||
-rw-r--r-- | internal/rubyserver/rubyserver.go | 1 | ||||
-rwxr-xr-x | ruby/git-hooks/gitlab-shell-hook | 9 | ||||
-rw-r--r-- | ruby/lib/gitlab/config.rb | 8 | ||||
-rw-r--r-- | ruby/lib/gitlab/git/hook.rb | 2 | ||||
-rw-r--r-- | ruby/lib/gitlab/git/remote_repository.rb | 2 | ||||
-rw-r--r-- | ruby/spec/spec_helper.rb | 2 |
18 files changed, 688 insertions, 19 deletions
diff --git a/.gitignore b/.gitignore index cde28605c..dfa56ee86 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ /_build /gitaly +/gitaly-hooks cmd/gitaly-ssh/gitaly-ssh /gitaly-ssh cmd/gitaly-wrapper/gitaly-wrapper @@ -3555,4 +3555,255 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ LICENSE-3rdparty.csv - gitlab.com/gitlab-org/gitaly/vendor/gopkg.in/DataDog/dd-trace-go.v1 Component,Origin,License,Copyright -import,io.opentracing,Apache-2.0,Copyright 2016-2017 The OpenTracing Authors +import,io.opentracing,Apache-2.0,Copyright 2016-2017 The OpenTracing Authors~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +LICENSE - gitlab.com/gitlab-org/gitaly/vendor/gopkg.in/yaml.v2 + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +LICENSE.libyaml - gitlab.com/gitlab-org/gitaly/vendor/gopkg.in/yaml.v2 +The following files were ported to Go from C files of libyaml, and thus +are still covered by their original copyright and license: + + apic.go + emitterc.go + parserc.go + readerc.go + scannerc.go + writerc.go + yamlh.go + yamlprivateh.go + +Copyright (c) 2006 Kirill Simonov + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +NOTICE - gitlab.com/gitlab-org/gitaly/vendor/gopkg.in/yaml.v2 +Copyright 2011-2016 Canonical Ltd. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + diff --git a/changelogs/unreleased/jc-hook-binary.yml b/changelogs/unreleased/jc-hook-binary.yml new file mode 100644 index 000000000..fa8a22077 --- /dev/null +++ b/changelogs/unreleased/jc-hook-binary.yml @@ -0,0 +1,5 @@ +--- +title: Create go binary to execute hooks +merge_request: 1328 +author: +type: other diff --git a/cmd/gitaly-hooks/.gitignore b/cmd/gitaly-hooks/.gitignore new file mode 100644 index 000000000..084737248 --- /dev/null +++ b/cmd/gitaly-hooks/.gitignore @@ -0,0 +1,2 @@ +testdata/gitaly-libexec +testdata/tempfile
\ No newline at end of file diff --git a/cmd/gitaly-hooks/hooks.go b/cmd/gitaly-hooks/hooks.go new file mode 100644 index 000000000..c699d0c6d --- /dev/null +++ b/cmd/gitaly-hooks/hooks.go @@ -0,0 +1,61 @@ +package main + +import ( + "bytes" + "context" + "errors" + "io" + "os" + "os/exec" + "path/filepath" + + "gitlab.com/gitlab-org/gitaly/internal/command" + "gitlab.com/gitlab-org/gitaly/internal/log" +) + +func main() { + var logger = log.NewHookLogger() + + if len(os.Args) < 2 { + logger.Fatal(errors.New("requires hook name")) + } + + gitlabRubyDir := os.Getenv("GITALY_RUBY_DIR") + if gitlabRubyDir == "" { + logger.Fatal(errors.New("GITALY_RUBY_DIR not set")) + } + + hookName := os.Args[1] + rubyHookPath := filepath.Join(gitlabRubyDir, "gitlab-shell", "hooks", hookName) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + var hookCmd *exec.Cmd + + switch hookName { + case "update": + args := os.Args[2:] + if len(args) != 3 { + logger.Fatal(errors.New("update hook missing required arguments")) + } + + hookCmd = exec.Command(rubyHookPath, args...) + case "pre-receive", "post-receive": + hookCmd = exec.Command(rubyHookPath) + default: + logger.Fatal(errors.New("hook name invalid")) + } + + var stderr bytes.Buffer + mw := io.MultiWriter(&stderr, os.Stderr) + + cmd, err := command.New(ctx, hookCmd, os.Stdin, os.Stdout, mw, os.Environ()...) + if err != nil { + logger.Fatalf("error when starting command for %v: %v", rubyHookPath, err) + } + + if err = cmd.Wait(); err != nil { + os.Exit(1) + } +} diff --git a/cmd/gitaly-hooks/hooks_test.go b/cmd/gitaly-hooks/hooks_test.go new file mode 100644 index 000000000..cf8990c20 --- /dev/null +++ b/cmd/gitaly-hooks/hooks_test.go @@ -0,0 +1,300 @@ +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "log" + "net/http" + "net/http/httptest" + "os" + "os/exec" + "path" + "path/filepath" + "strconv" + "testing" + + "github.com/stretchr/testify/require" + "gitlab.com/gitlab-org/gitaly/internal/command" + "gitlab.com/gitlab-org/gitaly/internal/config" + "gitlab.com/gitlab-org/gitaly/internal/testhelper" + "gopkg.in/yaml.v2" +) + +func TestMain(m *testing.M) { + os.Exit(testMain(m)) +} + +func testMain(m *testing.M) int { + defer testhelper.MustHaveNoChildProcess() + + configureGitalyHooksBinary() + + return m.Run() +} + +func TestHooksPrePostReceive(t *testing.T) { + secretToken := "secret token" + key := 1234 + glRepository := "some_repo" + + tempGitlabShellDir, cleanup := createTempGitlabShellDir(t) + defer cleanup() + + changes := "abc" + + ts := gitlabTestServer(t, secretToken, key, glRepository, changes, true) + defer ts.Close() + + writeTemporaryConfigFile(t, tempGitlabShellDir, ts.URL) + writeShellSecretFile(t, tempGitlabShellDir, secretToken) + + for _, hook := range []string{"pre-receive", "post-receive"} { + for envName, env := range map[string][]string{"new": env(t, glRepository, tempGitlabShellDir, key), "old": oldEnv(t, glRepository, tempGitlabShellDir, key)} { + t.Run(hook+"."+envName, func(t *testing.T) { + var stderr, stdout bytes.Buffer + stdin := bytes.NewBuffer([]byte(changes)) + cmd := exec.Command(fmt.Sprintf("../../ruby/git-hooks/%s", hook)) + cmd.Stderr = &stderr + cmd.Stdout = &stdout + cmd.Stdin = stdin + cmd.Env = env + + require.NoError(t, cmd.Run()) + require.Empty(t, stderr.String()) + require.Empty(t, stdout.String()) + }) + } + } +} + +func TestHooksUpdate(t *testing.T) { + key := 1234 + glRepository := "some_repo" + + tempGitlabShellDir, cleanup := createTempGitlabShellDir(t) + defer cleanup() + + writeTemporaryConfigFile(t, tempGitlabShellDir, "http://www.example.com") + writeShellSecretFile(t, tempGitlabShellDir, "the wrong token") + + require.NoError(t, os.MkdirAll(filepath.Join(tempGitlabShellDir, "hooks", "update.d"), 0755)) + testhelper.MustRunCommand(t, nil, "cp", "testdata/update", filepath.Join(tempGitlabShellDir, "hooks", "update.d", "update")) + + for envName, env := range map[string][]string{"new": env(t, glRepository, tempGitlabShellDir, key), "old": oldEnv(t, glRepository, tempGitlabShellDir, key)} { + t.Run(envName, func(t *testing.T) { + refval, oldval, newval := "refval", "oldval", "newval" + var stdout, stderr bytes.Buffer + + cmd := exec.Command(fmt.Sprintf("../../ruby/git-hooks/%s", "update"), refval, oldval, newval) + cmd.Env = env + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + require.NoError(t, cmd.Run()) + require.FileExists(t, "testdata/tempfile") + require.Empty(t, stdout.String()) + require.Empty(t, stderr.String()) + + var inputs []string + + f, err := os.Open("testdata/tempfile") + require.NoError(t, err) + require.NoError(t, json.NewDecoder(f).Decode(&inputs)) + require.Equal(t, []string{refval, oldval, newval}, inputs) + require.NoError(t, os.Remove("testdata/tempfile")) + }) + } +} + +func TestHooksPostReceiveFailed(t *testing.T) { + secretToken := "secret token" + key := 1234 + glRepository := "some_repo" + + tempGitlabShellDir, cleanup := createTempGitlabShellDir(t) + defer cleanup() + + // By setting the last parameter to false, the post-receive API call will + // send back {"reference_counter_increased": false}, indicating something went wrong + // with the call + + ts := gitlabTestServer(t, secretToken, key, glRepository, "", false) + defer ts.Close() + + writeTemporaryConfigFile(t, tempGitlabShellDir, ts.URL) + writeShellSecretFile(t, tempGitlabShellDir, secretToken) + + for envName, env := range map[string][]string{"new": env(t, glRepository, tempGitlabShellDir, key), "old": oldEnv(t, glRepository, tempGitlabShellDir, key)} { + t.Run(envName, func(t *testing.T) { + var stdout, stderr bytes.Buffer + + cmd := exec.Command(fmt.Sprintf("../../ruby/git-hooks/%s", "post-receive")) + cmd.Env = env + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + err := cmd.Run() + code, ok := command.ExitStatus(err) + + require.True(t, ok, "expect exit status in %v", err) + require.Equal(t, 1, code, "exit status") + require.Empty(t, stdout.String()) + require.Empty(t, stderr.String()) + }) + } +} + +func TestHooksNotAllowed(t *testing.T) { + secretToken := "secret token" + key := 1234 + glRepository := "some_repo" + + tempGitlabShellDir, cleanup := createTempGitlabShellDir(t) + defer cleanup() + + ts := gitlabTestServer(t, secretToken, key, glRepository, "", true) + defer ts.Close() + + writeTemporaryConfigFile(t, tempGitlabShellDir, ts.URL) + writeShellSecretFile(t, tempGitlabShellDir, "the wrong token") + + for envName, env := range map[string][]string{"new": env(t, glRepository, tempGitlabShellDir, key), "old": oldEnv(t, glRepository, tempGitlabShellDir, key)} { + t.Run(envName, func(t *testing.T) { + var stderr, stdout bytes.Buffer + + cmd := exec.Command(fmt.Sprintf("../../ruby/git-hooks/%s", "pre-receive")) + cmd.Stderr = &stderr + cmd.Stdout = &stdout + cmd.Env = env + + require.Error(t, cmd.Run()) + require.Equal(t, "GitLab: 401 Unauthorized\n", stderr.String()) + require.Equal(t, "", stdout.String()) + }) + } +} + +type GitlabShellConfig struct { + GitlabURL string `yaml:"gitlab_url"` +} + +func handleAllowed(t *testing.T, secretToken string, key int, glRepository, changes string) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + require.NoError(t, r.ParseForm()) + require.Equal(t, http.MethodPost, r.Method) + require.Equal(t, "application/x-www-form-urlencoded", r.Header.Get("Content-Type")) + require.Equal(t, strconv.Itoa(key), r.Form.Get("key_id")) + require.Equal(t, glRepository, r.Form.Get("gl_repository")) + require.Equal(t, "ssh", r.Form.Get("protocol")) + require.Equal(t, changes, r.Form.Get("changes")) + + w.Header().Set("Content-Type", "application/json") + if r.Form.Get("secret_token") == secretToken { + w.Write([]byte(`{"status":true}`)) + return + } + w.WriteHeader(http.StatusUnauthorized) + w.Write([]byte(`{"message":"401 Unauthorized"}`)) + } +} + +func handlePreReceive(t *testing.T, secretToken, glRepository string) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + require.NoError(t, r.ParseForm()) + require.Equal(t, http.MethodPost, r.Method) + require.Equal(t, "application/x-www-form-urlencoded", r.Header.Get("Content-Type")) + require.Equal(t, glRepository, r.Form.Get("gl_repository")) + require.Equal(t, secretToken, r.Form.Get("secret_token")) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"reference_counter_increased": true}`)) + } +} + +func handlePostReceive(t *testing.T, secretToken string, key int, glRepository, changes string, counterDecreased bool) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + require.NoError(t, r.ParseForm()) + require.Equal(t, http.MethodPost, r.Method) + require.Equal(t, "application/x-www-form-urlencoded", r.Header.Get("Content-Type")) + require.Equal(t, glRepository, r.Form.Get("gl_repository")) + require.Equal(t, secretToken, r.Form.Get("secret_token")) + require.Equal(t, fmt.Sprintf("key-%d", key), r.Form.Get("identifier")) + require.Equal(t, changes, r.Form.Get("changes")) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write([]byte(fmt.Sprintf(`{"reference_counter_decreased": %v}`, counterDecreased))) + } +} + +func gitlabTestServer(t *testing.T, secretToken string, key int, glRepository, changes string, postReceiveCounterDecreased bool) *httptest.Server { + mux := http.NewServeMux() + mux.Handle("/api/v4/internal/allowed", http.HandlerFunc(handleAllowed(t, secretToken, key, glRepository, changes))) + mux.Handle("/api/v4/internal/pre_receive", http.HandlerFunc(handlePreReceive(t, secretToken, glRepository))) + mux.Handle("/api/v4/internal/post_receive", http.HandlerFunc(handlePostReceive(t, secretToken, key, glRepository, changes, postReceiveCounterDecreased))) + + return httptest.NewServer(mux) +} + +func createTempGitlabShellDir(t *testing.T) (string, func()) { + tempDir, err := ioutil.TempDir("", "gitlab-shell") + require.NoError(t, err) + return tempDir, func() { + require.NoError(t, os.RemoveAll(tempDir)) + } +} + +func writeTemporaryConfigFile(t *testing.T, dir, testServerURL string) { + cfg := GitlabShellConfig{GitlabURL: testServerURL} + out, err := yaml.Marshal(cfg) + require.NoError(t, err) + require.NoError(t, ioutil.WriteFile(filepath.Join(dir, "config.yml"), out, 0644)) +} + +func env(t *testing.T, glRepo, gitlabShellDir string, key int) []string { + rubyDir, err := filepath.Abs("../../ruby") + require.NoError(t, err) + + return append(oldEnv(t, glRepo, gitlabShellDir, key), []string{ + "GITALY_BIN_DIR=testdata/gitaly-libexec", + fmt.Sprintf("GITALY_RUBY_DIR=%s", rubyDir), + }...) +} + +func oldEnv(t *testing.T, glRepo, gitlabShellDir string, key int) []string { + return append([]string{ + fmt.Sprintf("GL_ID=key-%d", key), + fmt.Sprintf("GL_REPOSITORY=%s", glRepo), + "GL_PROTOCOL=ssh", + fmt.Sprintf("GITALY_GITLAB_SHELL_DIR=%s", gitlabShellDir), + fmt.Sprintf("GITALY_LOG_DIR=%s", gitlabShellDir), + "GITALY_LOG_LEVEL=info", + "GITALY_LOG_FORMAT=json", + fmt.Sprintf("GITALY_LOG_DIR=%s", gitlabShellDir), + }, os.Environ()...) +} + +func writeShellSecretFile(t *testing.T, dir, secretToken string) { + require.NoError(t, ioutil.WriteFile(filepath.Join(dir, ".gitlab_shell_secret"), []byte(secretToken), 0644)) +} + +// configureGitalyHooksBinary builds gitaly-hooks command for tests +func configureGitalyHooksBinary() { + var err error + + config.Config.BinDir, err = filepath.Abs("testdata/gitaly-libexec") + if err != nil { + log.Fatal(err) + } + + goBuildArgs := []string{ + "build", + "-o", + path.Join(config.Config.BinDir, "gitaly-hooks"), + "gitlab.com/gitlab-org/gitaly/cmd/gitaly-hooks", + } + testhelper.MustRunCommand(nil, nil, "go", goBuildArgs...) +} diff --git a/cmd/gitaly-hooks/testdata/update b/cmd/gitaly-hooks/testdata/update new file mode 100755 index 000000000..a4076ec24 --- /dev/null +++ b/cmd/gitaly-hooks/testdata/update @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby +require 'json' + +open('testdata/tempfile', 'w') { |f| f.puts(JSON.dump(ARGV)) } @@ -9,7 +9,6 @@ require ( github.com/grpc-ecosystem/go-grpc-middleware v1.0.0 github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 github.com/kelseyhightower/envconfig v1.3.0 - github.com/kr/pretty v0.1.0 // indirect github.com/libgit2/git2go v0.0.0-20190104134018-ecaeb7a21d47 github.com/prometheus/client_golang v1.0.0 github.com/sirupsen/logrus v1.2.0 @@ -22,6 +21,5 @@ require ( golang.org/x/sys v0.0.0-20190422165155-953cdadca894 google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898 // indirect google.golang.org/grpc v1.16.0 - gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect - gopkg.in/yaml.v2 v2.2.2 // indirect + gopkg.in/yaml.v2 v2.2.2 ) @@ -51,11 +51,6 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= -github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/libgit2/git2go v0.0.0-20190104134018-ecaeb7a21d47 h1:HDt7WT3kpXSHq4mlOuLzgXH9LeOK1qlhyFdKIAzxxeM= github.com/libgit2/git2go v0.0.0-20190104134018-ecaeb7a21d47/go.mod h1:4bKN42efkbNYMZlvDfxGDxzl066GhpvIircZDsm8Y+Y= github.com/lightstep/lightstep-tracer-go v0.15.6 h1:D0GGa7afJ7GcQvu5as6ssLEEKYXvRgKI5d5cevtz8r4= @@ -156,8 +151,6 @@ gopkg.in/DataDog/dd-trace-go.v1 v1.7.0/go.mod h1:DVp8HmDh8PuTu2Z0fVVlBsyWaC++fzw gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= diff --git a/internal/git/receivepack.go b/internal/git/receivepack.go index c9670612f..f4dbdba8c 100644 --- a/internal/git/receivepack.go +++ b/internal/git/receivepack.go @@ -22,8 +22,8 @@ func HookEnv(req ReceivePackRequest) []string { return append([]string{ fmt.Sprintf("GL_ID=%s", req.GetGlId()), fmt.Sprintf("GL_USERNAME=%s", req.GetGlUsername()), - fmt.Sprintf("GITLAB_SHELL_DIR=%s", config.Config.GitlabShell.Dir), fmt.Sprintf("GL_REPOSITORY=%s", req.GetGlRepository()), + fmt.Sprintf("GITLAB_SHELL_DIR=%s", config.Config.GitlabShell.Dir), }, gitlabshell.Env()...) } diff --git a/internal/gitlabshell/env.go b/internal/gitlabshell/env.go index f3ff7bf73..70e391ece 100644 --- a/internal/gitlabshell/env.go +++ b/internal/gitlabshell/env.go @@ -12,5 +12,7 @@ func Env() []string { "GITALY_LOG_FORMAT=" + cfg.Logging.Format, "GITALY_LOG_LEVEL=" + cfg.Logging.Level, "GITLAB_SHELL_DIR=" + cfg.GitlabShell.Dir, //GITLAB_SHELL_DIR has been deprecated + "GITALY_BIN_DIR=" + config.Config.BinDir, + "GITALY_RUBY_DIR=" + config.Config.Ruby.Dir, } } diff --git a/internal/log/hook.go b/internal/log/hook.go new file mode 100644 index 000000000..54789ab3d --- /dev/null +++ b/internal/log/hook.go @@ -0,0 +1,42 @@ +package log + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + + "github.com/sirupsen/logrus" +) + +// HookLogger is a wrapper around *logrus.Logger +type HookLogger struct { + logger *logrus.Logger +} + +// NewHookLogger creates a file logger, since both stderr and stdout will be displayed in git output +func NewHookLogger() *HookLogger { + logger := logrus.New() + + filepath := filepath.Join(os.Getenv("GITALY_LOG_DIR"), "gitaly_hooks.log") + + logFile, err := os.OpenFile(filepath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644) + if err != nil { + logger.SetOutput(ioutil.Discard) + } else { + logger.SetOutput(logFile) + } + + return &HookLogger{logger: logger} +} + +// Fatal logs an error at the Fatal level and writes a generic message to stderr +func (h *HookLogger) Fatal(err error) { + h.Fatalf("%v", err) +} + +// Fatalf logs a formatted error at the Fatal level +func (h *HookLogger) Fatalf(format string, a ...interface{}) { + fmt.Fprintf(os.Stderr, "error executing git hook") + h.logger.Fatalf(format, a...) +} diff --git a/internal/rubyserver/rubyserver.go b/internal/rubyserver/rubyserver.go index 92196f79b..82f806c5c 100644 --- a/internal/rubyserver/rubyserver.go +++ b/internal/rubyserver/rubyserver.go @@ -117,6 +117,7 @@ func (s *Server) start() error { fmt.Sprintf("GITALY_RUBY_WRITE_BUFFER_SIZE=%d", streamio.WriteBufferSize), fmt.Sprintf("GITALY_RUBY_MAX_COMMIT_OR_TAG_MESSAGE_SIZE=%d", helper.MaxCommitOrTagMessageSize), "GITALY_RUBY_GITALY_BIN_DIR="+cfg.BinDir, + "GITALY_RUBY_DIR="+cfg.Ruby.Dir, "GITALY_VERSION="+version.GetVersion(), "GITALY_GIT_HOOKS_DIR="+hooks.Path()) env = append(env, gitlabshell.Env()...) diff --git a/ruby/git-hooks/gitlab-shell-hook b/ruby/git-hooks/gitlab-shell-hook index 7ad435b18..550f31d83 100755 --- a/ruby/git-hooks/gitlab-shell-hook +++ b/ruby/git-hooks/gitlab-shell-hook @@ -1,6 +1,9 @@ #!/bin/sh # This is the single source of truth for where Gitaly's embedded Git hooks are. -hooks_dir="$(dirname $0)/../gitlab-shell/hooks" - -exec "$hooks_dir/$(basename $0)" "$@" +if [ -n "$GITALY_BIN_DIR" ]; then + exec "$GITALY_BIN_DIR/gitaly-hooks" "$(basename $0)" "$@" +else + hooks_dir="$(dirname $0)/../gitlab-shell/hooks" + exec "$hooks_dir/$(basename $0)" "$@" +fi diff --git a/ruby/lib/gitlab/config.rb b/ruby/lib/gitlab/config.rb index 91ab1d741..2763e55f2 100644 --- a/ruby/lib/gitlab/config.rb +++ b/ruby/lib/gitlab/config.rb @@ -52,8 +52,12 @@ module Gitlab class Gitaly include TestSetup - def client_path - @client_path ||= ENV['GITALY_RUBY_GITALY_BIN_DIR'] + def bin_dir + @bin_dir ||= ENV['GITALY_RUBY_GITALY_BIN_DIR'] + end + + def ruby_dir + @ruby_dir ||= ENV['GITALY_RUBY_DIR'] end def rbtrace_enabled? diff --git a/ruby/lib/gitlab/git/hook.rb b/ruby/lib/gitlab/git/hook.rb index 3691f8639..61af0999a 100644 --- a/ruby/lib/gitlab/git/hook.rb +++ b/ruby/lib/gitlab/git/hook.rb @@ -107,6 +107,8 @@ module Gitlab 'GITALY_LOG_DIR' => Gitlab.config.logging.dir, 'GITALY_LOG_LEVEL' => Gitlab.config.logging.level, 'GITALY_LOG_FORMAT' => Gitlab.config.logging.format, + 'GITALY_RUBY_DIR' => Gitlab.config.gitaly.ruby_dir, + 'GITALY_BIN_DIR' => Gitlab.config.gitaly.bin_dir, 'GL_ID' => gl_id, 'GL_USERNAME' => gl_username, 'GL_REPOSITORY' => repository.gl_repository, diff --git a/ruby/lib/gitlab/git/remote_repository.rb b/ruby/lib/gitlab/git/remote_repository.rb index df857176d..721753399 100644 --- a/ruby/lib/gitlab/git/remote_repository.rb +++ b/ruby/lib/gitlab/git/remote_repository.rb @@ -36,7 +36,7 @@ module Gitlab end def fetch_env(git_config_options: []) - gitaly_ssh = File.absolute_path(File.join(Gitlab.config.gitaly.client_path, 'gitaly-ssh')) + gitaly_ssh = File.absolute_path(File.join(Gitlab.config.gitaly.bin_dir, 'gitaly-ssh')) gitaly_address = gitaly_client.address(storage) gitaly_token = gitaly_client.token(storage) diff --git a/ruby/spec/spec_helper.rb b/ruby/spec/spec_helper.rb index 9011b96f3..49e956f92 100644 --- a/ruby/spec/spec_helper.rb +++ b/ruby/spec/spec_helper.rb @@ -14,7 +14,7 @@ Dir[File.join(__dir__, 'support/helpers/*.rb')].each { |f| require f } Gitlab.config.git.test_global_ivar_override(:bin_path, 'git') Gitlab.config.git.test_global_ivar_override(:hooks_directory, File.join(Gitlab.config.gitlab_shell.path.to_s, "hooks")) -Gitlab.config.gitaly.test_global_ivar_override(:client_path, __dir__) +Gitlab.config.gitaly.test_global_ivar_override(:bin_dir, __dir__) RSpec.configure do |config| config.include FactoryBot::Syntax::Methods |