// Tests in this file need access to a real Gitaly server to run. The address // is supplied via the GITALY_ADDRESS environment variable package main import ( "archive/tar" "bufio" "bytes" "context" "fmt" "os" "os/exec" "path" "regexp" "strconv" "strings" "testing" "github.com/sirupsen/logrus" "github.com/stretchr/testify/require" "gitlab.com/gitlab-org/gitaly/v14/proto/go/gitalypb" "gitlab.com/gitlab-org/gitlab/workhorse/internal/api" "gitlab.com/gitlab-org/gitlab/workhorse/internal/gitaly" "gitlab.com/gitlab-org/gitlab/workhorse/internal/testhelper" ) var ( gitalyAddress string jsonGitalyServer string ) func init() { gitalyAddress = os.Getenv("GITALY_ADDRESS") jsonGitalyServer = fmt.Sprintf(`"GitalyServer":{"Address":"%s", "Token": ""}`, gitalyAddress) } func skipUnlessRealGitaly(t *testing.T) { t.Log(gitalyAddress) if gitalyAddress != "" { return } t.Skip(`Please set GITALY_ADDRESS="..." to run Gitaly integration tests`) } func realGitalyAuthResponse(apiResponse *api.Response) *api.Response { apiResponse.GitalyServer.Address = gitalyAddress return apiResponse } func realGitalyOkBody(t *testing.T) *api.Response { return realGitalyAuthResponse(gitOkBody(t)) } func realGitalyOkBodyWithSidechannel(t *testing.T) *api.Response { return realGitalyAuthResponse(gitOkBodyWithSidechannel(t)) } func ensureGitalyRepository(t *testing.T, apiResponse *api.Response) error { ctx, namespace, err := gitaly.NewNamespaceClient(context.Background(), apiResponse.GitalyServer) if err != nil { return err } ctx, repository, err := gitaly.NewRepositoryClient(ctx, apiResponse.GitalyServer) if err != nil { return err } // Remove the repository if it already exists, for consistency rmNsReq := &gitalypb.RemoveNamespaceRequest{ StorageName: apiResponse.Repository.StorageName, Name: apiResponse.Repository.RelativePath, } _, err = namespace.RemoveNamespace(ctx, rmNsReq) if err != nil { return err } createReq := &gitalypb.CreateRepositoryFromURLRequest{ Repository: &apiResponse.Repository, Url: "https://gitlab.com/gitlab-org/gitlab-test.git", } _, err = repository.CreateRepositoryFromURL(ctx, createReq) return err } func TestAllowedClone(t *testing.T) { testAllowedClone(t, realGitalyOkBody(t)) } func TestAllowedCloneWithSidechannel(t *testing.T) { gitaly.InitializeSidechannelRegistry(logrus.StandardLogger()) testAllowedClone(t, realGitalyOkBodyWithSidechannel(t)) } func testAllowedClone(t *testing.T, apiResponse *api.Response) { skipUnlessRealGitaly(t) // Create the repository in the Gitaly server require.NoError(t, ensureGitalyRepository(t, apiResponse)) // Prepare test server and backend ts := testAuthServer(t, nil, nil, 200, apiResponse) defer ts.Close() ws := startWorkhorseServer(ts.URL) defer ws.Close() // Do the git clone require.NoError(t, os.RemoveAll(scratchDir)) cloneCmd := exec.Command("git", "clone", fmt.Sprintf("%s/%s", ws.URL, testRepo), checkoutDir) runOrFail(t, cloneCmd) // We may have cloned an 'empty' repository, 'git log' will fail in it logCmd := exec.Command("git", "log", "-1", "--oneline") logCmd.Dir = checkoutDir runOrFail(t, logCmd) } func TestAllowedShallowClone(t *testing.T) { testAllowedShallowClone(t, realGitalyOkBody(t)) } func TestAllowedShallowCloneWithSidechannel(t *testing.T) { gitaly.InitializeSidechannelRegistry(logrus.StandardLogger()) testAllowedShallowClone(t, realGitalyOkBodyWithSidechannel(t)) } func testAllowedShallowClone(t *testing.T, apiResponse *api.Response) { skipUnlessRealGitaly(t) // Create the repository in the Gitaly server require.NoError(t, ensureGitalyRepository(t, apiResponse)) // Prepare test server and backend ts := testAuthServer(t, nil, nil, 200, apiResponse) defer ts.Close() ws := startWorkhorseServer(ts.URL) defer ws.Close() // Shallow git clone (depth 1) require.NoError(t, os.RemoveAll(scratchDir)) cloneCmd := exec.Command("git", "clone", "--depth", "1", fmt.Sprintf("%s/%s", ws.URL, testRepo), checkoutDir) runOrFail(t, cloneCmd) // We may have cloned an 'empty' repository, 'git log' will fail in it logCmd := exec.Command("git", "log", "-1", "--oneline") logCmd.Dir = checkoutDir runOrFail(t, logCmd) } func TestAllowedPush(t *testing.T) { skipUnlessRealGitaly(t) // Create the repository in the Gitaly server apiResponse := realGitalyOkBody(t) require.NoError(t, ensureGitalyRepository(t, apiResponse)) // Prepare the test server and backend ts := testAuthServer(t, nil, nil, 200, apiResponse) defer ts.Close() ws := startWorkhorseServer(ts.URL) defer ws.Close() // Perform the git push pushCmd := exec.Command("git", "push", fmt.Sprintf("%s/%s", ws.URL, testRepo), fmt.Sprintf("master:%s", newBranch())) pushCmd.Dir = checkoutDir runOrFail(t, pushCmd) } func TestAllowedGetGitBlob(t *testing.T) { skipUnlessRealGitaly(t) // Create the repository in the Gitaly server apiResponse := realGitalyOkBody(t) require.NoError(t, ensureGitalyRepository(t, apiResponse)) // the LICENSE file in the test repository oid := "50b27c6518be44c42c4d87966ae2481ce895624c" expectedBody := "The MIT License (MIT)" bodyLen := 1075 jsonParams := fmt.Sprintf( `{ %s, "GetBlobRequest":{ "repository":{"storage_name":"%s", "relative_path":"%s"}, "oid":"%s", "limit":-1 } }`, jsonGitalyServer, apiResponse.Repository.StorageName, apiResponse.Repository.RelativePath, oid, ) resp, body, err := doSendDataRequest("/something", "git-blob", jsonParams) require.NoError(t, err) shortBody := string(body[:len(expectedBody)]) require.Equal(t, 200, resp.StatusCode, "GET %q: status code", resp.Request.URL) require.Equal(t, expectedBody, shortBody, "GET %q: response body", resp.Request.URL) testhelper.RequireResponseHeader(t, resp, "Content-Length", strconv.Itoa(bodyLen)) requireNginxResponseBuffering(t, "no", resp, "GET %q: nginx response buffering", resp.Request.URL) } func TestAllowedGetGitArchive(t *testing.T) { skipUnlessRealGitaly(t) // Create the repository in the Gitaly server apiResponse := realGitalyOkBody(t) require.NoError(t, ensureGitalyRepository(t, apiResponse)) archivePath := path.Join(scratchDir, "my/path") archivePrefix := "repo-1" msg := serializedProtoMessage("GetArchiveRequest", &gitalypb.GetArchiveRequest{ Repository: &apiResponse.Repository, CommitId: "HEAD", Prefix: archivePrefix, Format: gitalypb.GetArchiveRequest_TAR, Path: []byte("files"), }) jsonParams := buildGitalyRPCParams(gitalyAddress, rpcArg{"ArchivePath", archivePath}, msg) resp, body, err := doSendDataRequest("/archive.tar", "git-archive", jsonParams) require.NoError(t, err) require.Equal(t, 200, resp.StatusCode, "GET %q: status code", resp.Request.URL) requireNginxResponseBuffering(t, "no", resp, "GET %q: nginx response buffering", resp.Request.URL) // Ensure the tar file is readable foundEntry := false tr := tar.NewReader(bytes.NewReader(body)) for { hdr, err := tr.Next() if err != nil { break } if hdr.Name == archivePrefix+"/" { foundEntry = true break } } require.True(t, foundEntry, "Couldn't find %v directory entry", archivePrefix) } func TestAllowedGetGitArchiveOldPayload(t *testing.T) { skipUnlessRealGitaly(t) // Create the repository in the Gitaly server apiResponse := realGitalyOkBody(t) repo := &apiResponse.Repository require.NoError(t, ensureGitalyRepository(t, apiResponse)) archivePath := path.Join(scratchDir, "my/path") archivePrefix := "repo-1" jsonParams := fmt.Sprintf( `{ %s, "GitalyRepository":{"storage_name":"%s","relative_path":"%s"}, "ArchivePath":"%s", "ArchivePrefix":"%s", "CommitId":"%s" }`, jsonGitalyServer, repo.StorageName, repo.RelativePath, archivePath, archivePrefix, "HEAD", ) resp, body, err := doSendDataRequest("/archive.tar", "git-archive", jsonParams) require.NoError(t, err) require.Equal(t, 200, resp.StatusCode, "GET %q: status code", resp.Request.URL) requireNginxResponseBuffering(t, "no", resp, "GET %q: nginx response buffering", resp.Request.URL) // Ensure the tar file is readable foundEntry := false tr := tar.NewReader(bytes.NewReader(body)) for { hdr, err := tr.Next() if err != nil { break } if hdr.Name == archivePrefix+"/" { foundEntry = true break } } require.True(t, foundEntry, "Couldn't find %v directory entry", archivePrefix) } func TestAllowedGetGitDiff(t *testing.T) { skipUnlessRealGitaly(t) // Create the repository in the Gitaly server apiResponse := realGitalyOkBody(t) require.NoError(t, ensureGitalyRepository(t, apiResponse)) leftCommit := "8a0f2ee90d940bfb0ba1e14e8214b0649056e4ab" rightCommit := "e395f646b1499e8e0279445fc99a0596a65fab7e" expectedBody := "diff --git a/README.md b/README.md" msg := serializedMessage("RawDiffRequest", &gitalypb.RawDiffRequest{ Repository: &apiResponse.Repository, LeftCommitId: leftCommit, RightCommitId: rightCommit, }) jsonParams := buildGitalyRPCParams(gitalyAddress, msg) resp, body, err := doSendDataRequest("/something", "git-diff", jsonParams) require.NoError(t, err) shortBody := string(body[:len(expectedBody)]) require.Equal(t, 200, resp.StatusCode, "GET %q: status code", resp.Request.URL) require.Equal(t, expectedBody, shortBody, "GET %q: response body", resp.Request.URL) requireNginxResponseBuffering(t, "no", resp, "GET %q: nginx response buffering", resp.Request.URL) } func TestAllowedGetGitFormatPatch(t *testing.T) { skipUnlessRealGitaly(t) // Create the repository in the Gitaly server apiResponse := realGitalyOkBody(t) require.NoError(t, ensureGitalyRepository(t, apiResponse)) leftCommit := "8a0f2ee90d940bfb0ba1e14e8214b0649056e4ab" rightCommit := "e395f646b1499e8e0279445fc99a0596a65fab7e" msg := serializedMessage("RawPatchRequest", &gitalypb.RawPatchRequest{ Repository: &apiResponse.Repository, LeftCommitId: leftCommit, RightCommitId: rightCommit, }) jsonParams := buildGitalyRPCParams(gitalyAddress, msg) resp, body, err := doSendDataRequest("/something", "git-format-patch", jsonParams) require.NoError(t, err) require.Equal(t, 200, resp.StatusCode, "GET %q: status code", resp.Request.URL) requireNginxResponseBuffering(t, "no", resp, "GET %q: nginx response buffering", resp.Request.URL) requirePatchSeries( t, body, "372ab6950519549b14d220271ee2322caa44d4eb", "57290e673a4c87f51294f5216672cbc58d485d25", "41ae11ba5d091d73d5de671f6fa7d1a4539e979e", "742518b2be68fc750bb4c357c0df821a88113286", rightCommit, ) } var extractPatchSeriesMatcher = regexp.MustCompile(`^From (\w+)`) // RequirePatchSeries takes a `git format-patch` blob, extracts the From xxxxx // lines and compares the SHAs to expected list. func requirePatchSeries(t *testing.T, blob []byte, expected ...string) { t.Helper() var actual []string footer := make([]string, 3) scanner := bufio.NewScanner(bytes.NewReader(blob)) for scanner.Scan() { line := scanner.Text() if matches := extractPatchSeriesMatcher.FindStringSubmatch(line); len(matches) == 2 { actual = append(actual, matches[1]) } footer = []string{footer[1], footer[2], line} } require.Equal(t, strings.Join(expected, "\n"), strings.Join(actual, "\n"), "patch series") // Check the last returned patch is complete // Don't assert on the final line, it is a git version require.Equal(t, "-- ", footer[0], "end of patch marker") }