forked from SonarSource/sonar-scanner-jenkins
-
Notifications
You must be signed in to change notification settings - Fork 57
/
SonarUtils.java
285 lines (249 loc) · 11.5 KB
/
SonarUtils.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
/*
* SonarQube Scanner for Jenkins
* Copyright (C) 2007-2024 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package hudson.plugins.sonar.utils;
import com.cloudbees.plugins.credentials.CredentialsProvider;
import hudson.EnvVars;
import hudson.FilePath;
import hudson.Util;
import hudson.model.Action;
import hudson.model.Actionable;
import hudson.model.Result;
import hudson.model.Run;
import hudson.model.TaskListener;
import hudson.plugins.sonar.SonarInstallation;
import hudson.plugins.sonar.action.SonarAnalysisAction;
import hudson.plugins.sonar.client.HttpClient;
import hudson.plugins.sonar.client.WsClient;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.LinkedList;
import java.util.List;
import java.util.Properties;
import java.util.logging.Level;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import javax.annotation.CheckForNull;
import javax.annotation.Nullable;
import org.apache.tools.ant.DirectoryScanner;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.types.FileSet;
import org.jenkinsci.plugins.plaincredentials.StringCredentials;
public final class SonarUtils {
public static final String SERVER_URL_KEY = "serverUrl";
public static final String DASHBOARD_URL_KEY = "dashboardUrl";
public static final String CE_TASK_ID_KEY = "ceTaskId";
public static final String REPORT_TASK_FILE_NAME = "report-task.txt";
public static final String PROPERTY_SONAR_LOGIN = "sonar.login";
public static final String PROPERTY_SONAR_TOKEN = "sonar.token";
private static final String SONARCLOUD_URL = "https://sonarcloud.io";
/**
* Hide utility-class constructor.
*/
private SonarUtils() {
}
public static <T extends Action> List<T> getPersistentActions(Actionable actionable, Class<T> type) {
List<T> filtered = new LinkedList<>();
// we use this method to avoid recursively calling transitive action factories
for (Action a : actionable.getActions()) {
if (a == null) {
continue;
}
if (type.isAssignableFrom(a.getClass())) {
filtered.add((T) a);
}
}
return filtered;
}
@CheckForNull
public static <T extends Action> T getPersistentAction(Actionable actionable, Class<T> type) {
// we use this method to avoid recursively calling transitive action factories
for (Action a : actionable.getActions()) {
if (a == null) {
continue;
}
if (type.isAssignableFrom(a.getClass())) {
return (T) a;
}
}
return null;
}
public static Properties extractReportTask(TaskListener listener, FilePath workspace) throws IOException, InterruptedException {
FilePath[] candidates = null;
if (workspace.exists()) {
candidates = find(workspace, "**/" + REPORT_TASK_FILE_NAME);
}
if (candidates == null || candidates.length == 0) {
listener.getLogger().println("WARN: Unable to locate '" + REPORT_TASK_FILE_NAME + "' in the workspace. Did the SonarScanner succeed?");
return null;
} else {
if (candidates.length > 1) {
listener.getLogger().println("WARN: Found multiple '" + REPORT_TASK_FILE_NAME + "' in the workspace. Taking the first one.");
Stream.of(candidates).forEach(p -> listener.getLogger().println(p));
}
FilePath reportTaskFile = candidates[0];
try (InputStream in = reportTaskFile.read()) {
Properties p = new Properties();
p.load(new InputStreamReader(in, StandardCharsets.UTF_8));
return p;
}
}
}
/**
* This method does the same as {@link FilePath#list(String)} but does not follow symlinks.
*/
private static FilePath[] find(FilePath workspace, String includes) {
FileSet fs = Util.createFileSet(new File(workspace.getRemote()), includes, null);
fs.setDefaultexcludes(true);
fs.setFollowSymlinks(false);
DirectoryScanner ds = fs.getDirectoryScanner(new Project());
String[] includedFiles = ds.getIncludedFiles();
FilePath[] candidates = new FilePath[includedFiles.length];
for (int i = 0; i < candidates.length; i++) {
candidates[i] = new FilePath(new File(workspace.getRemote(), includedFiles[i]));
}
return candidates;
}
@Nullable
/**
* Collects as much information as it finds from the sonar analysis in the build and adds it as an action to the build.
* Even if no information is found, the action is added, marking in the build that a sonar analysis ran.
*/
public static SonarAnalysisAction addBuildInfoTo(Run<?, ?> build, TaskListener listener, FilePath workspace, SonarInstallation sonarInstallation, @Nullable String credentialId,
boolean skippedIfNoBuild)
throws IOException, InterruptedException {
SonarAnalysisAction buildInfo = createSonarAnalysisAction(sonarInstallation, credentialId, build, listener);
boolean java11Warning = build.getLog(Integer.MAX_VALUE).stream()
.anyMatch(line -> line.contains("Please update to at least Java 11."));
Result result = build.getResult();
if (java11Warning && !Result.FAILURE.equals(result)) {
build.setResult(Result.UNSTABLE);
listener.getLogger().println("Pipeline marked as 'UNSTABLE'. Please update to at least Java 11. " +
"Find more information here on how to do this: https://sonarcloud.io/documentation/appendices/move-analysis-java-11/");
}
Properties reportTask = extractReportTask(listener, workspace);
if (reportTask != null) {
buildInfo.setServerUrl(reportTask.getProperty(SERVER_URL_KEY));
buildInfo.setUrl(reportTask.getProperty(DASHBOARD_URL_KEY));
buildInfo.setCeTaskId(reportTask.getProperty(CE_TASK_ID_KEY));
} else {
return addBuildInfoFromLastBuildTo(build, listener, sonarInstallation, credentialId, skippedIfNoBuild);
}
build.addAction(buildInfo);
return buildInfo;
}
public static SonarAnalysisAction addBuildInfoTo(Run<?, ?> build, TaskListener listener, FilePath workspace, SonarInstallation sonarInstallation)
throws IOException, InterruptedException {
return addBuildInfoTo(build, listener, workspace, sonarInstallation, sonarInstallation.getCredentialsId());
}
public static SonarAnalysisAction addBuildInfoTo(Run<?, ?> build, TaskListener listener, FilePath workspace, SonarInstallation sonarInstallation, @Nullable String credentialId)
throws IOException, InterruptedException {
return addBuildInfoTo(build, listener, workspace, sonarInstallation, credentialId, false);
}
public static SonarAnalysisAction addBuildInfoFromLastBuildTo(Run<?, ?> build, TaskListener listener, SonarInstallation sonarInstallation, boolean isSkipped)
throws IOException, InterruptedException {
return addBuildInfoFromLastBuildTo(build, listener, sonarInstallation, sonarInstallation.getCredentialsId(), isSkipped);
}
public static SonarAnalysisAction addBuildInfoFromLastBuildTo(Run<?, ?> build, TaskListener listener, SonarInstallation sonarInstallation, @Nullable String credentialId,
boolean isSkipped) throws IOException, InterruptedException {
Run<?, ?> previousBuild = build.getPreviousBuild();
if (previousBuild == null) {
return addEmptyBuildInfo(build, listener, sonarInstallation, credentialId, isSkipped);
}
for (SonarAnalysisAction analysis : previousBuild.getActions(SonarAnalysisAction.class)) {
if (analysis.getUrl() != null && analysis.getInstallationName().equals(sonarInstallation.getName())) {
SonarAnalysisAction copy = new SonarAnalysisAction(analysis);
copy.setSkipped(isSkipped);
build.addAction(copy);
return copy;
}
}
return addEmptyBuildInfo(build, listener, sonarInstallation, credentialId, isSkipped);
}
public static SonarAnalysisAction addEmptyBuildInfo(Run<?, ?> build, TaskListener listener, SonarInstallation sonarInstallation, @Nullable String credentialId, boolean isSkipped)
throws IOException, InterruptedException {
SonarAnalysisAction analysis = createSonarAnalysisAction(sonarInstallation, credentialId, build, listener);
analysis.setSkipped(isSkipped);
build.addAction(analysis);
return analysis;
}
private static SonarAnalysisAction createSonarAnalysisAction(SonarInstallation sonarInstallation, @Nullable String credentialId, Run<?, ?> build, TaskListener listener)
throws IOException, InterruptedException {
EnvVars envVars = BuilderUtils.getEnvAndBuildVars(build, listener);
return new SonarAnalysisAction(sonarInstallation.getName(), credentialId, envVars.expand(sonarInstallation.getServerUrl()));
}
public static String getMavenGoal(String version) {
Float majorMinor = extractMajorMinor(version);
if (majorMinor == null || majorMinor >= 3.0) {
return "org.sonarsource.scanner.maven:sonar-maven-plugin:" + version + ":sonar";
} else {
return "org.codehaus.mojo:sonar-maven-plugin:" + version + ":sonar";
}
}
@CheckForNull
public static Float extractMajorMinor(String version) {
Pattern p = Pattern.compile("\\d+\\.\\d+");
Matcher m = p.matcher(version);
if (m.find()) {
return Float.parseFloat(m.group());
}
return null;
}
@CheckForNull
public static String getAuthenticationToken(Run<?, ?> build, SonarInstallation inst, @Nullable String credentialsId) {
if (credentialsId == null) {
return inst.getServerAuthenticationToken(build);
}
StringCredentials cred = getCredentials(build, credentialsId);
if (cred == null) {
throw new IllegalStateException("Unable to find credential with id '" + credentialsId + "'");
}
return cred.getSecret().getPlainText();
}
public static StringCredentials getCredentials(Run<?, ?> build, String credentialsId) {
return CredentialsProvider.findCredentialById(credentialsId, StringCredentials.class, build);
}
public static String getTokenProperty(SonarInstallation inst, HttpClient client) {
try {
if (!isSonarCloud(inst) && getVersion(inst, client).compareTo(new Version("10.0")) < 0) {
return PROPERTY_SONAR_LOGIN;
} else {
return PROPERTY_SONAR_TOKEN;
}
} catch (Exception e) {
Logger.LOG.log(Level.WARNING, String.format("Failed to retrieve SonarQube instance version, '%s' is used by default",
PROPERTY_SONAR_LOGIN), e);
return PROPERTY_SONAR_LOGIN;
}
}
public static Version getVersion(SonarInstallation inst, HttpClient client) {
if (inst.getServerUrl() == null) {
throw new IllegalStateException("No server url on installation: " + inst.getName());
}
WsClient wsClient = new WsClient(client, inst.getServerUrl(), null);
return new Version(wsClient.getServerVersion());
}
public static boolean isSonarCloud(SonarInstallation inst) {
return inst.getServerUrl() != null && inst.getServerUrl().startsWith(SONARCLOUD_URL);
}
}