Skip to content
This repository has been archived by the owner on Feb 5, 2024. It is now read-only.

Gatebreaker #11

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions DOCS.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ Safety first, the host and token are stored in Drone Secrets.
* TRACE: Display DEBUG logs + the timings of all ElasticSearch queries and Web API calls executed by the SonarQube Scanner.
* `showProfiling`: Display logs to see where the analyzer spends time. Default value `false`
* `branchAnalysis`: Pass currently analysed branch to SonarQube. (Must not be active for initial scan!) Default value `false`
* `enableGateBreaker`: Abort pipeline if quality gate fais. Default value `false`


* `usingProperties`: Using the `sonar-project.properties` file in root directory as sonar parameters. (Not include `sonar_host` and
Expand Down
44 changes: 26 additions & 18 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,31 +92,39 @@ func main() {
Usage: "using sonar-project.properties",
EnvVar: "PLUGIN_USINGPROPERTIES",
},
cli.BoolFlag{
Name: "enableGateBreaker",
Usage: "fail if quality gate fails",
EnvVar: "PLUGIN_ENABLEGATEBREAKER",
},
}

app.Run(os.Args)
}

func run(c *cli.Context) {
plugin := Plugin{
Config: Config{
Key: c.String("key"),
Name: c.String("name"),
Host: c.String("host"),
Token: c.String("token"),

Version: c.String("ver"),
Branch: c.String("branch"),
Timeout: c.String("timeout"),
Sources: c.String("sources"),
Inclusions: c.String("inclusions"),
Exclusions: c.String("exclusions"),
Level: c.String("level"),
ShowProfiling: c.String("showProfiling"),
BranchAnalysis: c.Bool("branchAnalysis"),
UsingProperties: c.Bool("usingProperties"),
config := Config{
Key: c.String("key"),
Name: c.String("name"),
Host: c.String("host"),
Token: c.String("token"),

},
Version: c.String("ver"),
Branch: c.String("branch"),
Timeout: c.String("timeout"),
Sources: c.String("sources"),
Inclusions: c.String("inclusions"),
Exclusions: c.String("exclusions"),
Level: c.String("level"),
ShowProfiling: c.String("showProfiling"),
BranchAnalysis: c.Bool("branchAnalysis"),
UsingProperties: c.Bool("usingProperties"),
EnableGateBreaker: c.Bool("enableGateBreaker"),
}
plugin, pluginError := NewPlugin(config)
if pluginError != nil {
fmt.Println(pluginError)
os.Exit(1)
}

if err := plugin.Exec(); err != nil {
Expand Down
73 changes: 58 additions & 15 deletions plugin.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package main

import (
"bytes"
"fmt"
sonargo "github.com/magicsong/sonargo/sonar"
"os"
"os/exec"
"strings"
Expand All @@ -14,31 +16,38 @@ type (
Host string
Token string

Version string
Branch string
Sources string
Timeout string
Inclusions string
Exclusions string
Level string
ShowProfiling string
BranchAnalysis bool
UsingProperties bool
Version string
Branch string
Sources string
Timeout string
Inclusions string
Exclusions string
Level string
ShowProfiling string
BranchAnalysis bool
UsingProperties bool
EnableGateBreaker bool
}
Plugin struct {
Config Config
Config Config
SonarClient *sonargo.Client
}
)

func (p Plugin) Exec() error {
func (p Plugin) getProjectKey() string {
return strings.Replace(p.Config.Key, "/", ":", -1)
}

// Returns array of arguments that will be used during the command call
func (p Plugin) getCommandArgs() []string {
args := []string{
"-Dsonar.host.url=" + p.Config.Host,
"-Dsonar.login=" + p.Config.Token,
}

if !p.Config.UsingProperties {
argsParameter := []string{
"-Dsonar.projectKey=" + strings.Replace(p.Config.Key, "/", ":", -1),
"-Dsonar.projectKey=" + p.getProjectKey(),
"-Dsonar.projectName=" + p.Config.Name,
"-Dsonar.projectVersion=" + p.Config.Version,
"-Dsonar.sources=" + p.Config.Sources,
Expand All @@ -57,15 +66,49 @@ func (p Plugin) Exec() error {
args = append(args, "-Dsonar.branch.name=" + p.Config.Branch)
}

return args
}

func (p Plugin) Exec() error {
args := p.getCommandArgs()
cmd := exec.Command("sonar-scanner", args...)
// fmt.Printf("==> Executing: %s\n", strings.Join(cmd.Args, " "))
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr

var outb, errb bytes.Buffer
cmd.Stdout = &outb
cmd.Stderr = &errb
fmt.Printf("==> Code Analysis Result:\n")
err := cmd.Run()
if err != nil {
return err
}

_, _ = os.Stdout.Write(outb.Bytes())
_, _ = os.Stderr.Write(errb.Bytes())

if p.Config.EnableGateBreaker {
// Extract task id from command log
taskId, extractError := p.extractReportIdFromAnalysisLog(outb.String())
if extractError != nil {
return extractError
}
// Check if quality gate succeeded
if err = p.validateQualityGate(taskId); err != nil {
return err
}
}

return nil
}

func NewPlugin(config Config) (*Plugin, error) {
client, err := sonargo.NewClientByToken(config.Host+"/api", config.Token)
if err != nil {
return nil, err
}

return &Plugin{
Config: config,
SonarClient: client,
}, nil
}
74 changes: 74 additions & 0 deletions sonarapi.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package main

import (
"fmt"
sonargo "github.com/magicsong/sonargo/sonar"
"regexp"
"time"
)

const SONAR_TIME_FORMAT = "2006-01-02T15:04:05-0700"

// How long to wait between consecutive API requests (seconds)
const SONAR_REQUEST_SLEEP = 10

/**
* Extracts the report id from code analysis result
* The corresponding line looks like this:
More about the report processing at https://sonarcloud.io/api/ce/task?id=AW6aSZwxGXJ8Zd7jkmP2
*/
func (p Plugin) extractReportIdFromAnalysisLog(analysisLog string) (string, error) {
var re = regexp.MustCompile(`(?m)More about the report processing at .*\/api\/ce\/task\?id=(.*)$`)
matches := re.FindStringSubmatch(analysisLog)
if len(matches) != 2 { // expect exactly 2 results. 0 is full string. 1 is the id
return "", fmt.Errorf("unable to get report processing url from analysis result.")
}
return matches[1], nil
}

func (p Plugin) getCompletedTaskReport(taskId string) (*sonargo.CeTaskObject, error) {
for {
taskObject, _, apiError := p.SonarClient.Ce.Task(&sonargo.CeTaskOption{Id: taskId})
if apiError != nil {
return nil, apiError
}
startedTime, timeErr := time.Parse(SONAR_TIME_FORMAT, taskObject.Task.StartedAt)
if timeErr != nil {
return nil, timeErr
}
taskStatus := taskObject.Task.Status
if taskStatus != "SUCCESS" && taskStatus != "FAILED" {
fmt.Printf("Awaiting completion of analysis. Current status is \"%s\". Analysis started on %s.\n", taskStatus, startedTime)
time.Sleep(SONAR_REQUEST_SLEEP * time.Second)
continue
}
completedTime, timeErr := time.Parse(SONAR_TIME_FORMAT, taskObject.Task.ExecutedAt)
if timeErr != nil {
return nil, timeErr
}
fmt.Printf("Analysis completed on %s with status \"%s\".\n", completedTime, taskStatus)
if taskStatus == "FAILED" {
return nil, fmt.Errorf("pipeline aborted because processing by the Sonar server failed")
}
return taskObject, nil
}
}

func (p Plugin) validateQualityGate(taskId string) error {
// Get completed analysis report
ceTask, err := p.getCompletedTaskReport(taskId)
if err != nil {
return err
}

// Check Quality Gate
projectStatusOption := &sonargo.QualitygatesProjectStatusOption{AnalysisId: ceTask.Task.AnalysisID}
qualitygate, _, err := p.SonarClient.Qualitygates.ProjectStatus(projectStatusOption)
if err != nil {
return err
}
if qualitygate.ProjectStatus.Status == "ERROR" {
return fmt.Errorf("pipeline aborted because quality gate failed")
}
return nil
}