Skip to content

Commit a7b6274

Browse files
authored
Attest sonar wait flag (#507)
1 parent 04daf11 commit a7b6274

3 files changed

Lines changed: 78 additions & 27 deletions

File tree

cmd/kosli/attestSonar.go

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ type attestSonarOptions struct {
2424
projectKey string
2525
serverURL string
2626
revision string
27+
maxWait int
2728
payload SonarAttestationPayload
2829
}
2930

@@ -34,19 +35,24 @@ Retrieves results for the specified scan from SonarQube Cloud or SonarQube Serve
3435
The results are parsed to find the status of the project's quality gate which is used to determine the attestation's compliance status.
3536
3637
The scan to be retrieved can be specified in two ways:
37-
1. (Default) Using metadata created by the Sonar scanner. By default this is located within a temporary .scannerwork folder in the repo base directory.
38+
1. (Default) Using metadata created by the Sonar scanner. By default this is located within a temporary ^.scannerwork^ folder in the repo base directory.
3839
If you have overriden the location of this folder by passing parameters to the Sonar scanner, or are running Kosli's CLI locally outside the repo's base directory,
39-
you can provide the correct path using the --sonar-working-dir flag. This metadata is generated by a specific scan, allowing Kosli to retrieve the results of that scan.
40+
you can provide the correct path using the ^--sonar-working-dir^ flag. This metadata is generated by a specific scan, allowing Kosli to retrieve the results of that scan.
41+
If there are delays in the scan processing (either because the scanned project is very large, or because SonarQube is experiencing processing delays), it may happen that
42+
the scan results are not available by the time the attest sonar command is executed. In this case you can use the ^--max-wait^ flag to retry the command while waiting for the scan to be processed.
43+
This flag takes the maximum number of seconds to wait for the results to be available. The Kosli CLI will then attempt to retrieve the scan results until the maximum wait time is reached, with
44+
exponential backoff between retries. Once the results are available they are attested to Kosli as usual.
45+
4046
2. Providing the Sonar project key and the revision of the scan (plus the SonarQube server URL if relevant). If running the Kosli CLI in some CI/CD pipeline, the revision
4147
is defaulted to the commit SHA. If you are running the command locally, or have overriden the revision in SonarQube via parameters to the Sonar scanner, you can
42-
provide the correct revision using the --sonar-revision flag. Kosli then finds the scan results for the specified project key and revision.
48+
provide the correct revision using the ^--sonar-revision^ flag. Kosli then finds the scan results for the specified project key and revision.
4349
4450
Note that if your project is very large and you are using SonarQube Cloud's automatic analysis, it is possible for the attest sonar command to run before the SonarQube Cloud scan is completed.
4551
In this case, we recommend using Kosli's Sonar webhook integration ( https://docs.kosli.com/integrations/sonar/ ) rather than the CLI to attest the scan results.
4652
` + attestationBindingDesc
4753

4854
const attestSonarExample = `
49-
# report a SonarQube Cloud attestation about a trail using Sonar's metadata:
55+
# report a SonarQube Cloud attestation about a trail using SonarQube's metadata, with no retries:
5056
kosli attest sonar \
5157
--name yourAttestationName \
5258
--flow yourFlowName \
@@ -56,7 +62,7 @@ kosli attest sonar \
5662
--api-token yourAPIToken \
5763
--org yourOrgName \
5864
59-
# report a SonarQube Server attestation about a trail using Sonar's metadata:
65+
# report a SonarQube Server attestation about a trail using SonarQube's metadata, waiting for up to 60 seconds for the results to be available:
6066
kosli attest sonar \
6167
--name yourAttestationName \
6268
--flow yourFlowName \
@@ -65,6 +71,7 @@ kosli attest sonar \
6571
--sonar-working-dir yourSonarWorkingDirPath \
6672
--api-token yourAPIToken \
6773
--org yourOrgName \
74+
--max-wait 60
6875
6976
# report a SonarQube Cloud attestation for a specific branch about a trail using key/revision:
7077
kosli attest sonar \
@@ -91,7 +98,7 @@ kosli attest sonar \
9198
--api-token yourAPIToken \
9299
--org yourOrgName \
93100
94-
# report a SonarQube Cloud attestation about a trail with an attachment using Sonar's metadata:
101+
# report a SonarQube Cloud attestation about a trail with an attachment using SonarQube's metadata, waiting for up to 300 seconds for the results to be available:
95102
kosli attest sonar \
96103
--name yourAttestationName \
97104
--flow yourFlowName \
@@ -100,7 +107,8 @@ kosli attest sonar \
100107
--sonar-working-dir yourSonarWorkingDirPath \
101108
--attachment yourAttachmentPath \
102109
--api-token yourAPIToken \
103-
--org yourOrgName
110+
--org yourOrgName \
111+
--max-wait 300
104112
`
105113

106114
func newAttestSonarCmd(out io.Writer) *cobra.Command {
@@ -156,6 +164,7 @@ func newAttestSonarCmd(out io.Writer) *cobra.Command {
156164
cmd.Flags().StringVar(&o.projectKey, "sonar-project-key", "", sonarProjectKeyFlag)
157165
cmd.Flags().StringVar(&o.serverURL, "sonar-server-url", "https://sonarcloud.io", sonarServerURLFlag)
158166
cmd.Flags().StringVar(&o.revision, "sonar-revision", o.commitSHA, sonarRevisionFlag)
167+
cmd.Flags().IntVar(&o.maxWait, "max-wait", 30, sonarMaxWaitFlag)
159168

160169
err := RequireFlags(cmd, []string{"flow", "trail", "name", "sonar-api-token"})
161170
if err != nil {
@@ -173,9 +182,9 @@ func (o *attestSonarOptions) run(args []string) error {
173182
return err
174183
}
175184

176-
sc := sonar.NewSonarConfig(o.apiToken, o.workingDir, o.ceTaskURL, o.projectKey, o.serverURL, o.revision)
185+
sc := sonar.NewSonarConfig(o.apiToken, o.workingDir, o.ceTaskURL, o.projectKey, o.serverURL, o.revision, o.maxWait)
177186

178-
o.payload.SonarResults, err = sc.GetSonarResults()
187+
o.payload.SonarResults, err = sc.GetSonarResults(logger)
179188
if err != nil {
180189
return err
181190
}

cmd/kosli/root.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,7 @@ The ^.kosli_ignore^ will be treated as part of the artifact like any other file,
241241
sonarProjectKeyFlag = "[conditional] The project key of the SonarCloud/SonarQube project. Only required if you want to use the project key/revision to get the scan results rather than using Sonar's metadata file."
242242
sonarServerURLFlag = "[conditional] The URL of your SonarQube server. Only required if you are using SonarQube and not using SonarQube's metadata file to get scan results."
243243
sonarRevisionFlag = "[conditional] The revision of the SonarCloud/SonarQube project. Only required if you want to use the project key/revision to get the scan results rather than using Sonar's metadata file and you have overridden the default revision, or you aren't using a CI. Defaults to the value of the git commit flag."
244+
sonarMaxWaitFlag = "[optional] Allow the command to wait and retry fetching the scan results from SonarQube, up to the maximum number of seconds provided, with exponential backoff. Useful when using SonarQube's metadata file to retrieve and attest scans that take a long time to process . Defaults to 30 seconds."
244245
logicalEnvFlag = "[required] The logical environment."
245246
physicalEnvFlag = "[required] The physical environment."
246247
attestationTypeDescriptionFlag = "[optional] The attestation type description."

internal/sonar/sonar.go

Lines changed: 59 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ import (
88
"os"
99
"path/filepath"
1010
"strings"
11+
"time"
12+
13+
log "github.com/kosli-dev/cli/internal/logger"
1114
)
1215

1316
type SonarConfig struct {
@@ -17,6 +20,7 @@ type SonarConfig struct {
1720
revision string
1821
projectKey string
1922
serverURL string
23+
maxWait int
2024
}
2125

2226
// Structs to build the JSON for our attestation payload
@@ -109,18 +113,19 @@ type Error struct {
109113
Msg string `json:"msg"`
110114
}
111115

112-
func NewSonarConfig(apiToken, workingDir, ceTaskUrl, projectKey, serverURL, revision string) *SonarConfig {
116+
func NewSonarConfig(apiToken, workingDir, ceTaskUrl, projectKey, serverURL, revision string, maxWait int) *SonarConfig {
113117
return &SonarConfig{
114118
APIToken: apiToken,
115119
WorkingDir: workingDir,
116120
CETaskUrl: ceTaskUrl,
117121
revision: revision,
118122
projectKey: projectKey,
119123
serverURL: serverURL,
124+
maxWait: maxWait,
120125
}
121126
}
122127

123-
func (sc *SonarConfig) GetSonarResults() (*SonarResults, error) {
128+
func (sc *SonarConfig) GetSonarResults(logger *log.Logger) (*SonarResults, error) {
124129
httpClient := &http.Client{}
125130
var analysisID, tokenHeader string
126131
var err error
@@ -159,7 +164,7 @@ func (sc *SonarConfig) GetSonarResults() (*SonarResults, error) {
159164

160165
if analysisID == "" {
161166
//Get the analysis ID, status, project name and branch data from the ceTaskURL (ce API)
162-
analysisID, err = GetCETaskData(httpClient, project, sonarResults, sc.CETaskUrl, tokenHeader)
167+
analysisID, err = GetCETaskData(httpClient, project, sonarResults, sc.CETaskUrl, tokenHeader, sc.maxWait, logger)
163168
if err != nil {
164169
return nil, err
165170
}
@@ -210,32 +215,69 @@ func (sc *SonarConfig) readFile(project *Project, results *SonarResults) error {
210215
return nil
211216
}
212217

213-
func GetCETaskData(httpClient *http.Client, project *Project, sonarResults *SonarResults, ceTaskURL, tokenHeader string) (string, error) {
218+
func GetCETaskData(httpClient *http.Client, project *Project, sonarResults *SonarResults, ceTaskURL, tokenHeader string, maxWait int, logger *log.Logger) (string, error) {
214219
taskRequest, err := http.NewRequest("GET", ceTaskURL, nil)
215220
taskRequest.Header.Add("Authorization", tokenHeader)
216221
if err != nil {
217222
return "", err
218223
}
219224

220-
taskResponse, err := httpClient.Do(taskRequest)
221-
if err != nil {
222-
return "", err
225+
wait := 1 // start wait period
226+
retries := 0 // number of retries so far
227+
elapsed := 0 // seconds elapsed
228+
taskResponseData := &TaskResponse{}
229+
230+
for elapsed < maxWait {
231+
taskResponse, err := httpClient.Do(taskRequest)
232+
if err != nil {
233+
return "", err
234+
}
235+
236+
err = json.NewDecoder(taskResponse.Body).Decode(taskResponseData)
237+
if err != nil {
238+
return "", fmt.Errorf("please check your API token is correct and you have the correct permissions in SonarQube")
239+
}
240+
241+
if taskResponseData.Task.Status == "PENDING" || taskResponseData.Task.Status == "IN_PROGRESS" {
242+
// So that we don't wait longer than maxWait
243+
if (elapsed + wait) > maxWait {
244+
wait = maxWait - elapsed
245+
}
246+
247+
logger.Info("retry %d: waiting %ds for SonarQube scan to be processed...", retries+1, wait)
248+
time.Sleep(time.Duration(wait) * time.Second)
249+
250+
if elapsed > 300 { // If we've waited 5 minutes, we'll wait 300 seconds (5 minutes) before checking again. This is so that we don't end up with extremely long waiting intervals with the doubling approach.
251+
elapsed += wait
252+
retries++
253+
wait += 300
254+
} else { // Otherwise, we'll double the wait time each time
255+
elapsed += wait
256+
retries++
257+
wait *= 2
258+
}
259+
} else {
260+
taskResponse.Body.Close()
261+
break
262+
}
223263
}
224264

225-
taskResponseData := &TaskResponse{}
226-
err = json.NewDecoder(taskResponse.Body).Decode(taskResponseData)
227-
if err != nil {
228-
return "", fmt.Errorf("please check your API token is correct and you have the correct permissions in SonarQube")
265+
if elapsed != 0 {
266+
logger.Info("Waited for %d seconds for SonarQube scan to be processed. %d retries.\n", elapsed, retries)
229267
}
230268

231-
project.Name = taskResponseData.Task.ComponentName
232-
project.Key = taskResponseData.Task.ComponentKey
233-
sonarResults.TaskID = taskResponseData.Task.TaskID
234-
analysisId := taskResponseData.Task.AnalysisID
235-
sonarResults.Status = taskResponseData.Task.Status
269+
task := taskResponseData.Task
270+
271+
project.Name = task.ComponentName
272+
project.Key = task.ComponentKey
273+
sonarResults.TaskID = task.TaskID
274+
analysisId := task.AnalysisID
275+
sonarResults.Status = task.Status
236276

277+
// This should only happen if the task is pending - either because the project is large and the scan takes a long time
278+
// to process, or because SonarQube is experiencing delays for some reason.
237279
if analysisId == "" {
238-
return "", fmt.Errorf("analysis ID not found on %s", sonarResults.ServerUrl) // This should never happen
280+
return "", fmt.Errorf("analysis ID not found on %s. The scan results are not yet available, likely due to: \n1. Your project being particularly large and the scan taking time to process, or \n2. SonarQube is experiencing delays in processing scans. \nTry rerunning the command with the --max-retries flag.", sonarResults.ServerUrl)
239281
}
240282

241283
if project.Url == "" {
@@ -250,7 +292,6 @@ func GetCETaskData(httpClient *http.Client, project *Project, sonarResults *Sona
250292
sonarResults.Branch = nil
251293
}
252294

253-
taskResponse.Body.Close()
254295
return analysisId, nil
255296
}
256297

0 commit comments

Comments
 (0)