diff options
author | Paul Okstad <pokstad@gitlab.com> | 2020-02-21 18:13:13 +0300 |
---|---|---|
committer | Paul Okstad <pokstad@gitlab.com> | 2020-02-21 18:13:13 +0300 |
commit | de2413f70181a4ee3ce47b54ec6c848a9b5ddc77 (patch) | |
tree | 76bbaced34959630bb334990c6ea7eddc28da7d0 | |
parent | 14a29e8aa33a0067486baeb69ab6d072c7ddcf19 (diff) | |
parent | 43f93995df4c5cd8bc5ffcac2ebf2d2a24cdc32c (diff) |
Merge branch 'jv-clone-analyze-structured' into 'master'
Prepare 'gitaly-debug analyze-http-clone' for prometheus
See merge request gitlab-org/gitaly!1555
-rw-r--r-- | cmd/gitaly-debug/analyzehttp.go | 261 | ||||
-rw-r--r-- | internal/git/stats/analyzehttp.go | 392 | ||||
-rw-r--r-- | internal/git/stats/analyzehttp_test.go | 97 | ||||
-rw-r--r-- | internal/service/repository/create_from_url_test.go | 36 | ||||
-rw-r--r-- | internal/testhelper/githttp.go | 40 |
5 files changed, 578 insertions, 248 deletions
diff --git a/cmd/gitaly-debug/analyzehttp.go b/cmd/gitaly-debug/analyzehttp.go index 9a2749fc7..61cf0ce2e 100644 --- a/cmd/gitaly-debug/analyzehttp.go +++ b/cmd/gitaly-debug/analyzehttp.go @@ -1,243 +1,64 @@ package main import ( - "bytes" - "compress/gzip" + "context" "fmt" - "net/http" - "os" - "strings" - "time" - "gitlab.com/gitlab-org/gitaly/internal/git/pktline" + "gitlab.com/gitlab-org/gitaly/internal/git/stats" ) func analyzeHTTPClone(cloneURL string) { - wants := doBenchGet(cloneURL) - doBenchPost(cloneURL, wants) -} - -func doBenchGet(cloneURL string) []string { - req, err := http.NewRequest("GET", cloneURL+"/info/refs?service=git-upload-pack", nil) - noError(err) - - for k, v := range map[string]string{ - "User-Agent": "gitaly-debug", - "Accept": "*/*", - "Accept-Encoding": "deflate, gzip", - "Pragma": "no-cache", - } { - req.Header.Set(k, v) - } - - start := time.Now() - msg("---") - msg("--- GET %v", req.URL) - msg("---") - resp, err := http.DefaultClient.Do(req) - noError(err) - - msg("response after %v", time.Since(start)) - msg("response header: %v", resp.Header) - msg("HTTP status code %d", resp.StatusCode) - defer resp.Body.Close() - - body := resp.Body - if resp.Header.Get("Content-Encoding") == "gzip" { - body, err = gzip.NewReader(body) - noError(err) - } - - // Expected response: - // - "# service=git-upload-pack\n" - // - FLUSH - // - "<OID> <ref> <capabilities>\n" - // - "<OID> <ref>\n" - // - ... - // - FLUSH - // - var wants []string - var size int64 - seenFlush := false - scanner := pktline.NewScanner(body) - packets := 0 - refs := 0 - for ; scanner.Scan(); packets++ { - if seenFlush { - fatal("received packet after flush") - } - - data := string(pktline.Data(scanner.Bytes())) - size += int64(len(data)) - switch packets { - case 0: - msg("first packet %v", time.Since(start)) - if data != "# service=git-upload-pack\n" { - fatal(fmt.Errorf("unexpected header %q", data)) - } - case 1: - if !pktline.IsFlush(scanner.Bytes()) { - fatal("missing flush after service announcement") - } - default: - if packets == 2 && !strings.Contains(data, " side-band-64k") { - fatal(fmt.Errorf("missing side-band-64k capability in %q", data)) - } - - if pktline.IsFlush(scanner.Bytes()) { - seenFlush = true - continue - } - - split := strings.SplitN(data, " ", 2) - if len(split) != 2 { - continue - } - refs++ - - if strings.HasPrefix(split[1], "refs/heads/") || strings.HasPrefix(split[1], "refs/tags/") { - wants = append(wants, split[0]) - } - } - } - noError(scanner.Err()) - if !seenFlush { - fatal("missing flush in response") + st := &stats.Clone{ + URL: cloneURL, + Interactive: true, } - msg("received %d packets", packets) - msg("done in %v", time.Since(start)) - msg("payload data: %d bytes", size) - msg("received %d refs, selected %d wants", refs, len(wants)) - - return wants -} - -func doBenchPost(cloneURL string, wants []string) { - reqBodyRaw := &bytes.Buffer{} - reqBodyGzip := gzip.NewWriter(reqBodyRaw) - for i, oid := range wants { - if i == 0 { - oid += " multi_ack_detailed no-done side-band-64k thin-pack ofs-delta deepen-since deepen-not agent=git/2.21.0" - } - _, err := pktline.WriteString(reqBodyGzip, "want "+oid+"\n") - noError(err) + noError(st.Perform(context.Background())) + + fmt.Println("\n--- GET metrics:") + for _, entry := range []metric{ + {"response header time", st.Get.ResponseHeader()}, + {"first Git packet", st.Get.FirstGitPacket()}, + {"response body time", st.Get.ResponseBody()}, + {"payload size", st.Get.PayloadSize()}, + {"Git packets received", st.Get.Packets()}, + {"refs advertised", st.Get.RefsAdvertised()}, + {"wanted refs", st.RefsWanted()}, + } { + entry.print() } - noError(pktline.WriteFlush(reqBodyGzip)) - _, err := pktline.WriteString(reqBodyGzip, "done\n") - noError(err) - noError(reqBodyGzip.Close()) - - req, err := http.NewRequest("POST", cloneURL+"/git-upload-pack", reqBodyRaw) - noError(err) - for k, v := range map[string]string{ - "User-Agent": "gitaly-debug", - "Content-Type": "application/x-git-upload-pack-request", - "Accept": "application/x-git-upload-pack-result", - "Content-Encoding": "gzip", + fmt.Println("\n--- POST metrics:") + for _, entry := range []metric{ + {"response header time", st.Post.ResponseHeader()}, + {"time to server NAK", st.Post.NAK()}, + {"response body time", st.Post.ResponseBody()}, + {"largest single Git packet", st.Post.LargestPacketSize()}, + {"Git packets received", st.Post.Packets()}, } { - req.Header.Set(k, v) + entry.print() } - start := time.Now() - msg("---") - msg("--- POST %v", req.URL) - msg("---") - resp, err := http.DefaultClient.Do(req) - noError(err) - - msg("response after %v", time.Since(start)) - msg("response header: %v", resp.Header) - msg("HTTP status code %d", resp.StatusCode) - defer resp.Body.Close() - - // Expected response: - // - "NAK\n" - // - "<side band byte><pack or progress or error data> - // - ... - // - FLUSH - // - packets := 0 - scanner := pktline.NewScanner(resp.Body) - totalSize := make(map[byte]int64) - payloadSizeHistogram := make(map[int]int) - sideBandHistogram := make(map[byte]int) - seenFlush := false - for ; scanner.Scan(); packets++ { - if seenFlush { - fatal("received extra packet after flush") - } - - data := pktline.Data(scanner.Bytes()) - - if packets == 0 { - if !bytes.Equal([]byte("NAK\n"), data) { - fatal(fmt.Errorf("expected NAK, got %q", data)) - } - msg("received NAK after %v", time.Since(start)) - continue - } - - if pktline.IsFlush(scanner.Bytes()) { - seenFlush = true + for _, band := range stats.Bands() { + numPackets := st.Post.BandPackets(band) + if numPackets == 0 { continue } - if len(data) == 0 { - fatal("empty packet in PACK data") - } - - band := data[0] - if band < 1 || band > 3 { - fatal(fmt.Errorf("invalid sideband: %d", band)) - } - if sideBandHistogram[band] == 0 { - msg("received first %s packet after %v", bandToHuman(band), time.Since(start)) - } - - sideBandHistogram[band]++ - - // Print progress data as-is - if band == 2 { - _, err := os.Stdout.Write(data[1:]) - noError(err) - } - - n := len(data[1:]) - totalSize[band] += int64(n) - payloadSizeHistogram[n]++ - - if packets%100 == 0 && packets > 0 && band == 1 { - fmt.Printf(".") + fmt.Printf("\n--- POST %s band\n", band) + for _, entry := range []metric{ + {"time to first packet", st.Post.BandFirstPacket(band)}, + {"packets", numPackets}, + {"total payload size", st.Post.BandPayloadSize(band)}, + } { + entry.print() } } - - fmt.Println("") // Trailing newline for progress dots. - - noError(scanner.Err()) - if !seenFlush { - fatal("POST response did not end in flush") - } - - msg("received %d packets", packets) - msg("done in %v", time.Since(start)) - for i := byte(1); i <= 3; i++ { - msg("%8s band: %10d payload bytes, %6d packets", bandToHuman(i), totalSize[i], sideBandHistogram[i]) - } - msg("packet payload size histogram: %v", payloadSizeHistogram) } -func bandToHuman(b byte) string { - switch b { - case 1: - return "pack" - case 2: - return "progress" - case 3: - return "error" - default: - fatal(fmt.Errorf("invalid band %d", b)) - return "" // never reached - } +type metric struct { + key string + value interface{} } + +func (m metric) print() { fmt.Printf("%-40s %v\n", m.key, m.value) } diff --git a/internal/git/stats/analyzehttp.go b/internal/git/stats/analyzehttp.go new file mode 100644 index 000000000..5fb87349a --- /dev/null +++ b/internal/git/stats/analyzehttp.go @@ -0,0 +1,392 @@ +package stats + +import ( + "bytes" + "compress/gzip" + "context" + "errors" + "fmt" + "net/http" + "os" + "strings" + "time" + + "gitlab.com/gitlab-org/gitaly/internal/git/pktline" +) + +type Clone struct { + URL string + Interactive bool + + wants []string // all branch and tag pointers + Get + Post +} + +func (cl *Clone) RefsWanted() int { return len(cl.wants) } + +// Perform does a Git HTTP clone, discarding cloned data to /dev/null. +func (cl *Clone) Perform(ctx context.Context) error { + if err := cl.doGet(ctx); err != nil { + return ctxErr(ctx, err) + } + + if err := cl.doPost(ctx); err != nil { + return ctxErr(ctx, err) + } + + return nil +} + +func ctxErr(ctx context.Context, err error) error { + if ctx.Err() != nil { + return ctx.Err() + } + return err +} + +type Get struct { + start time.Time + responseHeader time.Duration + httpStatus int + firstGitPacket time.Duration + responseBody time.Duration + payloadSize int64 + packets int + refs int +} + +func (g *Get) ResponseHeader() time.Duration { return g.responseHeader } +func (g *Get) HTTPStatus() int { return g.httpStatus } +func (g *Get) FirstGitPacket() time.Duration { return g.firstGitPacket } +func (g *Get) ResponseBody() time.Duration { return g.responseBody } +func (g *Get) PayloadSize() int64 { return g.payloadSize } +func (g *Get) Packets() int { return g.packets } +func (g *Get) RefsAdvertised() int { return g.refs } + +func (cl *Clone) doGet(ctx context.Context) error { + req, err := http.NewRequest("GET", cl.URL+"/info/refs?service=git-upload-pack", nil) + if err != nil { + return err + } + + req = req.WithContext(ctx) + + for k, v := range map[string]string{ + "User-Agent": "gitaly-debug", + "Accept": "*/*", + "Accept-Encoding": "deflate, gzip", + "Pragma": "no-cache", + } { + req.Header.Set(k, v) + } + + cl.Get.start = time.Now() + cl.printInteractive("---") + cl.printInteractive("--- GET %v", req.URL) + cl.printInteractive("---") + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + cl.Get.responseHeader = time.Since(cl.Get.start) + cl.Get.httpStatus = resp.StatusCode + cl.printInteractive("response code: %d", resp.StatusCode) + cl.printInteractive("response header: %v", resp.Header) + + body := resp.Body + if resp.Header.Get("Content-Encoding") == "gzip" { + body, err = gzip.NewReader(body) + if err != nil { + return err + } + } + + // Expected response: + // - "# service=git-upload-pack\n" + // - FLUSH + // - "<OID> <ref> <capabilities>\n" + // - "<OID> <ref>\n" + // - ... + // - FLUSH + // + seenFlush := false + scanner := pktline.NewScanner(body) + for ; scanner.Scan(); cl.Get.packets++ { + if seenFlush { + return errors.New("received packet after flush") + } + + data := string(pktline.Data(scanner.Bytes())) + cl.Get.payloadSize += int64(len(data)) + switch cl.Get.packets { + case 0: + cl.Get.firstGitPacket = time.Since(cl.Get.start) + + if data != "# service=git-upload-pack\n" { + return fmt.Errorf("unexpected header %q", data) + } + case 1: + if !pktline.IsFlush(scanner.Bytes()) { + return errors.New("missing flush after service announcement") + } + case 2: + if !strings.Contains(data, " side-band-64k") { + return fmt.Errorf("missing side-band-64k capability in %q", data) + } + + fallthrough + default: + if pktline.IsFlush(scanner.Bytes()) { + seenFlush = true + continue + } + + split := strings.SplitN(data, " ", 2) + if len(split) != 2 { + continue + } + cl.Get.refs++ + + if strings.HasPrefix(split[1], "refs/heads/") || strings.HasPrefix(split[1], "refs/tags/") { + cl.wants = append(cl.wants, split[0]) + } + } + } + if err := scanner.Err(); err != nil { + return err + } + if !seenFlush { + return errors.New("missing flush in response") + } + + cl.Get.responseBody = time.Since(cl.Get.start) + + return nil +} + +type Post struct { + start time.Time + responseHeader time.Duration + httpStatus int + nak time.Duration + multiband map[string]*bandInfo + responseBody time.Duration + packets int + largestPacketSize int +} + +func (p *Post) ResponseHeader() time.Duration { return p.responseHeader } +func (p *Post) HTTPStatus() int { return p.httpStatus } +func (p *Post) NAK() time.Duration { return p.nak } +func (p *Post) ResponseBody() time.Duration { return p.responseBody } +func (p *Post) Packets() int { return p.packets } +func (p *Post) LargestPacketSize() int { return p.largestPacketSize } + +func (p *Post) BandPackets(b string) int { return p.multiband[b].packets } +func (p *Post) BandPayloadSize(b string) int64 { return p.multiband[b].size } +func (p *Post) BandFirstPacket(b string) time.Duration { return p.multiband[b].firstPacket } + +type bandInfo struct { + firstPacket time.Duration + size int64 + packets int +} + +func (bi *bandInfo) consume(start time.Time, data []byte) { + if bi.packets == 0 { + bi.firstPacket = time.Since(start) + } + bi.size += int64(len(data)) + bi.packets++ +} + +// See +// https://github.com/git/git/blob/v2.25.0/Documentation/technical/http-protocol.txt#L351 +// for background information. +func (cl *Clone) buildPost(ctx context.Context) (*http.Request, error) { + reqBodyRaw := &bytes.Buffer{} + reqBodyGzip := gzip.NewWriter(reqBodyRaw) + for i, oid := range cl.wants { + if i == 0 { + oid += " multi_ack_detailed no-done side-band-64k thin-pack ofs-delta deepen-since deepen-not agent=git/2.21.0" + } + if _, err := pktline.WriteString(reqBodyGzip, "want "+oid+"\n"); err != nil { + return nil, err + } + } + if err := pktline.WriteFlush(reqBodyGzip); err != nil { + return nil, err + } + if _, err := pktline.WriteString(reqBodyGzip, "done\n"); err != nil { + return nil, err + } + if err := reqBodyGzip.Close(); err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", cl.URL+"/git-upload-pack", reqBodyRaw) + if err != nil { + return nil, err + } + + req = req.WithContext(ctx) + + for k, v := range map[string]string{ + "User-Agent": "gitaly-debug", + "Content-Type": "application/x-git-upload-pack-request", + "Accept": "application/x-git-upload-pack-result", + "Content-Encoding": "gzip", + } { + req.Header.Set(k, v) + } + + return req, nil +} + +func (cl *Clone) doPost(ctx context.Context) error { + req, err := cl.buildPost(ctx) + if err != nil { + return err + } + + cl.Post.start = time.Now() + cl.printInteractive("---") + cl.printInteractive("--- POST %v", req.URL) + cl.printInteractive("---") + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + cl.Post.responseHeader = time.Since(cl.Post.start) + cl.Post.httpStatus = resp.StatusCode + cl.printInteractive("response code: %d", resp.StatusCode) + cl.printInteractive("response header: %v", resp.Header) + + // Expected response: + // - "NAK\n" + // - "<side band byte><pack or progress or error data> + // - ... + // - FLUSH + // + + cl.Post.multiband = make(map[string]*bandInfo) + for _, band := range Bands() { + cl.Post.multiband[band] = &bandInfo{} + } + + seenFlush := false + + scanner := pktline.NewScanner(resp.Body) + for ; scanner.Scan(); cl.Post.packets++ { + if seenFlush { + return errors.New("received extra packet after flush") + } + + if n := len(scanner.Bytes()); n > cl.Post.largestPacketSize { + cl.Post.largestPacketSize = n + } + + data := pktline.Data(scanner.Bytes()) + + if cl.Post.packets == 0 { + // We're now looking at the first git packet sent by the server. The + // server must conclude the ref negotiation. Because we have not sent any + // "have" messages there is nothing to negotiate and the server should + // send a single NAK. + if !bytes.Equal([]byte("NAK\n"), data) { + return fmt.Errorf("expected NAK, got %q", data) + } + cl.Post.nak = time.Since(cl.Post.start) + continue + } + + if pktline.IsFlush(scanner.Bytes()) { + seenFlush = true + continue + } + + if len(data) == 0 { + return errors.New("empty packet in PACK data") + } + + band, err := bandToHuman(data[0]) + if err != nil { + return err + } + + cl.Post.multiband[band].consume(cl.Post.start, data[1:]) + + // Print progress data as-is + if cl.Interactive && band == bandProgress { + if _, err := os.Stdout.Write(data[1:]); err != nil { + return err + } + } + + if cl.Interactive && cl.Post.packets%500 == 0 && cl.Post.packets > 0 && band == bandPack { + // Print dots to have some sort of progress meter for the user in + // interactive mode. It's not accurate progress, but it shows that + // something is happening. + if _, err := fmt.Print("."); err != nil { + return err + } + } + } + + if cl.Interactive { + // Trailing newline for progress dots. + if _, err := fmt.Println(""); err != nil { + return err + } + } + + if err := scanner.Err(); err != nil { + return err + } + if !seenFlush { + return errors.New("POST response did not end in flush") + } + + cl.Post.responseBody = time.Since(cl.Post.start) + return nil +} + +func (cl *Clone) printInteractive(format string, a ...interface{}) error { + if !cl.Interactive { + return nil + } + + if _, err := fmt.Println(fmt.Sprintf(format, a...)); err != nil { + return err + } + + return nil +} + +const ( + bandPack = "pack" + bandProgress = "progress" + bandError = "error" +) + +// These bands map to magic numbers 1, 2, 3. See +// https://git-scm.com/docs/protocol-capabilities/2.24.0#_side_band_side_band_64k +func Bands() []string { return []string{bandPack, bandProgress, bandError} } + +func bandToHuman(b byte) (string, error) { + bands := Bands() + + // Band index bytes are 1-indexed. + if b < 1 || int(b) > len(bands) { + return "", fmt.Errorf("invalid band index: %d", b) + } + + return bands[b-1], nil +} diff --git a/internal/git/stats/analyzehttp_test.go b/internal/git/stats/analyzehttp_test.go new file mode 100644 index 000000000..4b4e7ee24 --- /dev/null +++ b/internal/git/stats/analyzehttp_test.go @@ -0,0 +1,97 @@ +package stats + +import ( + "fmt" + "path/filepath" + "testing" + "time" + + "github.com/stretchr/testify/require" + "gitlab.com/gitlab-org/gitaly/internal/testhelper" +) + +func TestClone(t *testing.T) { + ctx, cancel := testhelper.Context() + defer cancel() + + _, repoPath, cleanup := testhelper.NewTestRepo(t) + defer cleanup() + + serverPort, stopGitServer := testhelper.GitServer(t, repoPath, nil) + defer stopGitServer() + + clone := Clone{URL: fmt.Sprintf("http://localhost:%d/%s", serverPort, filepath.Base(repoPath))} + require.NoError(t, clone.Perform(ctx), "perform analysis clone") + + const expectedWants = 90 // based on contents of _support/gitlab-test.git-packed-refs + require.Greater(t, clone.RefsWanted(), expectedWants, "number of wanted refs") + + require.Equal(t, 200, clone.Get.HTTPStatus(), "get status") + require.Greater(t, clone.Get.Packets(), 0, "number of get packets") + require.Greater(t, clone.Get.PayloadSize(), int64(0), "get payload size") + + previousValue := time.Duration(0) + for _, m := range []struct { + desc string + value time.Duration + }{ + {"time to receive response header", clone.Get.ResponseHeader()}, + {"time to first packet", clone.Get.FirstGitPacket()}, + {"time to receive response body", clone.Get.ResponseBody()}, + } { + require.True(t, m.value > previousValue, "get: expect %s (%v) to be greater than previous value %v", m.desc, m.value, previousValue) + previousValue = m.value + } + + require.Equal(t, 200, clone.Post.HTTPStatus(), "post status") + require.Greater(t, clone.Post.Packets(), 0, "number of post packets") + + require.Greater(t, clone.Post.BandPackets("progress"), 0, "number of progress packets") + require.Greater(t, clone.Post.BandPackets("pack"), 0, "number of pack packets") + + require.Greater(t, clone.Post.BandPayloadSize("progress"), int64(0), "progress payload bytes") + require.Greater(t, clone.Post.BandPayloadSize("pack"), int64(0), "pack payload bytes") + + previousValue = time.Duration(0) + for _, m := range []struct { + desc string + value time.Duration + }{ + {"time to receive response header", clone.Post.ResponseHeader()}, + {"time to receive NAK", clone.Post.NAK()}, + {"time to receive first progress message", clone.Post.BandFirstPacket("progress")}, + {"time to receive first pack message", clone.Post.BandFirstPacket("pack")}, + {"time to receive response body", clone.Post.ResponseBody()}, + } { + require.True(t, m.value > previousValue, "post: expect %s (%v) to be greater than previous value %v", m.desc, m.value, previousValue) + previousValue = m.value + } +} + +func TestBandToHuman(t *testing.T) { + testCases := []struct { + in byte + out string + fail bool + }{ + {in: 0, fail: true}, + {in: 1, out: "pack"}, + {in: 2, out: "progress"}, + {in: 3, out: "error"}, + {in: 4, fail: true}, + } + + for _, tc := range testCases { + t.Run(fmt.Sprintf("band index %d", tc.in), func(t *testing.T) { + out, err := bandToHuman(tc.in) + + if tc.fail { + require.Error(t, err) + return + } + + require.NoError(t, err) + require.Equal(t, tc.out, out, "band name") + }) + } +} diff --git a/internal/service/repository/create_from_url_test.go b/internal/service/repository/create_from_url_test.go index 4c9cf1796..d3f45f672 100644 --- a/internal/service/repository/create_from_url_test.go +++ b/internal/service/repository/create_from_url_test.go @@ -4,16 +4,13 @@ import ( "encoding/base64" "fmt" "io/ioutil" - "net" "net/http" - "net/http/cgi" "os" "path" "path/filepath" "testing" "github.com/stretchr/testify/require" - "gitlab.com/gitlab-org/gitaly/internal/config" "gitlab.com/gitlab-org/gitaly/internal/helper" "gitlab.com/gitlab-org/gitaly/internal/testhelper" "gitlab.com/gitlab-org/gitaly/proto/go/gitalypb" @@ -40,7 +37,9 @@ func TestSuccessfulCreateRepositoryFromURLRequest(t *testing.T) { user := "username123" password := "password321localhost" - port := gitServerWithBasicAuth(t, user, password, testRepoPath) + port, stopGitServer := gitServerWithBasicAuth(t, user, password, testRepoPath) + defer stopGitServer() + url := fmt.Sprintf("http://%s:%s@localhost:%d/%s", user, password, port, filepath.Base(testRepoPath)) req := &gitalypb.CreateRepositoryFromURLRequest{ @@ -172,35 +171,16 @@ func TestPreventingRedirect(t *testing.T) { require.Error(t, err) } -func gitServerWithBasicAuth(t testing.TB, user, pass, repoPath string) int { - f, err := os.Create(filepath.Join(repoPath, "git-daemon-export-ok")) - require.NoError(t, err) - require.NoError(t, f.Close()) - - listener, err := net.Listen("tcp", ":0") - require.NoError(t, err) - - s := http.Server{ - Handler: basicAuthMiddleware(t, user, pass, &cgi.Handler{ - Path: config.Config.Git.BinPath, - Dir: "/", - Args: []string{"http-backend"}, - Env: []string{ - "GIT_PROJECT_ROOT=" + filepath.Dir(repoPath), - }, - }), - } - go s.Serve(listener) - - return listener.Addr().(*net.TCPAddr).Port +func gitServerWithBasicAuth(t testing.TB, user, pass, repoPath string) (int, func() error) { + return testhelper.GitServer(t, repoPath, basicAuthMiddleware(t, user, pass)) } -func basicAuthMiddleware(t testing.TB, user, pass string, next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { +func basicAuthMiddleware(t testing.TB, user, pass string) func(http.ResponseWriter, *http.Request, http.Handler) { + return func(w http.ResponseWriter, r *http.Request, next http.Handler) { authUser, authPass, ok := r.BasicAuth() require.True(t, ok, "should contain basic auth") require.Equal(t, user, authUser, "username should match") require.Equal(t, pass, authPass, "password should match") next.ServeHTTP(w, r) - }) + } } diff --git a/internal/testhelper/githttp.go b/internal/testhelper/githttp.go new file mode 100644 index 000000000..0c1416978 --- /dev/null +++ b/internal/testhelper/githttp.go @@ -0,0 +1,40 @@ +package testhelper + +import ( + "io/ioutil" + "net" + "net/http" + "net/http/cgi" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" + "gitlab.com/gitlab-org/gitaly/internal/config" +) + +func GitServer(t testing.TB, repoPath string, middleware func(http.ResponseWriter, *http.Request, http.Handler)) (int, func() error) { + require.NoError(t, ioutil.WriteFile(filepath.Join(repoPath, "git-daemon-export-ok"), nil, 0644)) + + listener, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + + gitHTTPBackend := &cgi.Handler{ + Path: config.Config.Git.BinPath, + Dir: "/", + Args: []string{"http-backend"}, + Env: []string{ + "GIT_PROJECT_ROOT=" + filepath.Dir(repoPath), + }, + } + s := http.Server{Handler: gitHTTPBackend} + + if middleware != nil { + s.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + middleware(w, r, gitHTTPBackend) + }) + } + + go s.Serve(listener) + + return listener.Addr().(*net.TCPAddr).Port, s.Close +} |