Skip to content

Commit

Permalink
Retrieve quality gate status after the analysis is finished
Browse files Browse the repository at this point in the history
  • Loading branch information
LowCostCustoms committed Feb 11, 2021
1 parent 4beb848 commit 85b73d2
Show file tree
Hide file tree
Showing 7 changed files with 263 additions and 91 deletions.
18 changes: 13 additions & 5 deletions action.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
51 changes: 51 additions & 0 deletions internal/sonarscanner/analysis_status.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
37 changes: 37 additions & 0 deletions internal/sonarscanner/analysis_status_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
188 changes: 148 additions & 40 deletions internal/sonarscanner/sonar_scanner.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,15 @@ import (
"os/exec"
"path"
"regexp"
"strings"
"time"

"github.com/sirupsen/logrus"
"github.com/tidwall/gjson"
)

var QualityGateWaitTimeout = errors.New("quality gate wait timeout")
var AnalysisStatusWaitTimeout = errors.New("analysis status wait timeout")

const (
defaultWaitTimeout = 2 * time.Second
Expand Down Expand Up @@ -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 == "" {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
Expand All @@ -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) {
Expand Down Expand Up @@ -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)
}
Loading

0 comments on commit 85b73d2

Please sign in to comment.