437 lines
17 KiB
Go
437 lines
17 KiB
Go
package upstream
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/sirupsen/logrus"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
apipkg "gitlab.com/gitlab-org/gitlab/workhorse/internal/api"
|
|
"gitlab.com/gitlab-org/gitlab/workhorse/internal/config"
|
|
"gitlab.com/gitlab-org/gitlab/workhorse/internal/helper"
|
|
"gitlab.com/gitlab-org/gitlab/workhorse/internal/testhelper"
|
|
"gitlab.com/gitlab-org/gitlab/workhorse/internal/upstream/roundtripper"
|
|
)
|
|
|
|
const (
|
|
geoProxyEndpoint = "/api/v4/geo/proxy"
|
|
testDocumentRoot = "testdata/public"
|
|
)
|
|
|
|
type testCase struct {
|
|
desc string
|
|
path string
|
|
expectedResponse string
|
|
}
|
|
|
|
func TestMain(m *testing.M) {
|
|
// Secret should be configured before any Geo API poll happens to prevent
|
|
// race conditions where the first API call happens without a secret path
|
|
testhelper.ConfigureSecret()
|
|
|
|
os.Exit(m.Run())
|
|
}
|
|
|
|
func TestRouting(t *testing.T) {
|
|
handle := func(u *upstream, regex string) routeEntry {
|
|
handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
io.WriteString(w, regex)
|
|
})
|
|
return u.route("", regex, handler)
|
|
}
|
|
|
|
const (
|
|
foobar = `\A/foobar\z`
|
|
quxbaz = `\A/quxbaz\z`
|
|
main = ""
|
|
)
|
|
|
|
u := newUpstream(config.Config{}, logrus.StandardLogger(), func(u *upstream) {
|
|
u.Routes = []routeEntry{
|
|
handle(u, foobar),
|
|
handle(u, quxbaz),
|
|
handle(u, main),
|
|
}
|
|
}, nil)
|
|
ts := httptest.NewServer(u)
|
|
defer ts.Close()
|
|
|
|
testCases := []testCase{
|
|
{"main route works", "/", main},
|
|
{"foobar route works", "/foobar", foobar},
|
|
{"quxbaz route works", "/quxbaz", quxbaz},
|
|
{"path traversal works, ends up in quxbaz", "/foobar/../quxbaz", quxbaz},
|
|
{"escaped path traversal does not match any route", "/foobar%2f%2e%2e%2fquxbaz", main},
|
|
{"double escaped path traversal does not match any route", "/foobar%252f%252e%252e%252fquxbaz", main},
|
|
}
|
|
|
|
runTestCases(t, ts, testCases)
|
|
}
|
|
|
|
func TestPollGeoProxyApiStopsWhenExplicitlyDisabled(t *testing.T) {
|
|
up := upstream{
|
|
enableGeoProxyFeature: false,
|
|
geoProxyPollSleep: func(time.Duration) {},
|
|
geoPollerDone: make(chan struct{}),
|
|
}
|
|
|
|
go up.pollGeoProxyAPI()
|
|
|
|
select {
|
|
case <-up.geoPollerDone:
|
|
// happy
|
|
case <-time.After(10 * time.Second):
|
|
t.Fatal("timeout")
|
|
}
|
|
}
|
|
|
|
func TestPollGeoProxyApiStopsWhenGeoNotEnabled(t *testing.T) {
|
|
remoteServer, rsDeferredClose := startRemoteServer("Geo primary")
|
|
defer rsDeferredClose()
|
|
|
|
geoProxyEndpointResponseBody := `{"geo_enabled":false}`
|
|
railsServer, deferredClose := startRailsServer("Local Rails server", &geoProxyEndpointResponseBody)
|
|
defer deferredClose()
|
|
|
|
cfg := newUpstreamConfig(railsServer.URL)
|
|
roundTripper := roundtripper.NewBackendRoundTripper(cfg.Backend, "", 1*time.Minute, true)
|
|
remoteServerUrl := helper.URLMustParse(remoteServer.URL)
|
|
|
|
up := upstream{
|
|
Config: *cfg,
|
|
RoundTripper: roundTripper,
|
|
APIClient: apipkg.NewAPI(remoteServerUrl, "", roundTripper),
|
|
enableGeoProxyFeature: true,
|
|
geoProxyPollSleep: func(time.Duration) {},
|
|
geoPollerDone: make(chan struct{}),
|
|
}
|
|
|
|
go up.pollGeoProxyAPI()
|
|
|
|
select {
|
|
case <-up.geoPollerDone:
|
|
// happy
|
|
case <-time.After(10 * time.Second):
|
|
t.Fatal("timeout")
|
|
}
|
|
}
|
|
|
|
// This test can be removed when the environment variable `GEO_SECONDARY_PROXY` is removed
|
|
func TestGeoProxyFeatureDisabledOnGeoSecondarySite(t *testing.T) {
|
|
// We could just not set up the primary, but then we'd have to assert
|
|
// that the internal API call isn't made. This is easier.
|
|
remoteServer, rsDeferredClose := startRemoteServer("Geo primary")
|
|
defer rsDeferredClose()
|
|
|
|
geoProxyEndpointResponseBody := fmt.Sprintf(`{"geo_enabled":true,"geo_proxy_url":"%v"}`, remoteServer.URL)
|
|
railsServer, deferredClose := startRailsServer("Local Rails server", &geoProxyEndpointResponseBody)
|
|
defer deferredClose()
|
|
|
|
ws, wsDeferredClose, _ := startWorkhorseServer(railsServer.URL, false)
|
|
defer wsDeferredClose()
|
|
|
|
testCases := []testCase{
|
|
{"jobs request is served locally", "/api/v4/jobs/request", "Local Rails server received request to path /api/v4/jobs/request"},
|
|
{"health check is served locally", "/-/health", "Local Rails server received request to path /-/health"},
|
|
{"unknown route is served locally", "/anything", "Local Rails server received request to path /anything"},
|
|
}
|
|
|
|
runTestCases(t, ws, testCases)
|
|
}
|
|
|
|
func TestGeoProxyFeatureEnabledOnGeoSecondarySite(t *testing.T) {
|
|
testCases := []testCase{
|
|
{"push from secondary is forwarded", "/-/push_from_secondary/foo/bar.git/info/refs", "Geo primary received request to path /-/push_from_secondary/foo/bar.git/info/refs"},
|
|
{"LFS files are served locally", "/group/project.git/gitlab-lfs/objects/37446575700829a11278ad3a550f244f45d5ae4fe1552778fa4f041f9eaeecf6", "Local Rails server received request to path /group/project.git/gitlab-lfs/objects/37446575700829a11278ad3a550f244f45d5ae4fe1552778fa4f041f9eaeecf6"},
|
|
{"jobs request is forwarded", "/api/v4/jobs/request", "Geo primary received request to path /api/v4/jobs/request"},
|
|
{"health check is served locally", "/-/health", "Local Rails server received request to path /-/health"},
|
|
{"unknown route is forwarded", "/anything", "Geo primary received request to path /anything"},
|
|
}
|
|
|
|
runTestCasesWithGeoProxyEnabled(t, testCases)
|
|
}
|
|
|
|
// This test can be removed when the environment variable `GEO_SECONDARY_PROXY` is removed
|
|
func TestGeoProxyFeatureDisabledOnNonGeoSecondarySite(t *testing.T) {
|
|
geoProxyEndpointResponseBody := `{"geo_enabled":false}`
|
|
railsServer, deferredClose := startRailsServer("Local Rails server", &geoProxyEndpointResponseBody)
|
|
defer deferredClose()
|
|
|
|
ws, wsDeferredClose, _ := startWorkhorseServer(railsServer.URL, false)
|
|
defer wsDeferredClose()
|
|
|
|
testCases := []testCase{
|
|
{"LFS files are served locally", "/group/project.git/gitlab-lfs/objects/37446575700829a11278ad3a550f244f45d5ae4fe1552778fa4f041f9eaeecf6", "Local Rails server received request to path /group/project.git/gitlab-lfs/objects/37446575700829a11278ad3a550f244f45d5ae4fe1552778fa4f041f9eaeecf6"},
|
|
{"jobs request is served locally", "/api/v4/jobs/request", "Local Rails server received request to path /api/v4/jobs/request"},
|
|
{"health check is served locally", "/-/health", "Local Rails server received request to path /-/health"},
|
|
{"unknown route is served locally", "/anything", "Local Rails server received request to path /anything"},
|
|
}
|
|
|
|
runTestCases(t, ws, testCases)
|
|
}
|
|
|
|
func TestGeoProxyFeatureEnabledOnNonGeoSecondarySite(t *testing.T) {
|
|
geoProxyEndpointResponseBody := `{"geo_enabled":false}`
|
|
railsServer, deferredClose := startRailsServer("Local Rails server", &geoProxyEndpointResponseBody)
|
|
defer deferredClose()
|
|
|
|
ws, wsDeferredClose, _ := startWorkhorseServer(railsServer.URL, true)
|
|
defer wsDeferredClose()
|
|
|
|
testCases := []testCase{
|
|
{"LFS files are served locally", "/group/project.git/gitlab-lfs/objects/37446575700829a11278ad3a550f244f45d5ae4fe1552778fa4f041f9eaeecf6", "Local Rails server received request to path /group/project.git/gitlab-lfs/objects/37446575700829a11278ad3a550f244f45d5ae4fe1552778fa4f041f9eaeecf6"},
|
|
{"jobs request is served locally", "/api/v4/jobs/request", "Local Rails server received request to path /api/v4/jobs/request"},
|
|
{"health check is served locally", "/-/health", "Local Rails server received request to path /-/health"},
|
|
{"unknown route is served locally", "/anything", "Local Rails server received request to path /anything"},
|
|
}
|
|
|
|
runTestCases(t, ws, testCases)
|
|
}
|
|
|
|
func TestGeoProxyFeatureEnabledButWithAPIError(t *testing.T) {
|
|
geoProxyEndpointResponseBody := "Invalid response"
|
|
railsServer, deferredClose := startRailsServer("Local Rails server", &geoProxyEndpointResponseBody)
|
|
defer deferredClose()
|
|
|
|
ws, wsDeferredClose, _ := startWorkhorseServer(railsServer.URL, true)
|
|
defer wsDeferredClose()
|
|
|
|
testCases := []testCase{
|
|
{"LFS files are served locally", "/group/project.git/gitlab-lfs/objects/37446575700829a11278ad3a550f244f45d5ae4fe1552778fa4f041f9eaeecf6", "Local Rails server received request to path /group/project.git/gitlab-lfs/objects/37446575700829a11278ad3a550f244f45d5ae4fe1552778fa4f041f9eaeecf6"},
|
|
{"jobs request is served locally", "/api/v4/jobs/request", "Local Rails server received request to path /api/v4/jobs/request"},
|
|
{"health check is served locally", "/-/health", "Local Rails server received request to path /-/health"},
|
|
{"unknown route is served locally", "/anything", "Local Rails server received request to path /anything"},
|
|
}
|
|
|
|
runTestCases(t, ws, testCases)
|
|
}
|
|
|
|
func TestGeoProxyFeatureEnablingAndDisabling(t *testing.T) {
|
|
remoteServer, rsDeferredClose := startRemoteServer("Geo primary")
|
|
defer rsDeferredClose()
|
|
|
|
geoProxyEndpointEnabledResponseBody := fmt.Sprintf(`{"geo_enabled":true,"geo_proxy_url":"%v"}`, remoteServer.URL)
|
|
geoProxyEndpointDisabledResponseBody := `{"geo_enabled":true}`
|
|
geoProxyEndpointResponseBody := geoProxyEndpointEnabledResponseBody
|
|
|
|
railsServer, deferredClose := startRailsServer("Local Rails server", &geoProxyEndpointResponseBody)
|
|
defer deferredClose()
|
|
|
|
ws, wsDeferredClose, waitForNextApiPoll := startWorkhorseServer(railsServer.URL, true)
|
|
defer wsDeferredClose()
|
|
|
|
testCasesLocal := []testCase{
|
|
{"LFS files are served locally", "/group/project.git/gitlab-lfs/objects/37446575700829a11278ad3a550f244f45d5ae4fe1552778fa4f041f9eaeecf6", "Local Rails server received request to path /group/project.git/gitlab-lfs/objects/37446575700829a11278ad3a550f244f45d5ae4fe1552778fa4f041f9eaeecf6"},
|
|
{"jobs request is served locally", "/api/v4/jobs/request", "Local Rails server received request to path /api/v4/jobs/request"},
|
|
{"health check is served locally", "/-/health", "Local Rails server received request to path /-/health"},
|
|
{"unknown route is served locally", "/anything", "Local Rails server received request to path /anything"},
|
|
}
|
|
|
|
testCasesProxied := []testCase{
|
|
{"push from secondary is forwarded", "/-/push_from_secondary/foo/bar.git/info/refs", "Geo primary received request to path /-/push_from_secondary/foo/bar.git/info/refs"},
|
|
{"LFS files are served locally", "/group/project.git/gitlab-lfs/objects/37446575700829a11278ad3a550f244f45d5ae4fe1552778fa4f041f9eaeecf6", "Local Rails server received request to path /group/project.git/gitlab-lfs/objects/37446575700829a11278ad3a550f244f45d5ae4fe1552778fa4f041f9eaeecf6"},
|
|
{"jobs request is forwarded", "/api/v4/jobs/request", "Geo primary received request to path /api/v4/jobs/request"},
|
|
{"health check is served locally", "/-/health", "Local Rails server received request to path /-/health"},
|
|
{"unknown route is forwarded", "/anything", "Geo primary received request to path /anything"},
|
|
}
|
|
|
|
// Enabled initially, run tests
|
|
runTestCases(t, ws, testCasesProxied)
|
|
|
|
// Disable proxying and run tests. It's safe to write to
|
|
// geoProxyEndpointResponseBody because the polling goroutine is blocked.
|
|
geoProxyEndpointResponseBody = geoProxyEndpointDisabledResponseBody
|
|
waitForNextApiPoll()
|
|
|
|
runTestCases(t, ws, testCasesLocal)
|
|
|
|
// Re-enable proxying and run tests
|
|
geoProxyEndpointResponseBody = geoProxyEndpointEnabledResponseBody
|
|
waitForNextApiPoll()
|
|
|
|
runTestCases(t, ws, testCasesProxied)
|
|
}
|
|
|
|
func TestGeoProxyUpdatesExtraDataWhenChanged(t *testing.T) {
|
|
var expectedGeoProxyExtraData string
|
|
|
|
remoteServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
require.Equal(t, "1", r.Header.Get("Gitlab-Workhorse-Geo-Proxy"), "custom proxy header")
|
|
require.Equal(t, expectedGeoProxyExtraData, r.Header.Get("Gitlab-Workhorse-Geo-Proxy-Extra-Data"), "custom extra data header")
|
|
w.WriteHeader(http.StatusOK)
|
|
}))
|
|
defer remoteServer.Close()
|
|
|
|
geoProxyEndpointExtraData1 := fmt.Sprintf(`{"geo_enabled":true,"geo_proxy_url":"%v","geo_proxy_extra_data":"data1"}`, remoteServer.URL)
|
|
geoProxyEndpointExtraData2 := fmt.Sprintf(`{"geo_enabled":true,"geo_proxy_url":"%v","geo_proxy_extra_data":"data2"}`, remoteServer.URL)
|
|
geoProxyEndpointExtraData3 := fmt.Sprintf(`{"geo_enabled":true,"geo_proxy_url":"%v"}`, remoteServer.URL)
|
|
geoProxyEndpointResponseBody := geoProxyEndpointExtraData1
|
|
expectedGeoProxyExtraData = "data1"
|
|
|
|
railsServer, deferredClose := startRailsServer("Local Rails server", &geoProxyEndpointResponseBody)
|
|
defer deferredClose()
|
|
|
|
ws, wsDeferredClose, waitForNextApiPoll := startWorkhorseServer(railsServer.URL, true)
|
|
defer wsDeferredClose()
|
|
|
|
http.Get(ws.URL)
|
|
|
|
// Verify that the expected header changes after next updated poll.
|
|
geoProxyEndpointResponseBody = geoProxyEndpointExtraData2
|
|
expectedGeoProxyExtraData = "data2"
|
|
waitForNextApiPoll()
|
|
|
|
http.Get(ws.URL)
|
|
|
|
// Validate that non-existing extra data results in empty header
|
|
geoProxyEndpointResponseBody = geoProxyEndpointExtraData3
|
|
expectedGeoProxyExtraData = ""
|
|
waitForNextApiPoll()
|
|
|
|
http.Get(ws.URL)
|
|
}
|
|
|
|
func TestGeoProxySetsCustomHeader(t *testing.T) {
|
|
testCases := []struct {
|
|
desc string
|
|
json string
|
|
extraData string
|
|
}{
|
|
{"no extra data", `{"geo_enabled":true,"geo_proxy_url":"%v"}`, ""},
|
|
{"with extra data", `{"geo_enabled":true,"geo_proxy_url":"%v","geo_proxy_extra_data":"extra-geo-data"}`, "extra-geo-data"},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.desc, func(t *testing.T) {
|
|
remoteServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
require.Equal(t, "1", r.Header.Get("Gitlab-Workhorse-Geo-Proxy"), "custom proxy header")
|
|
require.Equal(t, tc.extraData, r.Header.Get("Gitlab-Workhorse-Geo-Proxy-Extra-Data"), "custom proxy extra data header")
|
|
w.WriteHeader(http.StatusOK)
|
|
}))
|
|
defer remoteServer.Close()
|
|
|
|
geoProxyEndpointResponseBody := fmt.Sprintf(tc.json, remoteServer.URL)
|
|
railsServer, deferredClose := startRailsServer("Local Rails server", &geoProxyEndpointResponseBody)
|
|
defer deferredClose()
|
|
|
|
ws, wsDeferredClose, _ := startWorkhorseServer(railsServer.URL, true)
|
|
defer wsDeferredClose()
|
|
|
|
http.Get(ws.URL)
|
|
})
|
|
}
|
|
}
|
|
|
|
func runTestCases(t *testing.T, ws *httptest.Server, testCases []testCase) {
|
|
t.Helper()
|
|
for _, tc := range testCases {
|
|
t.Run(tc.desc, func(t *testing.T) {
|
|
resp, err := http.Get(ws.URL + tc.path)
|
|
require.NoError(t, err)
|
|
defer resp.Body.Close()
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, 200, resp.StatusCode, "response code")
|
|
require.Equal(t, tc.expectedResponse, string(body))
|
|
})
|
|
}
|
|
}
|
|
|
|
func runTestCasesWithGeoProxyEnabled(t *testing.T, testCases []testCase) {
|
|
remoteServer, rsDeferredClose := startRemoteServer("Geo primary")
|
|
defer rsDeferredClose()
|
|
|
|
geoProxyEndpointResponseBody := fmt.Sprintf(`{"geo_enabled":true,"geo_proxy_url":"%v"}`, remoteServer.URL)
|
|
railsServer, deferredClose := startRailsServer("Local Rails server", &geoProxyEndpointResponseBody)
|
|
defer deferredClose()
|
|
|
|
ws, wsDeferredClose, _ := startWorkhorseServer(railsServer.URL, true)
|
|
defer wsDeferredClose()
|
|
|
|
runTestCases(t, ws, testCases)
|
|
}
|
|
|
|
func newUpstreamConfig(authBackend string) *config.Config {
|
|
return &config.Config{
|
|
Version: "123",
|
|
DocumentRoot: testDocumentRoot,
|
|
Backend: helper.URLMustParse(authBackend),
|
|
ImageResizerConfig: config.DefaultImageResizerConfig,
|
|
}
|
|
}
|
|
|
|
func startRemoteServer(serverName string) (*httptest.Server, func()) {
|
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
body := serverName + " received request to path " + r.URL.Path
|
|
|
|
w.WriteHeader(200)
|
|
fmt.Fprint(w, body)
|
|
}))
|
|
|
|
return ts, ts.Close
|
|
}
|
|
|
|
func startRailsServer(railsServerName string, geoProxyEndpointResponseBody *string) (*httptest.Server, func()) {
|
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
var body string
|
|
|
|
if r.URL.Path == geoProxyEndpoint {
|
|
w.Header().Set("Content-Type", "application/vnd.gitlab-workhorse+json")
|
|
body = *geoProxyEndpointResponseBody
|
|
} else {
|
|
body = railsServerName + " received request to path " + r.URL.Path
|
|
}
|
|
|
|
w.WriteHeader(200)
|
|
fmt.Fprint(w, body)
|
|
}))
|
|
|
|
return ts, ts.Close
|
|
}
|
|
|
|
func startWorkhorseServer(railsServerURL string, enableGeoProxyFeature bool) (*httptest.Server, func(), func()) {
|
|
geoProxySleepC := make(chan struct{})
|
|
geoProxySleep := func(time.Duration) {
|
|
geoProxySleepC <- struct{}{}
|
|
<-geoProxySleepC
|
|
}
|
|
|
|
myConfigureRoutes := func(u *upstream) {
|
|
// Enable environment variable "feature flag"
|
|
u.enableGeoProxyFeature = enableGeoProxyFeature
|
|
|
|
// Replace the time.Sleep function with geoProxySleep
|
|
u.geoProxyPollSleep = geoProxySleep
|
|
|
|
// call original
|
|
configureRoutes(u)
|
|
}
|
|
cfg := newUpstreamConfig(railsServerURL)
|
|
upstreamHandler := newUpstream(*cfg, logrus.StandardLogger(), myConfigureRoutes, nil)
|
|
ws := httptest.NewServer(upstreamHandler)
|
|
|
|
waitForNextApiPoll := func() {}
|
|
|
|
if enableGeoProxyFeature {
|
|
// Wait for geoProxySleep to be entered for the first time
|
|
<-geoProxySleepC
|
|
|
|
waitForNextApiPoll = func() {
|
|
// Cause geoProxySleep to return
|
|
geoProxySleepC <- struct{}{}
|
|
|
|
// Wait for geoProxySleep to be entered again
|
|
<-geoProxySleepC
|
|
}
|
|
}
|
|
|
|
return ws, ws.Close, waitForNextApiPoll
|
|
}
|