Skip to content

Commit 9b9908a

Browse files
committed
SCANJLIB-169 Rework the API to support functional errors
The bootstrapper now returns a result with a boolean allowing the caller to terminate without throwing an exception. This is used to log auth errors, but it may be also useful for other functional errors.
1 parent 43a6c7b commit 9b9908a

File tree

17 files changed

+449
-132
lines changed

17 files changed

+449
-132
lines changed

its/it-simple-scanner/src/main/java/com/sonar/scanner/lib/it/Main.java

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,13 @@
2121

2222
import java.util.HashMap;
2323
import java.util.Map;
24+
import java.util.concurrent.atomic.AtomicBoolean;
2425
import org.sonarsource.scanner.lib.EnvironmentConfig;
2526
import org.sonarsource.scanner.lib.ScannerEngineBootstrapper;
2627

2728
public class Main {
2829
public static void main(String[] args) {
30+
AtomicBoolean success = new AtomicBoolean(false);
2931
try {
3032

3133
Map<String, String> props = new HashMap<>(EnvironmentConfig.load());
@@ -36,20 +38,25 @@ public static void main(String[] args) {
3638
}
3739
}
3840

39-
runProject(props);
41+
success.set(runScanner(props));
4042
} catch (Exception e) {
4143
e.printStackTrace();
42-
System.exit(1);
44+
System.exit(2);
4345
}
44-
System.exit(0);
46+
System.exit(success.get() ? 0 : 1);
4547
}
4648

47-
private static void runProject(Map<String, String> props) throws Exception {
49+
private static boolean runScanner(Map<String, String> props) throws Exception {
4850

49-
try (var scannerEngine = ScannerEngineBootstrapper.create("Simple Scanner", "1.0")
51+
try (var bootstrapResult = ScannerEngineBootstrapper.create("Simple Scanner", "1.0")
5052
.addBootstrapProperties(props)
5153
.bootstrap()) {
52-
scannerEngine.analyze(props);
54+
if (bootstrapResult.isSuccessful()) {
55+
bootstrapResult.getEngineFacade().analyze(props);
56+
return true;
57+
} else {
58+
return false;
59+
}
5360
}
5461
}
5562
}

its/it-tests/src/test/java/com/sonar/scanner/lib/it/ProxyTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -206,8 +206,8 @@ public void simple_analysis_with_proxy_auth() throws Exception {
206206
params.put("sonar.scanner.proxyPort", "" + httpProxyPort);
207207

208208
BuildResult buildResult = scanner.executeSimpleProject(project("js-sample"), ORCHESTRATOR.getServer().getUrl(), params, Map.of());
209-
assertThat(buildResult.getLastStatus()).isEqualTo(1);
210-
assertThat(buildResult.getLogs()).contains("Error status returned by url", ": 407");
209+
assertThat(buildResult.getLastStatus()).isNotZero();
210+
assertThat(buildResult.getLogs()).contains("Failed to query server version: Proxy Authentication Required.");
211211
assertThat(seenByProxy).isEmpty();
212212

213213
params.put("sonar.scanner.proxyUser", PROXY_USER);

its/it-tests/src/test/java/com/sonar/scanner/lib/it/SSLTest.java

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ private static void startSSLTransparentReverseProxy(boolean requireClientAuth) t
115115

116116
// Handler Structure
117117
HandlerCollection handlers = new HandlerCollection();
118-
handlers.setHandlers(new Handler[] {proxyHandler(), new DefaultHandler()});
118+
handlers.setHandlers(new Handler[]{proxyHandler(), new DefaultHandler()});
119119
server.setHandler(handlers);
120120

121121
ServerConnector http = new ServerConnector(server, new HttpConnectionFactory(httpConfig));
@@ -178,7 +178,7 @@ public void simple_analysis_with_server_and_client_certificate() throws Exceptio
178178
BuildResult buildResult = scanner.executeSimpleProject(project("js-sample"), "https://localhost:" + httpsPort);
179179

180180
assertThat(buildResult.getLastStatus()).isNotZero();
181-
assertThat(buildResult.getLogs()).contains("javax.net.ssl.SSLHandshakeException");
181+
assertThat(buildResult.getLogs()).contains("None of the TrustManagers trust this certificate chain");
182182

183183
Path clientTruststore = Paths.get(SSLTest.class.getResource(KEYSTORE_CLIENT_WITH_CA_KEYTOOL).toURI()).toAbsolutePath();
184184
assertThat(clientTruststore).exists();
@@ -204,7 +204,7 @@ public void simple_analysis_with_server_and_without_client_certificate_is_failin
204204
BuildResult buildResult = scanner.executeSimpleProject(project("js-sample"), "https://localhost:" + httpsPort);
205205

206206
assertThat(buildResult.getLastStatus()).isNotZero();
207-
assertThat(buildResult.getLogs()).contains("javax.net.ssl.SSLHandshakeException");
207+
assertThat(buildResult.getLogs()).contains("None of the TrustManagers trust this certificate chain");
208208

209209
Path clientTruststore = Paths.get(SSLTest.class.getResource(KEYSTORE_CLIENT_WITH_CA_KEYTOOL).toURI()).toAbsolutePath();
210210
assertThat(clientTruststore).exists();
@@ -218,16 +218,10 @@ public void simple_analysis_with_server_and_without_client_certificate_is_failin
218218
// Voluntary missing client keystore
219219

220220
buildResult = scanner.executeSimpleProject(project("js-sample"), "https://localhost:" + httpsPort, params, Map.of());
221-
assertThat(buildResult.getLastStatus()).isEqualTo(1);
221+
assertThat(buildResult.getLastStatus()).isNotZero();
222222

223-
// different exception is thrown depending on the JDK version. See: https://bugs.java.com/bugdatabase/view_bug.do?bug_id=8172163
224-
String failedAnalysis = "(?s).*java\\.lang\\.IllegalStateException: Failed to get server version.*";
225223
assertThat(buildResult.getLogs())
226-
.matches(p -> p.matches(failedAnalysis + "Caused by: javax\\.net\\.ssl\\.SSLException: Broken pipe \\(Write failed\\).*") ||
227-
p.matches(failedAnalysis + "Caused by: javax\\.net\\.ssl\\.SSLProtocolException: Broken pipe \\(Write failed\\).*") ||
228-
p.matches(failedAnalysis + "Caused by: javax\\.net\\.ssl\\.SSLHandshakeException: Received fatal alert: bad_certificate.*") ||
229-
p.matches(failedAnalysis + "Caused by: java\\.net\\.SocketException: Broken pipe.*") ||
230-
p.matches(failedAnalysis + "Caused by: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target.*"));
224+
.contains("Failed to query server version: Call to URL [https://localhost:" + httpsPort + "/api/v2/analysis/version] failed: Received fatal alert: bad_certificate");
231225
}
232226

233227
private static Path project(String projectName) {
@@ -244,7 +238,7 @@ public void simple_analysis_with_server_certificate(String clientTrustStore, Str
244238

245239
BuildResult buildResult = scanner.executeSimpleProject(project("js-sample"), "https://localhost:" + httpsPort);
246240
assertThat(buildResult.getLastStatus()).isNotZero();
247-
assertThat(buildResult.getLogs()).contains("javax.net.ssl.SSLHandshakeException");
241+
assertThat(buildResult.getLogs()).contains("None of the TrustManagers trust this certificate chain");
248242

249243
Path clientTruststore = Paths.get(SSLTest.class.getResource(clientTrustStore).toURI()).toAbsolutePath();
250244
assertThat(clientTruststore).exists();
@@ -265,7 +259,7 @@ public void simple_analysis_with_server_certificate(String clientTrustStore, Str
265259

266260
@DataProvider()
267261
public static Object[][] variousClientTrustStores() {
268-
return new Object[][] {
262+
return new Object[][]{
269263
{KEYSTORE_CLIENT_WITH_CA_KEYTOOL, CLIENT_WITH_CA_KEYSTORE_PASSWORD, true},
270264
{KEYSTORE_CLIENT_WITH_CA_OPENSSL, CLIENT_WITH_CA_KEYSTORE_PASSWORD, false},
271265
{KEYSTORE_CLIENT_WITH_CERTIFICATE_KEYTOOL, CLIENT_WITH_CERTIFICATE_KEYSTORE_PASSWORD, true},

lib/src/main/java/org/sonarsource/scanner/lib/EnvironmentConfig.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ public class EnvironmentConfig {
4040
private static final String GENERIC_ENV_PREFIX = "SONAR_SCANNER_";
4141
private static final String SONAR_HOST_URL_ENV_VAR = "SONAR_HOST_URL";
4242
private static final String SONAR_USER_HOME_ENV_VAR = "SONAR_USER_HOME";
43-
private static final String TOKEN_ENV_VARIABLE = "SONAR_TOKEN";
43+
static final String TOKEN_ENV_VARIABLE = "SONAR_TOKEN";
4444

4545
private EnvironmentConfig() {
4646
// only static methods
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* SonarScanner Java Library
3+
* Copyright (C) 2011-2024 SonarSource SA
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the GNU Lesser General Public
8+
* License as published by the Free Software Foundation; either
9+
* version 3 of the License, or (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+
* Lesser General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Lesser General Public License
17+
* along with this program; if not, write to the Free Software Foundation,
18+
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19+
*/
20+
package org.sonarsource.scanner.lib;
21+
22+
/**
23+
* Closing this will automatically close the {@link ScannerEngineFacade} that it contains, if any.
24+
*/
25+
public interface ScannerEngineBootstrapResult extends AutoCloseable {
26+
27+
/**
28+
* Allow to test if the bootstrapping has been successful. If not, the {@link ScannerEngineFacade} should not be used.
29+
* A log message should have been emitted in case of failure.
30+
*
31+
* @return true if the bootstrapping has been successful, false otherwise
32+
*/
33+
boolean isSuccessful();
34+
35+
/**
36+
* Get the facade to interact with the engine. Only call this method if {@link #isSuccessful()} returns true.
37+
*
38+
* @return the facade to interact with the engine
39+
*/
40+
ScannerEngineFacade getEngineFacade();
41+
}

lib/src/main/java/org/sonarsource/scanner/lib/ScannerEngineBootstrapper.java

Lines changed: 67 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -33,14 +33,18 @@
3333
import org.apache.commons.lang3.StringUtils;
3434
import org.slf4j.Logger;
3535
import org.slf4j.LoggerFactory;
36+
import org.sonarsource.scanner.lib.internal.FailedBootstrap;
3637
import org.sonarsource.scanner.lib.internal.InternalProperties;
38+
import org.sonarsource.scanner.lib.internal.MessageException;
39+
import org.sonarsource.scanner.lib.internal.SuccessfulBootstrap;
3740
import org.sonarsource.scanner.lib.internal.cache.FileCache;
3841
import org.sonarsource.scanner.lib.internal.facade.forked.NewScannerEngineFacade;
3942
import org.sonarsource.scanner.lib.internal.facade.forked.ScannerEngineLauncherFactory;
4043
import org.sonarsource.scanner.lib.internal.facade.inprocess.InProcessScannerEngineFacade;
4144
import org.sonarsource.scanner.lib.internal.facade.inprocess.IsolatedLauncherFactory;
4245
import org.sonarsource.scanner.lib.internal.facade.simulation.SimulationScannerEngineFacade;
4346
import org.sonarsource.scanner.lib.internal.http.HttpConfig;
47+
import org.sonarsource.scanner.lib.internal.http.HttpException;
4448
import org.sonarsource.scanner.lib.internal.http.ScannerHttpClient;
4549
import org.sonarsource.scanner.lib.internal.http.ssl.CertificateStore;
4650
import org.sonarsource.scanner.lib.internal.util.ArchResolver;
@@ -111,10 +115,7 @@ public ScannerEngineBootstrapper setBootstrapProperty(String key, String value)
111115
return this;
112116
}
113117

114-
/**
115-
* Bootstrap the scanner-engine.
116-
*/
117-
public ScannerEngineFacade bootstrap() {
118+
public ScannerEngineBootstrapResult bootstrap() {
118119
if (LOG.isDebugEnabled()) {
119120
LOG.debug("Scanner max available memory: {}", FileUtils.byteCountToDisplaySize(Runtime.getRuntime().maxMemory()));
120121
}
@@ -125,22 +126,59 @@ public ScannerEngineFacade bootstrap() {
125126
var isSimulation = immutableProperties.containsKey(InternalProperties.SCANNER_DUMP_TO_FILE);
126127
var sonarUserHome = resolveSonarUserHome(immutableProperties);
127128
var fileCache = FileCache.create(sonarUserHome);
128-
var httpConfig = new HttpConfig(immutableProperties, sonarUserHome);
129-
scannerHttpClient.init(httpConfig);
130-
String serverVersion = null;
131-
if (!isSonarCloud) {
132-
serverVersion = getServerVersion(scannerHttpClient, isSimulation, immutableProperties);
133-
}
134129

135130
if (isSimulation) {
136-
return new SimulationScannerEngineFacade(immutableProperties, isSonarCloud, serverVersion);
137-
} else if (isSonarCloud || VersionUtils.isAtLeastIgnoringQualifier(serverVersion, SQ_VERSION_NEW_BOOTSTRAPPING)) {
138-
var launcher = scannerEngineLauncherFactory.createLauncher(scannerHttpClient, fileCache, immutableProperties);
139-
return new NewScannerEngineFacade(immutableProperties, launcher, isSonarCloud, serverVersion);
131+
var serverVersion = immutableProperties.getOrDefault(InternalProperties.SCANNER_VERSION_SIMULATION, "9.9");
132+
return new SuccessfulBootstrap(new SimulationScannerEngineFacade(immutableProperties, isSonarCloud, serverVersion));
133+
}
134+
135+
// No HTTP call should be made before this point
136+
try {
137+
var httpConfig = new HttpConfig(immutableProperties, sonarUserHome);
138+
scannerHttpClient.init(httpConfig);
139+
140+
var serverVersion = !isSonarCloud ? getServerVersion(scannerHttpClient) : null;
141+
if (isSonarCloud || VersionUtils.isAtLeastIgnoringQualifier(serverVersion, SQ_VERSION_NEW_BOOTSTRAPPING)) {
142+
var launcher = scannerEngineLauncherFactory.createLauncher(scannerHttpClient, fileCache, immutableProperties);
143+
return new SuccessfulBootstrap(new NewScannerEngineFacade(immutableProperties, launcher, isSonarCloud, serverVersion));
144+
} else {
145+
var launcher = launcherFactory.createLauncher(scannerHttpClient, fileCache);
146+
var adaptedProperties = adaptDeprecatedPropertiesForInProcessBootstrapping(immutableProperties, httpConfig);
147+
return new SuccessfulBootstrap(new InProcessScannerEngineFacade(adaptedProperties, launcher, false, serverVersion));
148+
}
149+
} catch (MessageException e) {
150+
return handleException(e);
151+
}
152+
}
153+
154+
private static ScannerEngineBootstrapResult handleException(MessageException e) {
155+
var message = new StringBuilder(e.getMessage());
156+
if (e.getCause() instanceof HttpException) {
157+
var httpEx = (HttpException) e.getCause();
158+
var code = httpEx.getCode();
159+
if (code == 401 || code == 403) {
160+
var helpMessage = "Please check the property " + ScannerProperties.SONAR_TOKEN +
161+
" or the environment variable " + EnvironmentConfig.TOKEN_ENV_VARIABLE + ".";
162+
message.append(". ").append(helpMessage);
163+
}
164+
if (code == 407) {
165+
var helpMessage = "Please check the properties " + ScannerProperties.SONAR_SCANNER_PROXY_USER +
166+
" and " + ScannerProperties.SONAR_SCANNER_PROXY_PASSWORD + ".";
167+
message.append(". ").append(helpMessage);
168+
}
169+
}
170+
logWithStacktraceOnlyIfDebug(message.toString(), e);
171+
return new FailedBootstrap();
172+
}
173+
174+
/**
175+
* For functional errors, the stacktrace is not necessary. It is only useful for debugging.
176+
*/
177+
private static void logWithStacktraceOnlyIfDebug(String message, Throwable t) {
178+
if (LOG.isDebugEnabled()) {
179+
LOG.error(message, t);
140180
} else {
141-
var launcher = launcherFactory.createLauncher(scannerHttpClient, fileCache);
142-
var adaptedProperties = adaptDeprecatedPropertiesForInProcessBootstrapping(immutableProperties, httpConfig);
143-
return new InProcessScannerEngineFacade(adaptedProperties, launcher, false, serverVersion);
181+
LOG.error(message);
144182
}
145183
}
146184

@@ -195,20 +233,21 @@ private static Path resolveSonarUserHome(Map<String, String> properties) {
195233
return Paths.get(sonarUserHome);
196234
}
197235

198-
private static String getServerVersion(ScannerHttpClient scannerHttpClient, boolean isSimulation, Map<String, String> properties) {
199-
if (isSimulation) {
200-
return properties.getOrDefault(InternalProperties.SCANNER_VERSION_SIMULATION, "5.6");
201-
}
202-
236+
private static String getServerVersion(ScannerHttpClient scannerHttpClient) {
203237
try {
204238
return scannerHttpClient.callRestApi("/analysis/version");
205239
} catch (Exception e) {
206-
try {
207-
return scannerHttpClient.callWebApi("/api/server/version");
208-
} catch (Exception e2) {
209-
var ex = new IllegalStateException("Failed to get server version", e2);
210-
ex.addSuppressed(e);
211-
throw ex;
240+
if (e instanceof HttpException && ((HttpException) e).getCode() == 404) {
241+
// Fallback to the old endpoint
242+
try {
243+
return scannerHttpClient.callWebApi("/api/server/version");
244+
} catch (Exception e2) {
245+
var ex = new MessageException("Failed to query server version: " + e2.getMessage(), e2);
246+
ex.addSuppressed(e);
247+
throw ex;
248+
}
249+
} else {
250+
throw new MessageException("Failed to query server version: " + e.getMessage(), e);
212251
}
213252
}
214253
}

lib/src/main/java/org/sonarsource/scanner/lib/ScannerEngineFacade.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121

2222
import java.util.Map;
2323

24-
2524
public interface ScannerEngineFacade extends AutoCloseable {
2625

2726
/**
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* SonarScanner Java Library
3+
* Copyright (C) 2011-2024 SonarSource SA
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the GNU Lesser General Public
8+
* License as published by the Free Software Foundation; either
9+
* version 3 of the License, or (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+
* Lesser General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Lesser General Public License
17+
* along with this program; if not, write to the Free Software Foundation,
18+
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19+
*/
20+
package org.sonarsource.scanner.lib.internal;
21+
22+
import org.sonarsource.scanner.lib.ScannerEngineBootstrapResult;
23+
import org.sonarsource.scanner.lib.ScannerEngineFacade;
24+
25+
public class FailedBootstrap implements ScannerEngineBootstrapResult {
26+
27+
@Override
28+
public boolean isSuccessful() {
29+
return false;
30+
}
31+
32+
@Override
33+
public ScannerEngineFacade getEngineFacade() {
34+
throw new UnsupportedOperationException("No engine facade available");
35+
}
36+
37+
@Override
38+
public void close() {
39+
// No operation
40+
}
41+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* SonarScanner Java Library
3+
* Copyright (C) 2011-2024 SonarSource SA
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the GNU Lesser General Public
8+
* License as published by the Free Software Foundation; either
9+
* version 3 of the License, or (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+
* Lesser General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Lesser General Public License
17+
* along with this program; if not, write to the Free Software Foundation,
18+
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19+
*/
20+
package org.sonarsource.scanner.lib.internal;
21+
22+
/**
23+
* Functional error that should not log a stacktrace by default
24+
*/
25+
public class MessageException extends RuntimeException {
26+
public MessageException(String message) {
27+
super(message);
28+
}
29+
30+
public MessageException(String message, Throwable cause) {
31+
super(message, cause);
32+
}
33+
}

0 commit comments

Comments
 (0)