Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
61a358c7df
commit
b98649abf5
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue