Commit a75097c9 authored by Alessio Caiazza's avatar Alessio Caiazza 💡
Browse files

Merge branch 'update-debug-jobs-list-output' into 'master'

Update debug jobs list output

See merge request !992
parents 1403cfc3 35c57070
Pipeline #29664163 failed with stages
in 18 minutes and 30 seconds
......@@ -3,6 +3,7 @@ package commands
import (
"fmt"
"net/http"
"regexp"
"strings"
"sync"
......@@ -145,6 +146,10 @@ func (b *buildsHelper) releaseRequest(runner *common.RunnerConfig) bool {
}
func (b *buildsHelper) addBuild(build *common.Build) {
if build == nil {
return
}
b.lock.Lock()
defer b.lock.Unlock()
......@@ -270,8 +275,31 @@ func (b *buildsHelper) Collect(ch chan<- prometheus.Metric) {
}
func (b *buildsHelper) ListJobsHandler(w http.ResponseWriter, r *http.Request) {
version := r.URL.Query().Get("v")
if version == "" {
version = "1"
}
handlers := map[string]http.HandlerFunc{
"1": b.listJobsHandlerV1,
"2": b.listJobsHandlerV2,
}
handler, ok := handlers[version]
if !ok {
w.WriteHeader(http.StatusNotFound)
fmt.Fprintf(w, "Request version %q not supported", version)
return
}
w.Header().Add("X-List-Version", version)
w.Header().Add("Content-Type", "text/plain")
w.WriteHeader(http.StatusOK)
handler(w, r)
}
func (b *buildsHelper) listJobsHandlerV1(w http.ResponseWriter, r *http.Request) {
for _, job := range b.builds {
fmt.Fprintf(
w,
......@@ -280,4 +308,24 @@ func (b *buildsHelper) ListJobsHandler(w http.ResponseWriter, r *http.Request) {
job.CurrentState, job.CurrentStage, job.CurrentExecutorStage(),
)
}
}
func (b *buildsHelper) listJobsHandlerV2(w http.ResponseWriter, r *http.Request) {
for _, job := range b.builds {
url := CreateJobURL(job.RepoCleanURL(), job.ID)
fmt.Fprintf(
w,
"url=%s state=%s stage=%s executor_stage=%s duration=%s\n",
url, job.CurrentState, job.CurrentStage, job.CurrentExecutorStage(), job.Duration(),
)
}
}
func CreateJobURL(projectURL string, jobID int) string {
r := regexp.MustCompile("(\\.git$)?")
URL := r.ReplaceAllString(projectURL, "")
return fmt.Sprintf("%s/-/jobs/%d", URL, jobID)
}
package commands
import (
"bytes"
"fmt"
"net/http"
"net/http/httptest"
"regexp"
"testing"
"github.com/prometheus/client_golang/prometheus"
......@@ -137,6 +142,80 @@ func TestBuildsHelperFindSessionByURL(t *testing.T) {
assert.Nil(t, foundSession)
}
type listJobsHandlerVersioningTest struct {
URL string
expectedVersion string
expectedCode int
}
func TestBuildsHelper_ListJobsHandlerVersioning(t *testing.T) {
baseURL := "/test/url"
tests := map[string]listJobsHandlerVersioningTest{
"no version specified": {
URL: baseURL,
expectedVersion: "1",
expectedCode: http.StatusOK,
},
"version 1 specified": {
URL: baseURL + "?v=1",
expectedVersion: "1",
expectedCode: http.StatusOK,
},
"version 2 specified": {
URL: baseURL + "?v=2",
expectedVersion: "2",
expectedCode: http.StatusOK,
},
"unsupported version specified": {
URL: baseURL + "?v=3",
expectedCode: http.StatusNotFound,
},
}
b := &buildsHelper{}
mux := http.NewServeMux()
mux.HandleFunc(baseURL, b.ListJobsHandler)
server := httptest.NewServer(mux)
defer server.Close()
for name, test := range tests {
t.Run(name, func(t *testing.T) {
req, err := http.NewRequest(http.MethodGet, server.URL+test.URL, nil)
require.NoError(t, err)
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
require.NotNil(t, resp)
assert.Equal(t, test.expectedCode, resp.StatusCode)
if test.expectedVersion != "" {
require.Contains(t, resp.Header, "X-List-Version")
assert.Equal(t, test.expectedVersion, resp.Header.Get("X-List-Version"))
}
})
}
}
type fakeResponseWriter struct {
output *bytes.Buffer
header http.Header
statusCode int
}
func (w *fakeResponseWriter) Header() http.Header { return w.header }
func (w *fakeResponseWriter) Write(data []byte) (int, error) { return w.output.Write(data) }
func (w *fakeResponseWriter) WriteHeader(statusCode int) { w.statusCode = statusCode }
func newFakeResponseWriter() *fakeResponseWriter {
return &fakeResponseWriter{
output: &bytes.Buffer{},
header: http.Header{},
}
}
var testBuildCurrentID int
func getTestBuild() *common.Build {
......@@ -152,6 +231,92 @@ func getTestBuild() *common.Build {
build.ID = testBuildCurrentID
build.Runner = &runner
build.JobInfo = jobInfo
build.GitInfo = common.GitInfo{
RepoURL: "https://gitlab.example.com/my-namespace/my-project.git",
}
return build
}
type listJobsHandlerTest struct {
build *common.Build
version string
expectedOutput []string
expectedRegexp []*regexp.Regexp
expectedStatus int
}
func TestBuildsHelper_ListJobsHandler(t *testing.T) {
build := getTestBuild()
tests := map[string]listJobsHandlerTest{
"no jobs": {
build: nil,
expectedStatus: http.StatusOK,
},
"job exists": {
build: build,
expectedOutput: []string{
fmt.Sprintf("id=%d url=https://gitlab.example.com/my-namespace/my-project.git", build.ID),
},
expectedStatus: http.StatusOK,
},
"job exists v2": {
build: build,
version: "2",
expectedOutput: []string{
fmt.Sprintf("url=https://gitlab.example.com/my-namespace/my-project/-/jobs/%d", build.ID),
},
expectedRegexp: []*regexp.Regexp{
regexp.MustCompile("duration=[0-9hms.]+"),
},
expectedStatus: http.StatusOK,
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
writer := newFakeResponseWriter()
URL := "/"
if test.version != "" {
URL = fmt.Sprintf("/?v=%s", test.version)
}
req, err := http.NewRequest(http.MethodGet, URL, nil)
require.NoError(t, err)
b := &buildsHelper{}
b.addBuild(test.build)
b.ListJobsHandler(writer, req)
if len(test.expectedOutput) == 0 && len(test.expectedRegexp) == 0 {
assert.Empty(t, writer.output.String())
} else {
for _, expectedOutput := range test.expectedOutput {
assert.Contains(t, writer.output.String(), expectedOutput)
}
for _, expectedRegexp := range test.expectedRegexp {
assert.Regexp(t, expectedRegexp, writer.output.String())
}
}
assert.Equal(t, test.expectedStatus, writer.statusCode)
})
}
}
func TestCreateJobURL(t *testing.T) {
testCases := map[string]string{
"http://gitlab.example.com/my-namespace/my-project.git": "http://gitlab.example.com/my-namespace/my-project/-/jobs/1",
"http://gitlab.example.com/my-namespace/my-project": "http://gitlab.example.com/my-namespace/my-project/-/jobs/1",
"http://gitlab.example.com/my-namespace/my.git.project.git": "http://gitlab.example.com/my-namespace/my.git.project/-/jobs/1",
"http://gitlab.example.com/my-namespace/my.git.project": "http://gitlab.example.com/my-namespace/my.git.project/-/jobs/1",
}
for URL, expectedURL := range testCases {
jobURL := CreateJobURL(URL, 1)
assert.Equal(t, expectedURL, jobURL)
}
}
......@@ -406,7 +406,7 @@ func (mr *RunCommand) serveMetrics(mux *http.ServeMux) {
}
func (mr *RunCommand) serveDebugData(mux *http.ServeMux) {
mux.Handle("/debug/jobs/list", http.HandlerFunc(mr.buildsHelper.ListJobsHandler))
mux.HandleFunc("/debug/jobs/list", mr.buildsHelper.ListJobsHandler)
}
func (mr *RunCommand) setupMetricsAndDebugServer() {
......
......@@ -87,6 +87,8 @@ type Build struct {
executorStageResolver func() ExecutorStage
logger BuildLogger
allVariables JobVariables
startedAt time.Time
}
func (b *Build) Log() *logrus.Entry {
......@@ -291,6 +293,8 @@ func (b *Build) run(ctx context.Context, executor Executor) (err error) {
buildFinish <- b.executeScript(runContext, executor)
}()
b.startedAt = time.Now()
// Wait for signals: cancel, timeout, abort or finish
b.Log().Debugln("Waiting for signals...")
select {
......@@ -639,3 +643,7 @@ func (b *Build) GetCacheRequestTimeout() int {
}
return timeout
}
func (b *Build) Duration() time.Duration {
return time.Since(b.startedAt)
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment