debian-mirror-gitlab/workhorse/gitaly_integration_test.go

423 lines
13 KiB
Go
Raw Normal View History

2021-02-22 17:27:13 +05:30
// 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"
2023-04-23 21:23:45 +05:30
"net/url"
2021-02-22 17:27:13 +05:30
"os"
"os/exec"
"path"
"regexp"
"strconv"
"strings"
"testing"
"github.com/stretchr/testify/require"
2022-08-13 15:12:31 +05:30
"gitlab.com/gitlab-org/gitaly/v15/proto/go/gitalypb"
2022-10-11 01:57:18 +05:30
"gitlab.com/gitlab-org/gitaly/v15/streamio"
2021-02-22 17:27:13 +05:30
2021-10-27 15:23:28 +05:30
"gitlab.com/gitlab-org/gitlab/workhorse/internal/api"
"gitlab.com/gitlab-org/gitlab/workhorse/internal/gitaly"
"gitlab.com/gitlab-org/gitlab/workhorse/internal/testhelper"
2021-02-22 17:27:13 +05:30
)
var (
2023-04-23 21:23:45 +05:30
gitalyAddresses []string
2021-02-22 17:27:13 +05:30
)
2023-04-23 21:23:45 +05:30
// Convert from tcp://127.0.0.1:8075 to dns scheme variants:
// * dns:127.0.0.1:8075
// * dns:///127.0.0.1:8075
func convertToDNSSchemes(address string) []string {
uri, err := url.Parse(address)
if err != nil {
panic(fmt.Sprintf("invalid GITALY_ADDRESS url %s: %s", address, err))
}
return []string{
fmt.Sprintf("dns:///%s", uri.Host),
fmt.Sprintf("dns:%s", uri.Host),
}
}
func jsonGitalyServer(address string) string {
return fmt.Sprintf(`"GitalyServer":{"Address":"%s", "Token": ""}`, address)
}
2021-02-22 17:27:13 +05:30
func init() {
2023-04-23 21:23:45 +05:30
rawAddress := os.Getenv("GITALY_ADDRESS")
if rawAddress != "" {
gitalyAddresses = append(gitalyAddresses, rawAddress)
gitalyAddresses = append(gitalyAddresses, convertToDNSSchemes(rawAddress)...)
}
2021-02-22 17:27:13 +05:30
}
func skipUnlessRealGitaly(t *testing.T) {
2023-04-23 21:23:45 +05:30
t.Log(gitalyAddresses)
if len(gitalyAddresses) != 0 {
2021-02-22 17:27:13 +05:30
return
}
t.Skip(`Please set GITALY_ADDRESS="..." to run Gitaly integration tests`)
}
2023-04-23 21:23:45 +05:30
func realGitalyAuthResponse(gitalyAddress string, apiResponse *api.Response) *api.Response {
2021-02-22 17:27:13 +05:30
apiResponse.GitalyServer.Address = gitalyAddress
return apiResponse
}
2023-04-23 21:23:45 +05:30
func realGitalyOkBody(t *testing.T, gitalyAddress string) *api.Response {
return realGitalyAuthResponse(gitalyAddress, gitOkBody(t))
2021-02-22 17:27:13 +05:30
}
func ensureGitalyRepository(t *testing.T, apiResponse *api.Response) error {
2022-07-16 23:28:13 +05:30
ctx, namespace, err := gitaly.NewNamespaceClient(
context.Background(),
apiResponse.GitalyServer,
)
2021-02-22 17:27:13 +05:30
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
}
2022-10-11 01:57:18 +05:30
stream, err := repository.CreateRepositoryFromBundle(ctx)
if err != nil {
return fmt.Errorf("initiate stream: %w", err)
}
if err := stream.Send(&gitalypb.CreateRepositoryFromBundleRequest{Repository: &apiResponse.Repository}); err != nil {
return err
}
gitBundle := exec.Command("git", "-C", path.Join(testRepoRoot, testRepo), "bundle", "create", "-", "--all")
gitBundle.Stdout = streamio.NewWriter(func(p []byte) error {
return stream.Send(&gitalypb.CreateRepositoryFromBundleRequest{Data: p})
})
if err := gitBundle.Run(); err != nil {
return fmt.Errorf("run git bundle --create: %w", err)
}
if _, err := stream.CloseAndRecv(); err != nil {
return fmt.Errorf("finish CreateRepositoryFromBundle: %w", err)
2021-02-22 17:27:13 +05:30
}
2022-10-11 01:57:18 +05:30
return nil
2021-02-22 17:27:13 +05:30
}
func TestAllowedClone(t *testing.T) {
skipUnlessRealGitaly(t)
2023-04-23 21:23:45 +05:30
for _, gitalyAddress := range gitalyAddresses {
t.Run(gitalyAddress, func(t *testing.T) {
// Create the repository in the Gitaly server
apiResponse := realGitalyOkBody(t, gitalyAddress)
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)
})
}
2021-02-22 17:27:13 +05:30
}
func TestAllowedShallowClone(t *testing.T) {
skipUnlessRealGitaly(t)
2023-04-23 21:23:45 +05:30
for _, gitalyAddress := range gitalyAddresses {
t.Run(gitalyAddress, func(t *testing.T) {
// Create the repository in the Gitaly server
apiResponse := realGitalyOkBody(t, gitalyAddress)
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)
})
}
2021-02-22 17:27:13 +05:30
}
func TestAllowedPush(t *testing.T) {
skipUnlessRealGitaly(t)
2023-04-23 21:23:45 +05:30
for _, gitalyAddress := range gitalyAddresses {
t.Run(gitalyAddress, func(t *testing.T) {
// Create the repository in the Gitaly server
apiResponse := realGitalyOkBody(t, gitalyAddress)
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)
})
}
2021-02-22 17:27:13 +05:30
}
func TestAllowedGetGitBlob(t *testing.T) {
skipUnlessRealGitaly(t)
2023-04-23 21:23:45 +05:30
for _, gitalyAddress := range gitalyAddresses {
t.Run(gitalyAddress, func(t *testing.T) {
// Create the repository in the Gitaly server
apiResponse := realGitalyOkBody(t, gitalyAddress)
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(gitalyAddress), 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)
})
}
2021-02-22 17:27:13 +05:30
}
func TestAllowedGetGitArchive(t *testing.T) {
skipUnlessRealGitaly(t)
2023-04-23 21:23:45 +05:30
for _, gitalyAddress := range gitalyAddresses {
t.Run(gitalyAddress, func(t *testing.T) {
// Create the repository in the Gitaly server
apiResponse := realGitalyOkBody(t, gitalyAddress)
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
}
}
2021-02-22 17:27:13 +05:30
2023-04-23 21:23:45 +05:30
require.True(t, foundEntry, "Couldn't find %v directory entry", archivePrefix)
})
2021-02-22 17:27:13 +05:30
}
}
func TestAllowedGetGitArchiveOldPayload(t *testing.T) {
skipUnlessRealGitaly(t)
2023-04-23 21:23:45 +05:30
for _, gitalyAddress := range gitalyAddresses {
t.Run(gitalyAddress, func(t *testing.T) {
// Create the repository in the Gitaly server
apiResponse := realGitalyOkBody(t, gitalyAddress)
repo := &apiResponse.Repository
require.NoError(t, ensureGitalyRepository(t, apiResponse))
2021-02-22 17:27:13 +05:30
2023-04-23 21:23:45 +05:30
archivePath := path.Join(scratchDir, "my/path")
archivePrefix := "repo-1"
2021-02-22 17:27:13 +05:30
2023-04-23 21:23:45 +05:30
jsonParams := fmt.Sprintf(
`{
2021-02-22 17:27:13 +05:30
%s,
"GitalyRepository":{"storage_name":"%s","relative_path":"%s"},
"ArchivePath":"%s",
"ArchivePrefix":"%s",
"CommitId":"%s"
}`,
2023-04-23 21:23:45 +05:30
jsonGitalyServer(gitalyAddress), 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
}
}
2021-02-22 17:27:13 +05:30
2023-04-23 21:23:45 +05:30
require.True(t, foundEntry, "Couldn't find %v directory entry", archivePrefix)
})
2021-02-22 17:27:13 +05:30
}
}
func TestAllowedGetGitDiff(t *testing.T) {
skipUnlessRealGitaly(t)
2023-04-23 21:23:45 +05:30
for _, gitalyAddress := range gitalyAddresses {
t.Run(gitalyAddress, func(t *testing.T) {
// Create the repository in the Gitaly server
apiResponse := realGitalyOkBody(t, gitalyAddress)
require.NoError(t, ensureGitalyRepository(t, apiResponse))
msg := serializedMessage("RawDiffRequest", &gitalypb.RawDiffRequest{
Repository: &apiResponse.Repository,
LeftCommitId: "b0e52af38d7ea43cf41d8a6f2471351ac036d6c9",
RightCommitId: "732401c65e924df81435deb12891ef570167d2e2",
})
jsonParams := buildGitalyRPCParams(gitalyAddress, msg)
resp, body, err := doSendDataRequest("/something", "git-diff", 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)
expectedBody := "diff --git a/LICENSE b/LICENSE\n"
require.Equal(t, expectedBody, string(body[:len(expectedBody)]),
"GET %q: response body", resp.Request.URL)
})
}
2021-02-22 17:27:13 +05:30
}
func TestAllowedGetGitFormatPatch(t *testing.T) {
skipUnlessRealGitaly(t)
2023-04-23 21:23:45 +05:30
for _, gitalyAddress := range gitalyAddresses {
t.Run(gitalyAddress, func(t *testing.T) {
// Create the repository in the Gitaly server
apiResponse := realGitalyOkBody(t, gitalyAddress)
require.NoError(t, ensureGitalyRepository(t, apiResponse))
msg := serializedMessage("RawPatchRequest", &gitalypb.RawPatchRequest{
Repository: &apiResponse.Repository,
LeftCommitId: "b0e52af38d7ea43cf41d8a6f2471351ac036d6c9",
RightCommitId: "0e1b353b348f8477bdbec1ef47087171c5032cd9",
})
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,
"732401c65e924df81435deb12891ef570167d2e2",
"33bcff41c232a11727ac6d660bd4b0c2ba86d63d",
"0e1b353b348f8477bdbec1ef47087171c5032cd9",
)
})
}
2021-02-22 17:27:13 +05:30
}
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")
}