Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2024-08-01 06:06:54 +00:00
parent 61a358c7df
commit b98649abf5
11 changed files with 622 additions and 13 deletions

View File

@ -60,7 +60,6 @@ Note the following:
- The `override_project_ci` strategy takes precedence over other policies using the `inject` strategy. If any policy with `override_project_ci` applies, the project CI configuration will be ignored.
- You should choose unique job names for pipeline execution policies. Some CI/CD configurations are based on job names and it can lead to unwanted results if a job exists multiple times in the same pipeline. The `needs` keyword, for example makes one job dependent on another. In case of multiple jobs with the same name, it will randomly depend on one of them.
- Pipeline execution policies remain in effect even if the project lacks a CI/CD configuration file.
- The ability to enforce a scan execution policy and pipeline execution policy concurrently against the same project is not currently supported. You can use pipeline execution policies in isolation, or you can create scan execution policies and pipeline execution policies that target a different set of projects within the scope. Support for enforcing both a scan execution policy and pipeline execution policy on the same project is proposed in [issue 473112](https://gitlab.com/gitlab-org/gitlab/-/issues/473112).
### Job naming best practice

View File

@ -53,13 +53,13 @@ module QA
def check_element(name, click_by_js = false, **kwargs)
log_by_js("checking", name, click_by_js, **kwargs)
super
log_slow_code(name, **kwargs) { super }
end
def uncheck_element(name, click_by_js = false, **kwargs)
log_by_js("unchecking", name, click_by_js, **kwargs)
super
log_slow_code(name, **kwargs) { super }
end
def log_by_js(action, name, click_by_js, **kwargs)
@ -72,7 +72,7 @@ module QA
def click_element_coordinates(name, **kwargs)
log(%(clicking the coordinates of :#{highlight_element(name)}), :info)
super
log_slow_code(name, **kwargs) { super }
end
# @param name [Symbol, String] name of the data_qa_selector or data-testid element
@ -98,7 +98,7 @@ module QA
log(%(filling :#{highlight_element(name)} with "#{masked_content}"), :info)
super
log_slow_code(name) { super }
end
def select_element(name, value)
@ -147,7 +147,7 @@ module QA
def wait_for_animated_element(name)
log("waiting for animated element: #{name}")
super
log_slow_code(name) { super }
end
def within_element(name, **kwargs)

View File

@ -238,8 +238,8 @@ internal/upload/uploads.go:62:16: Error return value of `fmt.Fprintln` is not ch
internal/upload/uploads.go:101:15: Error return value of `fmt.Fprintln` is not checked (errcheck)
internal/upload/uploads_test.go:527:3: negative-positive: use assert.Positive (testifylint)
internal/upload/uploads_test.go:545:3: negative-positive: use assert.Positive (testifylint)
internal/upstream/routes.go:150:68: `(*upstream).wsRoute` - `matchers` always receives `nil` (unparam)
internal/upstream/routes.go:210: Function 'configureRoutes' is too long (236 > 60) (funlen)
internal/upstream/routes.go:384: internal/upstream/routes.go:384: Line contains TODO/BUG/FIXME/NOTE/OPTIMIZE/HACK: "TODO: We should probably not return a HT..." (godox)
internal/upstream/routes.go:151:68: `(*upstream).wsRoute` - `matchers` always receives `nil` (unparam)
internal/upstream/routes.go:211: Function 'configureRoutes' is too long (250 > 60) (funlen)
internal/upstream/routes.go:399: internal/upstream/routes.go:399: Line contains TODO/BUG/FIXME/NOTE/OPTIMIZE/HACK: "TODO: We should probably not return a HT..." (godox)
internal/upstream/upstream.go:116: internal/upstream/upstream.go:116: Line contains TODO/BUG/FIXME/NOTE/OPTIMIZE/HACK: "TODO: move to LabKit https://gitlab.com/..." (godox)
internal/zipartifacts/open_archive.go:78:28: response body must be closed (bodyclose)

View File

@ -238,8 +238,8 @@ internal/upload/uploads.go:62:16: Error return value of `fmt.Fprintln` is not ch
internal/upload/uploads.go:101:15: Error return value of `fmt.Fprintln` is not checked (errcheck)
internal/upload/uploads_test.go:527:3: negative-positive: use assert.Positive (testifylint)
internal/upload/uploads_test.go:545:3: negative-positive: use assert.Positive (testifylint)
internal/upstream/routes.go:150:68: `(*upstream).wsRoute` - `matchers` always receives `nil` (unparam)
internal/upstream/routes.go:210: Function 'configureRoutes' is too long (236 > 60) (funlen)
internal/upstream/routes.go:384: internal/upstream/routes.go:384: Line contains TODO/BUG/FIXME/NOTE/OPTIMIZE/HACK: "TODO: We should probably not return a HT..." (godox)
internal/upstream/routes.go:151:68: `(*upstream).wsRoute` - `matchers` always receives `nil` (unparam)
internal/upstream/routes.go:211: Function 'configureRoutes' is too long (250 > 60) (funlen)
internal/upstream/routes.go:399: internal/upstream/routes.go:399: Line contains TODO/BUG/FIXME/NOTE/OPTIMIZE/HACK: "TODO: We should probably not return a HT..." (godox)
internal/upstream/upstream.go:116: internal/upstream/upstream.go:116: Line contains TODO/BUG/FIXME/NOTE/OPTIMIZE/HACK: "TODO: move to LabKit https://gitlab.com/..." (godox)
internal/zipartifacts/open_archive.go:78:28: response body must be closed (bodyclose)

View File

@ -0,0 +1,364 @@
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gitlab.com/gitlab-org/gitlab/workhorse/internal/api"
"gitlab.com/gitlab-org/gitlab/workhorse/internal/secret"
"gitlab.com/gitlab-org/gitlab/workhorse/internal/testhelper"
)
type gobAuthServer struct {
shouldReceiveRequestPath string
respondWithStatusCode int
}
type gobUpstreamServer struct {
shouldReceiveRequestPath string
shouldBeCalled bool
respondWithStatusCode int
respondWithBody string
}
type gobTestCase struct {
desc string
path string
method string
body string
shouldRespondWithBody string
shouldRespondWithStatusCode int
authServer gobAuthServer
upstream gobUpstreamServer
}
func TestGOBEndpoints(t *testing.T) {
testCases := [][]gobTestCase{
genGETTestcases("traces"),
genGETTestcases("metrics"),
genGETTestcases("logs"),
genGETTestcases("analytics"),
genGETTestcases("services"),
genPOSTTestcases("traces"),
genPOSTTestcases("metrics"),
genPOSTTestcases("logs"),
}
for _, signalTestCases := range testCases {
for _, tc := range signalTestCases {
t.Run(tc.desc, func(t *testing.T) {
runTest(t, tc)
})
}
}
}
func genGETTestcases(signal string) []gobTestCase {
return []gobTestCase{
{
desc: fmt.Sprintf("GET /%s, successful auth, proxies successful upstream", signal),
method: "GET",
path: fmt.Sprintf("/api/v4/projects/1/observability/v1/%s", signal),
shouldRespondWithBody: "hello world",
shouldRespondWithStatusCode: 200,
authServer: gobAuthServer{
respondWithStatusCode: 200,
shouldReceiveRequestPath: fmt.Sprintf("/api/v4/internal/observability/project/1/read/%s", signal),
},
upstream: gobUpstreamServer{
respondWithStatusCode: 200,
respondWithBody: "hello world",
shouldReceiveRequestPath: fmt.Sprintf("/observability/v1/%s", signal),
shouldBeCalled: true,
},
},
{
desc: fmt.Sprintf("GET /%s with multi-digit projectID, successful auth, proxies successful upstream", signal),
method: "GET",
path: fmt.Sprintf("/api/v4/projects/11111/observability/v1/%s", signal),
shouldRespondWithBody: "hello world",
shouldRespondWithStatusCode: 200,
authServer: gobAuthServer{
respondWithStatusCode: 200,
shouldReceiveRequestPath: fmt.Sprintf("/api/v4/internal/observability/project/11111/read/%s", signal),
},
upstream: gobUpstreamServer{
respondWithStatusCode: 200,
respondWithBody: "hello world",
shouldReceiveRequestPath: fmt.Sprintf("/observability/v1/%s", signal),
shouldBeCalled: true,
},
},
{
desc: fmt.Sprintf("GET /%s with url-encode projectID, successful auth, proxies successful upstream", signal),
method: "GET",
path: fmt.Sprintf("/api/v4/projects/diaspora%%2Fdiaspora/observability/v1/%s", signal),
shouldRespondWithBody: "hello world",
shouldRespondWithStatusCode: 200,
authServer: gobAuthServer{
respondWithStatusCode: 200,
shouldReceiveRequestPath: fmt.Sprintf("/api/v4/internal/observability/project/diaspora%%2Fdiaspora/read/%s", signal),
},
upstream: gobUpstreamServer{
respondWithStatusCode: 200,
respondWithBody: "hello world",
shouldReceiveRequestPath: fmt.Sprintf("/observability/v1/%s", signal),
shouldBeCalled: true,
},
},
{
desc: fmt.Sprintf("GET /%s/some/subpath subpath, successful auth, proxies successful upstream", signal),
method: "GET",
path: fmt.Sprintf("/api/v4/projects/1/observability/v1/%s/some/subpath", signal),
shouldRespondWithBody: "hello world",
shouldRespondWithStatusCode: 200,
authServer: gobAuthServer{
respondWithStatusCode: 200,
shouldReceiveRequestPath: fmt.Sprintf("/api/v4/internal/observability/project/1/read/%s", signal),
},
upstream: gobUpstreamServer{
respondWithStatusCode: 200,
respondWithBody: "hello world",
shouldReceiveRequestPath: fmt.Sprintf("/observability/v1/%s/some/subpath", signal),
shouldBeCalled: true,
},
},
{
desc: fmt.Sprintf("GET /%s, unsuccessful auth, returns auth status code", signal),
method: "GET",
path: fmt.Sprintf("/api/v4/projects/1/observability/v1/%s", signal),
shouldRespondWithBody: "",
shouldRespondWithStatusCode: 401,
authServer: gobAuthServer{
respondWithStatusCode: 401,
shouldReceiveRequestPath: fmt.Sprintf("/api/v4/internal/observability/project/1/read/%s", signal),
},
upstream: gobUpstreamServer{
shouldBeCalled: false,
},
},
{
desc: fmt.Sprintf("GET /%s, successful auth, correctly proxies upstream failure", signal),
method: "GET",
path: fmt.Sprintf("/api/v4/projects/1/observability/v1/%s", signal),
shouldRespondWithBody: "",
shouldRespondWithStatusCode: 500,
authServer: gobAuthServer{
respondWithStatusCode: 200,
shouldReceiveRequestPath: fmt.Sprintf("/api/v4/internal/observability/project/1/read/%s", signal),
},
upstream: gobUpstreamServer{
respondWithStatusCode: 500,
respondWithBody: "",
shouldReceiveRequestPath: fmt.Sprintf("/observability/v1/%s", signal),
shouldBeCalled: true,
},
},
}
}
func genPOSTTestcases(signal string) []gobTestCase {
return []gobTestCase{
{
desc: fmt.Sprintf("POST /%s, successful auth, proxies successful upstream", signal),
method: "POST",
path: fmt.Sprintf("/api/v4/projects/1/observability/v1/%s", signal),
body: "my posted data",
shouldRespondWithBody: "hello world",
shouldRespondWithStatusCode: 200,
authServer: gobAuthServer{
respondWithStatusCode: 200,
shouldReceiveRequestPath: fmt.Sprintf("/api/v4/internal/observability/project/1/write/%s", signal),
},
upstream: gobUpstreamServer{
respondWithStatusCode: 200,
respondWithBody: "hello world",
shouldReceiveRequestPath: fmt.Sprintf("/observability/v1/%s", signal),
shouldBeCalled: true,
},
},
{
desc: fmt.Sprintf("POST /%s/some/subpath, successful auth, proxies successful upstream", signal),
method: "POST",
path: fmt.Sprintf("/api/v4/projects/1/observability/v1/%s/some/subpath", signal),
body: "my posted data",
shouldRespondWithBody: "hello world",
shouldRespondWithStatusCode: 200,
authServer: gobAuthServer{
respondWithStatusCode: 200,
shouldReceiveRequestPath: fmt.Sprintf("/api/v4/internal/observability/project/1/write/%s", signal),
},
upstream: gobUpstreamServer{
respondWithStatusCode: 200,
respondWithBody: "hello world",
shouldReceiveRequestPath: fmt.Sprintf("/observability/v1/%s/some/subpath", signal),
shouldBeCalled: true,
},
},
{
desc: fmt.Sprintf("POST /%s, unsuccessful auth, returns auth status code", signal),
method: "POST",
path: fmt.Sprintf("/api/v4/projects/1/observability/v1/%s", signal),
body: "my posted data",
shouldRespondWithBody: "",
shouldRespondWithStatusCode: 401,
authServer: gobAuthServer{
respondWithStatusCode: 401,
shouldReceiveRequestPath: fmt.Sprintf("/api/v4/internal/observability/project/1/write/%s", signal),
},
upstream: gobUpstreamServer{
shouldBeCalled: false,
},
},
{
desc: fmt.Sprintf("POST /%s, successful auth, correctly proxies upstream failure", signal),
method: "POST",
path: fmt.Sprintf("/api/v4/projects/1/observability/v1/%s", signal),
body: "my posted data",
shouldRespondWithBody: "",
shouldRespondWithStatusCode: 500,
authServer: gobAuthServer{
respondWithStatusCode: 200,
shouldReceiveRequestPath: fmt.Sprintf("/api/v4/internal/observability/project/1/write/%s", signal),
},
upstream: gobUpstreamServer{
respondWithStatusCode: 500,
respondWithBody: "",
shouldReceiveRequestPath: fmt.Sprintf("/observability/v1/%s", signal),
shouldBeCalled: true,
},
},
}
}
func runTest(t *testing.T, tc gobTestCase) {
gobSettingsHeaders := map[string]string{
"foo": "bar",
"baz": "foobar",
}
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if tc.upstream.shouldBeCalled != true {
assert.Fail(t, "upstream should not be called")
}
assert.Equal(t, tc.upstream.shouldReceiveRequestPath, r.URL.Path, "requested upstream endpoint")
// Assert that upstream received the headers that were returned in the api.Response
// from the authServer
for name, value := range gobSettingsHeaders {
assert.Equal(t, value, r.Header.Get(name), "received correct header")
}
defer r.Body.Close()
b, err := io.ReadAll(r.Body)
assert.NoError(t, err)
assert.Equal(t, tc.body, string(b), "received body upstream")
w.WriteHeader(tc.upstream.respondWithStatusCode)
_, err = w.Write([]byte(tc.upstream.respondWithBody))
assert.NoError(t, err, "write auth response")
}))
defer upstream.Close()
authServer := testhelper.TestServerWithHandler(nil, func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, tc.authServer.shouldReceiveRequestPath, r.URL.Path, "requested auth endpoint")
// Auth request should use the same method as the original Workhorse request
assert.Equal(t, tc.method, r.Method, "auth request method")
// return a 204 No Content response if we don't receive the JWT header from Workhorse
if r.Header.Get(secret.RequestHeader) == "" {
w.WriteHeader(204)
return
}
w.Header().Set("Content-Type", api.ResponseContentType)
// Should not receive the body of the original Workhorse request
defer r.Body.Close()
b, err := io.ReadAll(r.Body)
assert.NoError(t, err)
assert.Empty(t, b)
data, err := json.Marshal(&api.Response{
Gob: api.GOBSettings{
Backend: upstream.URL,
Headers: gobSettingsHeaders,
},
})
if err != nil {
w.WriteHeader(503)
fmt.Fprint(w, err)
return
}
w.WriteHeader(tc.authServer.respondWithStatusCode)
// Mimic the internal API where response body is only written on success
if tc.authServer.respondWithStatusCode == 200 {
w.Write(data)
}
})
defer authServer.Close()
workhorse := startWorkhorseServer(t, authServer.URL)
// Do the request
req, err := http.NewRequest(tc.method, workhorse.URL+tc.path, strings.NewReader(tc.body))
require.NoError(t, err)
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
require.NoError(t, err)
assert.Equal(t, tc.shouldRespondWithStatusCode, resp.StatusCode, "response status code")
assert.Equal(t, tc.shouldRespondWithBody, string(body), "response body")
}

View File

@ -187,6 +187,8 @@ type Response struct {
UploadHashFunctions []string
// NeedAudit indicates whether git events should be audited to rails.
NeedAudit bool `json:"NeedAudit"`
// Gob contains settings for the GitLab Observability Backend (GOB).
Gob GOBSettings `json:"gob"`
}
// GitalyServer represents configuration parameters for a Gitaly server,

View File

@ -0,0 +1,43 @@
package api
import (
"fmt"
"net/url"
)
var (
errDetailsNotSpecified = fmt.Errorf("gob details not specified")
errBackendNotSpecified = fmt.Errorf("gob backend not specified")
errIncorrectBackendScheme = fmt.Errorf("gob only supports http/https protocols")
)
// GOBSettings holds the configuration for proxying a request to the upstream
// GitLab Observability Backend
type GOBSettings struct {
// The location of the GitLab Observability Backend (GOB) instance to connect to.
Backend string `json:"backend"`
// Any headers (e.g., Authorization) to send with the upstream request
Headers map[string]string `json:"headers"`
}
// Upstream returns the GitLab Observability Backend location
func (g *GOBSettings) Upstream() (*url.URL, error) {
if g == nil {
return nil, errDetailsNotSpecified
}
if g.Backend == "" {
return nil, errBackendNotSpecified
}
u, err := url.Parse(g.Backend)
if err != nil {
return nil, err
}
if u.Scheme != "http" && u.Scheme != "https" {
return nil, errIncorrectBackendScheme
}
return u, nil
}

View File

@ -0,0 +1,64 @@
package api
import (
"testing"
)
func TestGOBSettings(t *testing.T) {
for _, tc := range []struct {
valid bool
desc string
gob *GOBSettings
}{
{
desc: "should return error when gob is nil",
valid: false,
gob: nil,
}, {
desc: "should return error when no backend",
valid: false,
gob: &GOBSettings{
Headers: map[string]string{},
Backend: "",
},
},
{
desc: "should return error when backend not a valid URL",
valid: false,
gob: &GOBSettings{
Headers: map[string]string{},
Backend: "invalid",
},
},
{
desc: "should return error when backend protocol not supported",
valid: false,
gob: &GOBSettings{
Headers: map[string]string{},
Backend: "tcp://observe.gitlab.com",
},
},
{
desc: "should be valid when backend protocol is http",
valid: true,
gob: &GOBSettings{
Headers: map[string]string{},
Backend: "http://observe.gitlab.com",
},
},
{
desc: "should be valid when backend protocol is https",
valid: true,
gob: &GOBSettings{
Headers: map[string]string{},
Backend: "https://observe.gitlab.com",
},
},
} {
t.Run(tc.desc, func(t *testing.T) {
if _, err := tc.gob.Upstream(); (err != nil) == tc.valid {
t.Fatalf("valid=%v: %s: %+v", tc.valid, err, tc.gob)
}
})
}
}

View File

@ -0,0 +1,112 @@
// Package gob manages request proxies to GitLab Observability Backend
package gob
import (
"fmt"
"net/http"
"regexp"
"time"
"gitlab.com/gitlab-org/gitlab/workhorse/internal/api"
"gitlab.com/gitlab-org/gitlab/workhorse/internal/config"
"gitlab.com/gitlab-org/gitlab/workhorse/internal/helper/fail"
proxypkg "gitlab.com/gitlab-org/gitlab/workhorse/internal/proxy"
"gitlab.com/gitlab-org/gitlab/workhorse/internal/secret"
"gitlab.com/gitlab-org/gitlab/workhorse/internal/upstream/roundtripper"
)
// Internal endpoint namespace for observability authorization
const gobInternalProjectAuthPath = "/api/v4/internal/observability/project/"
var projectPathRegex = regexp.MustCompile(`^/api/v4/projects/([^/]+)`)
// Proxy manages the authorization and upstream connection to
// GitLab Observability Backend
type Proxy struct {
version string
api *api.API
proxyHeadersTimeout time.Duration
developmentMode bool
}
// NewProxy returns a new Proxy for connecting to GitLab Observability Backend
func NewProxy(
api *api.API,
version string,
proxyHeadersTimeout time.Duration,
cfg config.Config) *Proxy {
return &Proxy{
api: api,
version: version,
proxyHeadersTimeout: proxyHeadersTimeout,
developmentMode: cfg.DevelopmentMode,
}
}
// WithProjectAuth configures the proxy to use a Rails API path for authorization
func (p *Proxy) WithProjectAuth(path string) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if p.api.URL == nil {
fail.Request(w, r, fmt.Errorf("api URL has not been set"))
return
}
authURL := *p.api.URL
projectID, err := extractProjectID(r)
if err != nil {
fail.Request(w, r, err)
return
}
authURL.Path = gobInternalProjectAuthPath + projectID + path
authReq := &http.Request{
Method: r.Method,
URL: &authURL,
Header: r.Header.Clone(),
}
authReq = authReq.WithContext(r.Context())
authorizer := p.api.PreAuthorizeHandler(func(_ http.ResponseWriter, _ *http.Request, a *api.Response) {
// Successful authorization
p.serveHTTP(w, r, a)
}, "")
authorizer.ServeHTTP(w, authReq)
})
}
func (p *Proxy) serveHTTP(w http.ResponseWriter, r *http.Request, a *api.Response) {
backend, err := a.Gob.Upstream()
if err != nil {
fail.Request(w, r, err)
return
}
// Remove prefix from path so it matches the cloud.gitlab.com/observability/ routing layer.
// https://gitlab.com/gitlab-com/gl-infra/production-engineering/-/issues/25077
outReq := r.Clone(r.Context())
outReq.URL.Path = projectPathRegex.ReplaceAllLiteralString(r.URL.EscapedPath(), "")
rt := secret.NewRoundTripper(
roundtripper.NewBackendRoundTripper(
backend,
"",
p.proxyHeadersTimeout,
p.developmentMode,
), p.version)
pxy := proxypkg.NewProxy(
backend,
p.version,
rt,
proxypkg.WithCustomHeaders(a.Gob.Headers),
proxypkg.WithForcedTargetHostHeader(),
)
pxy.ServeHTTP(w, outReq)
}
func extractProjectID(r *http.Request) (string, error) {
matches := projectPathRegex.FindStringSubmatch(r.URL.EscapedPath())
if len(matches) != 2 {
return "", fmt.Errorf("%s does not match expected %s", r.URL.EscapedPath(), projectPathRegex.String())
}
return matches[1], nil
}

View File

@ -34,7 +34,17 @@ func mustParseAddress(address, scheme string) string {
// NewBackendRoundTripper returns a new RoundTripper instance using the provided values
func NewBackendRoundTripper(backend *url.URL, socket string, proxyHeadersTimeout time.Duration, developmentMode bool) http.RoundTripper {
return newBackendRoundTripper(backend, socket, proxyHeadersTimeout, developmentMode, nil)
var tlsConf *tls.Config
if developmentMode {
// GitLab Observability Backend uses a LetsEncyrpt staging cert during development.
// We do not want to add them to the trust store: https://letsencrypt.org/docs/staging-environment/
//nolint:gosec
tlsConf = &tls.Config{
InsecureSkipVerify: true,
}
}
return newBackendRoundTripper(backend, socket, proxyHeadersTimeout, developmentMode, tlsConf)
}
func newBackendRoundTripper(backend *url.URL, socket string, proxyHeadersTimeout time.Duration, developmentMode bool, tlsConf *tls.Config) http.RoundTripper {

View File

@ -18,6 +18,7 @@ import (
"gitlab.com/gitlab-org/gitlab/workhorse/internal/config"
"gitlab.com/gitlab-org/gitlab/workhorse/internal/dependencyproxy"
"gitlab.com/gitlab-org/gitlab/workhorse/internal/git"
gobpkg "gitlab.com/gitlab-org/gitlab/workhorse/internal/gob"
"gitlab.com/gitlab-org/gitlab/workhorse/internal/helper"
"gitlab.com/gitlab-org/gitlab/workhorse/internal/imageresizer"
proxypkg "gitlab.com/gitlab-org/gitlab/workhorse/internal/proxy"
@ -246,6 +247,8 @@ func configureRoutes(u *upstream) {
probeUpstream := static.ErrorPagesUnless(u.DevelopmentMode, staticpages.ErrorFormatJSON, proxy)
healthUpstream := static.ErrorPagesUnless(u.DevelopmentMode, staticpages.ErrorFormatText, proxy)
gob := gobpkg.NewProxy(api, u.Version, u.ProxyHeadersTimeout, u.Config)
u.Routes = []routeEntry{
// Git Clone
u.route("GET", gitProjectPattern+`info/refs\z`, git.GetInfoRefsHandler(api)),
@ -360,6 +363,18 @@ func configureRoutes(u *upstream) {
u.route("POST", apiPattern+`v4/users\z`, tempfileMultipartProxy),
u.route("PUT", apiPattern+`v4/users/[0-9]+\z`, tempfileMultipartProxy),
// GitLab Observability Backend (GOB). Write paths are versioned with v1 to align with
// OpenTelemetry compatibility, where SDKs POST to /v1/traces, /v1/logs and /v1/metrics.
u.route("POST", apiProjectPattern+`/observability/v1/traces`, gob.WithProjectAuth("/write/traces")),
u.route("POST", apiProjectPattern+`/observability/v1/logs`, gob.WithProjectAuth("/write/logs")),
u.route("POST", apiProjectPattern+`/observability/v1/metrics`, gob.WithProjectAuth("/write/metrics")),
u.route("GET", apiProjectPattern+`/observability/v1/analytics`, gob.WithProjectAuth("/read/analytics")),
u.route("GET", apiProjectPattern+`/observability/v1/traces`, gob.WithProjectAuth("/read/traces")),
u.route("GET", apiProjectPattern+`/observability/v1/logs`, gob.WithProjectAuth("/read/logs")),
u.route("GET", apiProjectPattern+`/observability/v1/metrics`, gob.WithProjectAuth("/read/metrics")),
u.route("GET", apiProjectPattern+`/observability/v1/services`, gob.WithProjectAuth("/read/services")),
// Explicitly proxy API requests
u.route("", apiPattern, proxy),