diff --git a/action.go b/action.go index 75d9e3f..39885da 100644 --- a/action.go +++ b/action.go @@ -55,20 +55,28 @@ func main() { // Wait for the analysis task result if needed. if env.WaitForQualityGate { - log.Info("Retrieving the analysis task status ...") + log.Info("Retrieving the project analysis status ...") ctx, cancel := context.WithTimeout(context.Background(), env.QualityGateWaitTimeout) defer cancel() - status, err := run.RetrieveLastAnalysisTaskStatus(ctx) + status, err := run.RetrieveProjectanalysisStatus(ctx) if err != nil { log.Fatalf("Failed to retrieve the task status: %s", err) } - if status != sonarscanner.TaskStatusSuccess { - log.Fatalf("The analysis task failed with the status %s", status) + taskStatus := status.TaskStatus + if taskStatus != sonarscanner.TaskStatusSuccess { + log.Fatalf("Analysis task failed with the status '%s'", taskStatus) } - log.Infof("The analysis task finished with the status %s", status) + analysisStatus := status.AnalysisStatus + if analysisStatus == sonarscanner.AnalysisStatusError { + log.Fatalf("Quality gate failed with the status '%s'", analysisStatus) + } + + log.Infof("Quality gate status '%s'", analysisStatus) } + + log.Infof("Done") } diff --git a/internal/sonarscanner/analysis_status.go b/internal/sonarscanner/analysis_status.go new file mode 100644 index 0000000..ff51bb3 --- /dev/null +++ b/internal/sonarscanner/analysis_status.go @@ -0,0 +1,51 @@ +package sonarscanner + +import "fmt" + +type AnalysisStatus int + +const ( + AnalysisStatusUndefined AnalysisStatus = iota + AnalysisStatusOk AnalysisStatus = iota + AnalysisStatusWarning AnalysisStatus = iota + AnalysisStatusError AnalysisStatus = iota + AnalysisStatusNone AnalysisStatus = iota +) + +const ( + analysisStatusUndefinedStr = "UNDEFINED" + analysisStatusOkStr = "OK" + analysisStatusWarningStr = "WARN" + analysisStatusErrorStr = "ERROR" + analysisStatusNoneStr = "NONE" +) + +func (status AnalysisStatus) String() string { + switch status { + case AnalysisStatusOk: + return analysisStatusOkStr + case AnalysisStatusWarning: + return analysisStatusWarningStr + case AnalysisStatusError: + return analysisStatusErrorStr + case AnalysisStatusNone: + return analysisStatusNoneStr + default: + return analysisStatusUndefinedStr + } +} + +func parseAnalysisStatus(value string) (AnalysisStatus, error) { + switch value { + case analysisStatusOkStr: + return AnalysisStatusOk, nil + case analysisStatusWarningStr: + return AnalysisStatusWarning, nil + case analysisStatusErrorStr: + return AnalysisStatusError, nil + case analysisStatusNoneStr: + return AnalysisStatusNone, nil + default: + return AnalysisStatusUndefined, fmt.Errorf("unexpected analysis status '%s'", value) + } +} diff --git a/internal/sonarscanner/analysis_status_test.go b/internal/sonarscanner/analysis_status_test.go new file mode 100644 index 0000000..2a9671a --- /dev/null +++ b/internal/sonarscanner/analysis_status_test.go @@ -0,0 +1,37 @@ +package sonarscanner + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestAnalysisStatusToString(t *testing.T) { + assert.Equal(t, "OK", fmt.Sprint(AnalysisStatusOk)) + assert.Equal(t, "WARN", fmt.Sprint(AnalysisStatusWarning)) + assert.Equal(t, "ERROR", fmt.Sprint(AnalysisStatusError)) + assert.Equal(t, "NONE", fmt.Sprint(AnalysisStatusNone)) + assert.Equal(t, "UNDEFINED", fmt.Sprint(AnalysisStatusUndefined)) +} + +func TestParseAnalysisStatus(t *testing.T) { + assertAnalysisStatusParsedAs(t, "OK", AnalysisStatusOk) + assertAnalysisStatusParsedAs(t, "WARN", AnalysisStatusWarning) + assertAnalysisStatusParsedAs(t, "ERROR", AnalysisStatusError) + assertAnalysisStatusParsedAs(t, "NONE", AnalysisStatusNone) +} + +func TestParseInvalidAnalysisStatus(t *testing.T) { + status, err := parseAnalysisStatus("UNDEFINED") + + assert.NotNil(t, err) + assert.Equal(t, AnalysisStatusUndefined, status) +} + +func assertAnalysisStatusParsedAs(t *testing.T, value string, expectedStatus AnalysisStatus) { + actualStatus, err := parseAnalysisStatus(value) + + assert.Nil(t, err) + assert.Equal(t, expectedStatus, actualStatus) +} diff --git a/internal/sonarscanner/sonar_scanner.go b/internal/sonarscanner/sonar_scanner.go index 6333e26..c65442d 100644 --- a/internal/sonarscanner/sonar_scanner.go +++ b/internal/sonarscanner/sonar_scanner.go @@ -14,6 +14,7 @@ import ( "os/exec" "path" "regexp" + "strings" "time" "github.com/sirupsen/logrus" @@ -21,6 +22,7 @@ import ( ) var QualityGateWaitTimeout = errors.New("quality gate wait timeout") +var AnalysisStatusWaitTimeout = errors.New("analysis status wait timeout") const ( defaultWaitTimeout = 2 * time.Second @@ -56,6 +58,26 @@ type Run struct { log *logrus.Entry } +type ProjectAnalysisStatus struct { + TaskStatus TaskStatus + AnalysisStatus AnalysisStatus +} + +type taskStatusResponse struct { + analysisId string + taskStatus TaskStatus +} + +type analysisStatusResponse struct { + analysisStatus AnalysisStatus +} + +var undefinedResponse = taskStatusResponse{taskStatus: TaskStatusUndefined} +var undefinedAnalysisStatus = ProjectAnalysisStatus{ + TaskStatus: TaskStatusUndefined, + AnalysisStatus: AnalysisStatusUndefined, +} + func (c *RunFactory) NewRun() (*Run, error) { metadataFileName := c.MetadataFileName if metadataFileName == "" { @@ -170,17 +192,49 @@ func (r *Run) RunScanner(ctx context.Context) error { return runSonarScanner(r.log.WithField("prefix", "sonar-scanner-cli"), cmd) } -func (r *Run) RetrieveLastAnalysisTaskStatus(ctx context.Context) (TaskStatus, error) { +func (r *Run) RetrieveProjectanalysisStatus(ctx context.Context) (ProjectAnalysisStatus, error) { + status := ProjectAnalysisStatus{ + TaskStatus: TaskStatusUndefined, + AnalysisStatus: AnalysisStatusUndefined, + } + r.log.Infof("Using metadata file %s", r.metadataFilePath) url, err := getTaskUrlFromFile(r.metadataFilePath) if err != nil { - return TaskStatusUndefined, err + return undefinedAnalysisStatus, err } r.log.Infof("Using task result url %s", url) - return r.retrieveTaskStatus(ctx, url) + client := &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: r.tlsConfig, + }, + Timeout: defaultRequestTimeout, + } + + r.log.Infof("Retrieving analysis task status") + + taskStatus, err := r.retrieveTaskStatus(ctx, client, url) + if err != nil { + return status, err + } + + status.TaskStatus = taskStatus.taskStatus + if status.TaskStatus != TaskStatusSuccess { + return status, nil + } + + r.log.Infof("Retrieving quality gate status") + + analysisStatus, err := r.retrieveProjectAnalysisStatus(ctx, client, taskStatus.analysisId) + if err != nil { + return status, err + } + + status.AnalysisStatus = analysisStatus + return status, nil } func (r *Run) runReverseProxy(ctx context.Context) error { @@ -237,25 +291,20 @@ func (r *Run) getSonarScannerArgs() []string { return args } -func (r *Run) retrieveTaskStatus(ctx context.Context, url string) (TaskStatus, error) { - client := &http.Client{ - Transport: &http.Transport{ - TLSClientConfig: r.tlsConfig, - }, - Timeout: defaultRequestTimeout, - } +func (r *Run) retrieveTaskStatus(ctx context.Context, client *http.Client, url string) (taskStatusResponse, error) { for { r.log.Debugf("Reading task status from the server") - taskStatus, err := r.requestTaskStatus(ctx, client, url) + response, err := r.requestTaskStatus(ctx, client, url) if err != nil { - return TaskStatusUndefined, err + return undefinedResponse, err } - r.log.Debugf("Task status returned in the response %s", taskStatus) + taskStatus := response.taskStatus + r.log.Debugf("Task status returned in the response was '%s'", taskStatus) if taskStatus == TaskStatusSuccess || taskStatus == TaskStatusCancelled || taskStatus == TaskStatusUndefined { - return taskStatus, nil + return response, nil } r.log.Debugf("Waiting for %s before next poll", defaultWaitTimeout) @@ -264,35 +313,107 @@ func (r *Run) retrieveTaskStatus(ctx context.Context, url string) (TaskStatus, e case <-time.After(defaultWaitTimeout): continue case <-ctx.Done(): - return TaskStatusUndefined, QualityGateWaitTimeout + return undefinedResponse, QualityGateWaitTimeout } } } -func (r *Run) requestTaskStatus(ctx context.Context, client *http.Client, url string) (TaskStatus, error) { - request, err := http.NewRequestWithContext(ctx, "GET", url, nil) +func (r *Run) makeSonarServerRequest( + ctx context.Context, + client *http.Client, + method string, + url string, +) (*gjson.Result, error) { + request, err := http.NewRequestWithContext(ctx, method, url, nil) if err != nil { - return TaskStatusUndefined, err + return nil, err } if r.sonarLogin != "" { - r.log.Debugf("Using basic auth") + r.log.Debugf("Using basic auth for request %s", url) request.SetBasicAuth(r.sonarLogin, r.sonarPassword) } response, err := client.Do(request) + if err != nil { + return nil, err + } + + defer response.Body.Close() + + return processResponse(response) +} + +func (r *Run) requestTaskStatus(ctx context.Context, client *http.Client, url string) (taskStatusResponse, error) { + response, err := r.makeSonarServerRequest(ctx, client, "GET", url) + if err != nil { + return undefinedResponse, err + } + if err != nil { if err == context.Canceled { - return TaskStatusUndefined, QualityGateWaitTimeout + return undefinedResponse, QualityGateWaitTimeout } - return TaskStatusUndefined, err + return undefinedResponse, err } - defer response.Body.Close() + taskStatus, err := parseTaskStatus(response.Get("task.status").Str) + if err != nil { + return undefinedResponse, err + } + + return taskStatusResponse{ + analysisId: response.Get("task.analysisId").Str, + taskStatus: taskStatus, + }, nil +} + +func (r *Run) retrieveProjectAnalysisStatus( + ctx context.Context, + client *http.Client, + analysisId string, +) (AnalysisStatus, error) { + url := getApiUrl(r.sonarHostUrl, fmt.Sprintf("/api/qualitygates/project_status?analysisId=%s", analysisId)) + r.log.Debugf("Reading analysis status from %s", url) + + response, err := r.makeSonarServerRequest(ctx, client, "GET", url) + if err != nil { + if err == context.Canceled { + return AnalysisStatusUndefined, AnalysisStatusWaitTimeout + } + + return AnalysisStatusUndefined, err + } + + status, err := parseAnalysisStatus(response.Get("projectStatus.status").Str) + if err != nil { + return AnalysisStatusUndefined, err + } - return processTaskStatusResponse(response) + r.log.Debugf("Analysis status returned in response was '%s'", status) + + return status, nil +} + +func processResponse(response *http.Response) (*gjson.Result, error) { + if response.StatusCode != 200 { + return nil, fmt.Errorf("server returned response with code %d", response.StatusCode) + } + + contentType := response.Header.Get("content-type") + if contentType != "application/json" { + return nil, fmt.Errorf("unexpected response content-type '%s'", contentType) + } + + responseBody, err := ioutil.ReadAll(response.Body) + if err != nil { + return nil, err + } + + responseJSON := gjson.ParseBytes(responseBody) + return &responseJSON, nil } func getTaskUrlFromFile(fileName string) (string, error) { @@ -320,21 +441,8 @@ func getTaskUrl(reader io.Reader) (string, error) { return "", errors.New("metadata file doesn't contain task url") } -func processTaskStatusResponse(response *http.Response) (TaskStatus, error) { - if response.StatusCode != http.StatusOK { - return TaskStatusUndefined, fmt.Errorf("unexpected response code %d", response.StatusCode) - } - - contentType := response.Header.Get("content-type") - if contentType != "application/json" { - return TaskStatusUndefined, fmt.Errorf("unexpected response content-type '%s'", contentType) - } - - responseBody, err := ioutil.ReadAll(response.Body) - if err != nil { - return TaskStatusUndefined, err - } - - responseJSON := gjson.ParseBytes(responseBody) - return parseTaskStatus(responseJSON.Get("task.status").Str) +func getApiUrl(host, endpoint string) string { + host = strings.TrimSuffix(host, "/") + endpoint = strings.TrimPrefix(endpoint, "/") + return fmt.Sprintf("%s/%s", host, endpoint) } diff --git a/internal/sonarscanner/sonar_scanner_test.go b/internal/sonarscanner/sonar_scanner_test.go index 0f4934d..90291eb 100644 --- a/internal/sonarscanner/sonar_scanner_test.go +++ b/internal/sonarscanner/sonar_scanner_test.go @@ -68,9 +68,7 @@ func TestProcessTaskStatusResponse(t *testing.T) { reader: strings.NewReader( ` { - "task": { - "status": "IN_PROGRESS" - } + "message": "ok" } `, ), @@ -80,10 +78,10 @@ func TestProcessTaskStatusResponse(t *testing.T) { }, } - status, err := processTaskStatusResponse(response) + json, err := processResponse(response) assert.Nil(t, err) - assert.Equal(t, status, TaskStatusInProgress) + assert.Equal(t, "ok", json.Get("message").Str) } func TestProcessTaskStatusResponseWithInvalidStatus(t *testing.T) { @@ -93,9 +91,7 @@ func TestProcessTaskStatusResponseWithInvalidStatus(t *testing.T) { reader: strings.NewReader( ` { - "task": { - "status": "IN_PROGRESS" - } + "message": "not ok" } `, ), @@ -105,10 +101,9 @@ func TestProcessTaskStatusResponseWithInvalidStatus(t *testing.T) { }, } - status, err := processTaskStatusResponse(response) + _, err := processResponse(response) assert.NotNil(t, err) - assert.Equal(t, status, TaskStatusUndefined) } func TestProcessTaskStatusResponseWithInvalidContentType(t *testing.T) { @@ -122,35 +117,9 @@ func TestProcessTaskStatusResponseWithInvalidContentType(t *testing.T) { }, } - status, err := processTaskStatusResponse(response) + _, err := processResponse(response) assert.NotNil(t, err) - assert.Equal(t, status, TaskStatusUndefined) -} - -func TestProcessTaskStatusResponseWithInvalidResponse(t *testing.T) { - response := &http.Response{ - StatusCode: 200, - Body: &testReader{ - reader: strings.NewReader( - ` - { - "task": { - "noStatus": "error" - } - } - `, - ), - }, - Header: http.Header{ - "Content-Type": {"application/json"}, - }, - } - - status, err := processTaskStatusResponse(response) - - assert.NotNil(t, err) - assert.Equal(t, status, TaskStatusUndefined) } func TestGetTaskUrlFromFile(t *testing.T) { @@ -324,3 +293,9 @@ func TestGetSonarScannerArgs(t *testing.T) { assert.Contains(t, args, "-Dsonar.scanner.metadataFilePath=/opt/mfp") assert.Contains(t, args, "-Dsonar.host.url=http://localhost:6969") } + +func TestGetApiUrl(t *testing.T) { + assert.Equal(t, "http://host/api/url/", getApiUrl("http://host/", "/api/url/")) + assert.Equal(t, "http://host/api/url", getApiUrl("http://host", "api/url")) + assert.Equal(t, "http://host/api/url", getApiUrl("http://host/", "api/url")) +} diff --git a/internal/sonarscanner/task_status.go b/internal/sonarscanner/task_status.go index eb52996..2d047d3 100644 --- a/internal/sonarscanner/task_status.go +++ b/internal/sonarscanner/task_status.go @@ -52,9 +52,6 @@ func parseTaskStatus(status string) (TaskStatus, error) { case taskStatusFailedStr: return TaskStatusFailed, nil default: - return TaskStatusUndefined, fmt.Errorf( - "unexpected task status '%s'", - status, - ) + return TaskStatusUndefined, fmt.Errorf("unexpected task status '%s'", status) } } diff --git a/internal/sonarscanner/task_status_test.go b/internal/sonarscanner/task_status_test.go index 1bbab53..918ca51 100644 --- a/internal/sonarscanner/task_status_test.go +++ b/internal/sonarscanner/task_status_test.go @@ -36,11 +36,7 @@ func TestParseTaskStatusInvalidInput(t *testing.T) { assert.Equal(t, status, TaskStatusUndefined) } -func assertTaskStatusParsedAs( - t *testing.T, - statusString string, - expectedStatus TaskStatus, -) { +func assertTaskStatusParsedAs(t *testing.T, statusString string, expectedStatus TaskStatus) { status, err := parseTaskStatus(statusString) assert.Nil(t, err)