diff options
Diffstat (limited to 'workhorse')
-rw-r--r-- | workhorse/config_test.go | 32 | ||||
-rw-r--r-- | workhorse/internal/api/api.go | 46 | ||||
-rw-r--r-- | workhorse/internal/api/api_test.go | 74 | ||||
-rw-r--r-- | workhorse/internal/upstream/routes.go | 7 | ||||
-rw-r--r-- | workhorse/internal/upstream/upstream.go | 33 | ||||
-rw-r--r-- | workhorse/main.go | 13 |
6 files changed, 190 insertions, 15 deletions
diff --git a/workhorse/config_test.go b/workhorse/config_test.go index b1b04cb9a65..5a2743f375a 100644 --- a/workhorse/config_test.go +++ b/workhorse/config_test.go @@ -101,6 +101,38 @@ func TestConfigDefaults(t *testing.T) { require.Equal(t, expectedCfg, cfg) } +func TestCableConfigDefault(t *testing.T) { + backendURL, err := url.Parse("http://localhost:1234") + require.NoError(t, err) + + args := []string{ + "-authBackend", backendURL.String(), + } + boot, cfg, err := buildConfig("test", args) + require.NoError(t, err, "build config") + + expectedBoot := &bootConfig{ + secretPath: "./.gitlab_workhorse_secret", + listenAddr: "localhost:8181", + listenNetwork: "tcp", + logFormat: "text", + } + + require.Equal(t, expectedBoot, boot) + + expectedCfg := &config.Config{ + Backend: backendURL, + CableBackend: backendURL, + Version: "(unknown version)", + DocumentRoot: "public", + ProxyHeadersTimeout: 5 * time.Minute, + APIQueueTimeout: queueing.DefaultTimeout, + APICILongPollingDuration: 50 * time.Nanosecond, + ImageResizerConfig: config.DefaultImageResizerConfig, + } + require.Equal(t, expectedCfg, cfg) +} + func TestConfigFlagParsing(t *testing.T) { backendURL, err := url.Parse("http://localhost:1234") require.NoError(t, err) diff --git a/workhorse/internal/api/api.go b/workhorse/internal/api/api.go index 5dae6eb01bb..db1c4cbbc27 100644 --- a/workhorse/internal/api/api.go +++ b/workhorse/internal/api/api.go @@ -3,6 +3,7 @@ package api import ( "bytes" "encoding/json" + "errors" "fmt" "io" "net/http" @@ -29,6 +30,8 @@ const ( ResponseContentType = "application/vnd.gitlab-workhorse+json" failureResponseLimit = 32768 + + geoProxyEndpointPath = "/api/v4/geo/proxy" ) type API struct { @@ -37,6 +40,8 @@ type API struct { Version string } +var ErrNotGeoSecondary = errors.New("this is not a Geo secondary site") + var ( requestsCounter = promauto.NewCounterVec( prometheus.CounterOpts{ @@ -61,6 +66,10 @@ func NewAPI(myURL *url.URL, version string, roundTripper http.RoundTripper) *API } } +type GeoProxyEndpointResponse struct { + GeoProxyURL string `json:"geo_proxy_url"` +} + type HandleFunc func(http.ResponseWriter, *http.Request, *Response) type MultipartUploadParams struct { @@ -389,3 +398,40 @@ func bufferResponse(r io.Reader) (*bytes.Buffer, error) { func validResponseContentType(resp *http.Response) bool { return helper.IsContentType(ResponseContentType, resp.Header.Get("Content-Type")) } + +// TODO: Cache the result of the API requests https://gitlab.com/gitlab-org/gitlab/-/issues/329671 +func (api *API) GetGeoProxyURL() (*url.URL, error) { + geoProxyApiUrl := *api.URL + geoProxyApiUrl.Path, geoProxyApiUrl.RawPath = joinURLPath(api.URL, geoProxyEndpointPath) + geoProxyApiReq := &http.Request{ + Method: "GET", + URL: &geoProxyApiUrl, + Header: make(http.Header), + } + + httpResponse, err := api.doRequestWithoutRedirects(geoProxyApiReq) + if err != nil { + return nil, fmt.Errorf("GetGeoProxyURL: do request: %v", err) + } + defer httpResponse.Body.Close() + + if httpResponse.StatusCode != http.StatusOK { + return nil, fmt.Errorf("GetGeoProxyURL: Received HTTP status code: %v", httpResponse.StatusCode) + } + + response := &GeoProxyEndpointResponse{} + if err := json.NewDecoder(httpResponse.Body).Decode(response); err != nil { + return nil, fmt.Errorf("GetGeoProxyURL: decode response: %v", err) + } + + if response.GeoProxyURL == "" { + return nil, ErrNotGeoSecondary + } + + geoProxyURL, err := url.Parse(response.GeoProxyURL) + if err != nil { + return nil, fmt.Errorf("GetGeoProxyURL: Could not parse Geo proxy URL: %v, err: %v", response.GeoProxyURL, err) + } + + return geoProxyURL, nil +} diff --git a/workhorse/internal/api/api_test.go b/workhorse/internal/api/api_test.go new file mode 100644 index 00000000000..5ab677c4233 --- /dev/null +++ b/workhorse/internal/api/api_test.go @@ -0,0 +1,74 @@ +package api + +import ( + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "regexp" + "testing" + + "github.com/stretchr/testify/require" + "gitlab.com/gitlab-org/labkit/log" + + "gitlab.com/gitlab-org/gitlab-workhorse/internal/helper" + "gitlab.com/gitlab-org/gitlab-workhorse/internal/secret" + "gitlab.com/gitlab-org/gitlab-workhorse/internal/testhelper" + "gitlab.com/gitlab-org/gitlab-workhorse/internal/upstream/roundtripper" +) + +func TestGetGeoProxyURLWhenGeoSecondary(t *testing.T) { + geoProxyURL, err := getGeoProxyURLGivenResponse(t, `{"geo_proxy_url":"http://primary"}`) + + require.NoError(t, err) + require.NotNil(t, geoProxyURL) + require.Equal(t, "http://primary", geoProxyURL.String()) +} + +func TestGetGeoProxyURLWhenGeoPrimaryOrNonGeo(t *testing.T) { + geoProxyURL, err := getGeoProxyURLGivenResponse(t, "{}") + + require.Error(t, err) + require.Equal(t, ErrNotGeoSecondary, err) + require.Nil(t, geoProxyURL) +} + +func getGeoProxyURLGivenResponse(t *testing.T, givenInternalApiResponse string) (*url.URL, error) { + t.Helper() + ts := testRailsServer(regexp.MustCompile(`/api/v4/geo/proxy`), 200, givenInternalApiResponse) + defer ts.Close() + backend := helper.URLMustParse(ts.URL) + version := "123" + rt := roundtripper.NewTestBackendRoundTripper(backend) + testhelper.ConfigureSecret() + + apiClient := NewAPI(backend, version, rt) + + geoProxyURL, err := apiClient.GetGeoProxyURL() + + return geoProxyURL, err +} + +func testRailsServer(url *regexp.Regexp, code int, body string) *httptest.Server { + return testhelper.TestServerWithHandler(url, func(w http.ResponseWriter, r *http.Request) { + // return a 204 No Content response if we don't receive the JWT header + if r.Header.Get(secret.RequestHeader) == "" { + w.WriteHeader(204) + return + } + + w.Header().Set("Content-Type", ResponseContentType) + + logEntry := log.WithFields(log.Fields{ + "method": r.Method, + "url": r.URL, + }) + logEntryWithCode := logEntry.WithField("code", code) + + // Write pure string + logEntryWithCode.Info("UPSTREAM") + + w.WriteHeader(code) + fmt.Fprint(w, body) + }) +} diff --git a/workhorse/internal/upstream/routes.go b/workhorse/internal/upstream/routes.go index 230b67ed059..d46397e226e 100644 --- a/workhorse/internal/upstream/routes.go +++ b/workhorse/internal/upstream/routes.go @@ -191,12 +191,7 @@ func buildProxy(backend *url.URL, version string, rt http.RoundTripper, cfg conf // see upstream.ServeHTTP func configureRoutes(u *upstream) { - api := apipkg.NewAPI( - u.Backend, - u.Version, - u.RoundTripper, - ) - + api := u.APIClient static := &staticpages.Static{DocumentRoot: u.DocumentRoot, Exclude: staticExclude} proxy := buildProxy(u.Backend, u.Version, u.RoundTripper, u.Config) cableProxy := proxypkg.NewProxy(u.CableBackend, u.Version, u.CableRoundTripper) diff --git a/workhorse/internal/upstream/upstream.go b/workhorse/internal/upstream/upstream.go index 80e7d4056b6..c41eb98683b 100644 --- a/workhorse/internal/upstream/upstream.go +++ b/workhorse/internal/upstream/upstream.go @@ -8,6 +8,7 @@ package upstream import ( "fmt" + "os" "net/http" "strings" @@ -15,8 +16,10 @@ import ( "github.com/sirupsen/logrus" "gitlab.com/gitlab-org/labkit/correlation" + 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/log" "gitlab.com/gitlab-org/gitlab-workhorse/internal/rejectmethods" "gitlab.com/gitlab-org/gitlab-workhorse/internal/upload" "gitlab.com/gitlab-org/gitlab-workhorse/internal/upstream/roundtripper" @@ -32,11 +35,13 @@ var ( type upstream struct { config.Config - URLPrefix urlprefix.Prefix - Routes []routeEntry - RoundTripper http.RoundTripper - CableRoundTripper http.RoundTripper - accessLogger *logrus.Logger + URLPrefix urlprefix.Prefix + Routes []routeEntry + RoundTripper http.RoundTripper + CableRoundTripper http.RoundTripper + APIClient *apipkg.API + accessLogger *logrus.Logger + enableGeoProxyFeature bool } func NewUpstream(cfg config.Config, accessLogger *logrus.Logger) http.Handler { @@ -60,6 +65,13 @@ func newUpstream(cfg config.Config, accessLogger *logrus.Logger, routesCallback up.RoundTripper = roundtripper.NewBackendRoundTripper(up.Backend, up.Socket, up.ProxyHeadersTimeout, cfg.DevelopmentMode) up.CableRoundTripper = roundtripper.NewBackendRoundTripper(up.CableBackend, up.CableSocket, up.ProxyHeadersTimeout, cfg.DevelopmentMode) up.configureURLPrefix() + up.APIClient = apipkg.NewAPI( + up.Backend, + up.Version, + up.RoundTripper, + ) + // Kind of a feature flag. See https://gitlab.com/groups/gitlab-org/-/epics/5914#note_564974130 + up.enableGeoProxyFeature = os.Getenv("GEO_SECONDARY_PROXY") == "1" routesCallback(&up) var correlationOpts []correlation.InboundHandlerOption @@ -108,6 +120,17 @@ func (u *upstream) ServeHTTP(w http.ResponseWriter, r *http.Request) { // Look for a matching route var route *routeEntry + + if u.enableGeoProxyFeature { + geoProxyURL, err := u.APIClient.GetGeoProxyURL() + + if err == nil { + log.WithRequest(r).WithFields(log.Fields{"geoProxyURL": geoProxyURL}).Info("Geo Proxy: Set route according to Geo Proxy logic") + } else if err != apipkg.ErrNotGeoSecondary { + log.WithRequest(r).WithError(err).Error("Geo Proxy: Unable to determine Geo Proxy URL. Falling back to normal routing") + } + } + for _, ro := range u.Routes { if ro.isMatch(prefix.Strip(URIPath), r) { route = &ro diff --git a/workhorse/main.go b/workhorse/main.go index de282b2c670..f5cb3f77746 100644 --- a/workhorse/main.go +++ b/workhorse/main.go @@ -95,7 +95,7 @@ func buildConfig(arg0 string, args []string) (*bootConfig, *config.Config, error fset.StringVar(&cfg.Socket, "authSocket", "", "Optional: Unix domain socket to dial authBackend at") // actioncable backend - cableBackend := fset.String("cableBackend", upstream.DefaultBackend.String(), "ActionCable backend") + cableBackend := fset.String("cableBackend", "", "ActionCable backend") fset.StringVar(&cfg.CableSocket, "cableSocket", "", "Optional: Unix domain socket to dial cableBackend at") fset.StringVar(&cfg.DocumentRoot, "documentRoot", "public", "Path to static files content") @@ -123,9 +123,14 @@ func buildConfig(arg0 string, args []string) (*bootConfig, *config.Config, error return nil, nil, fmt.Errorf("authBackend: %v", err) } - cfg.CableBackend, err = parseAuthBackend(*cableBackend) - if err != nil { - return nil, nil, fmt.Errorf("cableBackend: %v", err) + if *cableBackend != "" { + // A custom -cableBackend has been specified + cfg.CableBackend, err = parseAuthBackend(*cableBackend) + if err != nil { + return nil, nil, fmt.Errorf("cableBackend: %v", err) + } + } else { + cfg.CableBackend = cfg.Backend } tomlData := "" |