Skip to content

Commit 5b67c37

Browse files
authored
SCANJLIB-263 Add support for the upcoming SonarQube Cloud US region
1 parent fee2445 commit 5b67c37

15 files changed

+564
-64
lines changed

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ public class EnvironmentConfig {
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";
4343
public static final String TOKEN_ENV_VARIABLE = "SONAR_TOKEN";
44+
public static final String REGION_ENV_VARIABLE = "SONAR_REGION";
4445

4546
private EnvironmentConfig() {
4647
// only static methods
@@ -54,7 +55,8 @@ public static Map<String, String> load(Map<String, String> env) {
5455
var loadedProps = new HashMap<String, String>();
5556
Optional.ofNullable(env.get(SONAR_HOST_URL_ENV_VAR)).ifPresent(url -> loadedProps.put(ScannerProperties.HOST_URL, url));
5657
Optional.ofNullable(env.get(SONAR_USER_HOME_ENV_VAR)).ifPresent(path -> loadedProps.put(ScannerProperties.SONAR_USER_HOME, path));
57-
Optional.ofNullable(env.get(TOKEN_ENV_VARIABLE)).ifPresent(path -> loadedProps.put(ScannerProperties.SONAR_TOKEN, path));
58+
Optional.ofNullable(env.get(TOKEN_ENV_VARIABLE)).ifPresent(token -> loadedProps.put(ScannerProperties.SONAR_TOKEN, token));
59+
Optional.ofNullable(env.get(REGION_ENV_VARIABLE)).ifPresent(region -> loadedProps.put(ScannerProperties.SONAR_REGION, region));
5860
env.forEach((key, value) -> {
5961
if (!key.equals(SONAR_SCANNER_JSON_PARAMS) && key.startsWith(GENERIC_ENV_PREFIX)) {
6062
processEnvVariable(key, value, loadedProps);

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

Lines changed: 15 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@
3737
import org.sonarsource.scanner.lib.internal.MessageException;
3838
import org.sonarsource.scanner.lib.internal.SuccessfulBootstrap;
3939
import org.sonarsource.scanner.lib.internal.cache.FileCache;
40+
import org.sonarsource.scanner.lib.internal.endpoint.ScannerEndpoint;
41+
import org.sonarsource.scanner.lib.internal.endpoint.ScannerEndpointResolver;
4042
import org.sonarsource.scanner.lib.internal.facade.forked.NewScannerEngineFacade;
4143
import org.sonarsource.scanner.lib.internal.facade.forked.ScannerEngineLauncherFactory;
4244
import org.sonarsource.scanner.lib.internal.facade.inprocess.InProcessScannerEngineFacade;
@@ -77,8 +79,7 @@ public class ScannerEngineBootstrapper {
7779

7880
private static final Logger LOG = LoggerFactory.getLogger(ScannerEngineBootstrapper.class);
7981

80-
private static final String SONARCLOUD_HOST = "https://sonarcloud.io";
81-
private static final String SONARCLOUD_REST_API = "https://api.sonarcloud.io";
82+
8283
static final String SQ_VERSION_NEW_BOOTSTRAPPING = "10.6";
8384
static final String SQ_VERSION_TOKEN_AUTHENTICATION = "10.0";
8485

@@ -125,37 +126,38 @@ public ScannerEngineBootstrapResult bootstrap() {
125126
if (LOG.isDebugEnabled()) {
126127
LOG.debug("Scanner max available memory: {}", FileUtils.byteCountToDisplaySize(Runtime.getRuntime().maxMemory()));
127128
}
128-
initBootstrapDefaultValues();
129+
var endpoint = ScannerEndpointResolver.resolveEndpoint(bootstrapProperties);
130+
initBootstrapDefaultValues(endpoint);
129131
var immutableProperties = Map.copyOf(bootstrapProperties);
130132
var sonarUserHome = resolveSonarUserHome(immutableProperties);
131133
var httpConfig = new HttpConfig(immutableProperties, sonarUserHome, system);
132-
var isSonarCloud = isSonarCloud(immutableProperties);
134+
var isSonarQubeCloud = endpoint.isSonarQubeCloud();
133135
var isSimulation = immutableProperties.containsKey(InternalProperties.SCANNER_DUMP_TO_FILE);
134136
var fileCache = FileCache.create(sonarUserHome);
135137

136138
if (isSimulation) {
137139
var serverVersion = immutableProperties.getOrDefault(InternalProperties.SCANNER_VERSION_SIMULATION, "9.9");
138-
return new SuccessfulBootstrap(new SimulationScannerEngineFacade(immutableProperties, isSonarCloud, serverVersion));
140+
return new SuccessfulBootstrap(new SimulationScannerEngineFacade(immutableProperties, isSonarQubeCloud, serverVersion));
139141
}
140142

141143
// No HTTP call should be made before this point
142144
try {
143145
scannerHttpClient.init(httpConfig);
144146

145-
var serverVersion = !isSonarCloud ? getServerVersion(scannerHttpClient) : null;
147+
var serverVersion = !isSonarQubeCloud ? getServerVersion(scannerHttpClient) : null;
146148

147-
if (!isSonarCloud && VersionUtils.isAtLeastIgnoringQualifier(serverVersion, SQ_VERSION_TOKEN_AUTHENTICATION) && Objects.nonNull(httpConfig.getLogin())) {
149+
if (!isSonarQubeCloud && VersionUtils.isAtLeastIgnoringQualifier(serverVersion, SQ_VERSION_TOKEN_AUTHENTICATION) && Objects.nonNull(httpConfig.getLogin())) {
148150
LOG.warn("Use of '{}' property has been deprecated in favor of '{}' (or the env variable alternative '{}'). Please use the latter when passing a token.", SONAR_LOGIN,
149151
SONAR_TOKEN, TOKEN_ENV_VARIABLE);
150152
}
151153

152154
ScannerEngineFacade scannerFacade;
153-
if (isSonarCloud || VersionUtils.isAtLeastIgnoringQualifier(serverVersion, SQ_VERSION_NEW_BOOTSTRAPPING)) {
155+
if (isSonarQubeCloud || VersionUtils.isAtLeastIgnoringQualifier(serverVersion, SQ_VERSION_NEW_BOOTSTRAPPING)) {
154156
var launcher = scannerEngineLauncherFactory.createLauncher(scannerHttpClient, fileCache, immutableProperties);
155157

156158
var adaptedProperties = adaptSslPropertiesToScannerProperties(immutableProperties, httpConfig);
157159

158-
scannerFacade = new NewScannerEngineFacade(adaptedProperties, launcher, isSonarCloud, serverVersion);
160+
scannerFacade = new NewScannerEngineFacade(adaptedProperties, launcher, isSonarQubeCloud, serverVersion);
159161
} else {
160162
var launcher = launcherFactory.createLauncher(scannerHttpClient, fileCache);
161163
var adaptedProperties = adaptDeprecatedPropertiesForInProcessBootstrapping(immutableProperties, httpConfig);
@@ -201,7 +203,7 @@ private static void logWithStacktraceOnlyIfDebug(String message, Throwable t) {
201203
}
202204

203205
private static void logServerType(ScannerEngineFacade engine) {
204-
if (engine.isSonarCloud()) {
206+
if (engine.isSonarQubeCloud()) {
205207
LOG.info("Communicating with SonarQube Cloud");
206208
} else {
207209
LOG.info("Communicating with {} {}", engine.getServerLabel(), engine.getServerVersion());
@@ -295,10 +297,9 @@ private static String formatMessage(Exception e) {
295297
return e.getMessage();
296298
}
297299

298-
private void initBootstrapDefaultValues() {
299-
setBootstrapPropertyIfNotAlreadySet(ScannerProperties.HOST_URL, getSonarCloudUrl());
300-
setBootstrapPropertyIfNotAlreadySet(ScannerProperties.API_BASE_URL,
301-
isSonarCloud(bootstrapProperties) ? SONARCLOUD_REST_API : (StringUtils.removeEnd(bootstrapProperties.get(ScannerProperties.HOST_URL), "/") + "/api/v2"));
300+
private void initBootstrapDefaultValues(ScannerEndpoint endpoint) {
301+
setBootstrapPropertyIfNotAlreadySet(ScannerProperties.HOST_URL, endpoint.getWebEndpoint());
302+
setBootstrapPropertyIfNotAlreadySet(ScannerProperties.API_BASE_URL, endpoint.getApiEndpoint());
302303
if (!bootstrapProperties.containsKey(SCANNER_OS)) {
303304
setBootstrapProperty(SCANNER_OS, new OsResolver(system, new Paths2()).getOs().name().toLowerCase(Locale.ENGLISH));
304305
}
@@ -328,14 +329,6 @@ static Map<String, String> adaptSslPropertiesToScannerProperties(Map<String, Str
328329
return Map.copyOf(result);
329330
}
330331

331-
private String getSonarCloudUrl() {
332-
return bootstrapProperties.getOrDefault(ScannerProperties.SONARCLOUD_URL, SONARCLOUD_HOST);
333-
}
334-
335-
private boolean isSonarCloud(Map<String, String> properties) {
336-
return getSonarCloudUrl().equals(properties.get(ScannerProperties.HOST_URL));
337-
}
338-
339332
private void setBootstrapPropertyIfNotAlreadySet(String key, @Nullable String value) {
340333
if (!bootstrapProperties.containsKey(key) && value != null) {
341334
setBootstrapProperty(key, value);

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ public interface ScannerEngineFacade extends AutoCloseable {
3939
/**
4040
* @return true if the scanner is connected to SonarQube Cloud, false otherwise
4141
*/
42-
boolean isSonarCloud();
42+
boolean isSonarQubeCloud();
4343

4444

4545
/**

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

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ private ScannerProperties() {
3333
}
3434

3535
/**
36-
* URL of the Sonar server, default to SonarCloud
36+
* URL of the SonarQube Server, default to SonarQube Cloud if not set
3737
*/
3838
public static final String HOST_URL = "sonar.host.url";
3939

@@ -43,9 +43,14 @@ private ScannerProperties() {
4343
public static final String API_BASE_URL = "sonar.scanner.apiBaseUrl";
4444

4545
/**
46-
* URL of the SonarCloud server, default to https://sonarcloud.io. Useful for testing purposes.
46+
* URL of the SonarQube Cloud instance, default to https://sonarcloud.io. Useful for testing purposes.
4747
*/
48-
public static final String SONARCLOUD_URL = "sonar.scanner.sonarcloudUrl";
48+
public static final String SONARQUBE_CLOUD_URL = "sonar.scanner.sonarcloudUrl";
49+
50+
/**
51+
* SonarQube Cloud region.
52+
*/
53+
public static final String SONAR_REGION = "sonar.region";
4954

5055
/**
5156
* Working directory containing generated reports and temporary data.
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/*
2+
* SonarScanner Java Library
3+
* Copyright (C) 2011-2025 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.endpoint;
21+
22+
import java.util.Arrays;
23+
import java.util.Locale;
24+
import java.util.Optional;
25+
import java.util.Set;
26+
import java.util.stream.Collectors;
27+
import javax.annotation.Nullable;
28+
import org.apache.commons.lang3.StringUtils;
29+
30+
public enum OfficialSonarQubeCloudInstance {
31+
GLOBAL("https://sonarcloud.io", "https://api.sonarcloud.io"),
32+
US("https://sonarqube.us", "https://api.sonarqube.us");
33+
34+
35+
private final ScannerEndpoint endpoint;
36+
37+
OfficialSonarQubeCloudInstance(String webEndpoint, String apiEndpoint) {
38+
this.endpoint = new ScannerEndpoint(webEndpoint, apiEndpoint, true);
39+
}
40+
41+
public static Set<String> getRegionCodes() {
42+
return Arrays.stream(OfficialSonarQubeCloudInstance.values()).filter(r -> r != GLOBAL).map(Enum::name).map(s -> s.toLowerCase(Locale.ENGLISH)).collect(Collectors.toSet());
43+
}
44+
45+
public static Optional<OfficialSonarQubeCloudInstance> fromRegionCode(@Nullable String regionCode) {
46+
if (StringUtils.isBlank(regionCode)) {
47+
return Optional.of(GLOBAL);
48+
}
49+
try {
50+
return Optional.of(OfficialSonarQubeCloudInstance.valueOf(regionCode.toUpperCase(Locale.ENGLISH)));
51+
} catch (IllegalArgumentException e) {
52+
return Optional.empty();
53+
}
54+
}
55+
56+
public static Optional<OfficialSonarQubeCloudInstance> fromWebEndpoint(String url) {
57+
return Arrays.stream(OfficialSonarQubeCloudInstance.values())
58+
.filter(r -> r.endpoint.getWebEndpoint().equals(url))
59+
.findFirst();
60+
}
61+
62+
public ScannerEndpoint getEndpoint() {
63+
return endpoint;
64+
}
65+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
* SonarScanner Java Library
3+
* Copyright (C) 2011-2025 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.endpoint;
21+
22+
public class ScannerEndpoint {
23+
24+
private final String webEndpoint;
25+
private final String apiEndpoint;
26+
private final boolean isSonarQubeCloud;
27+
28+
public ScannerEndpoint(String webEndpoint, String apiEndpoint, boolean isSonarQubeCloud) {
29+
this.webEndpoint = webEndpoint;
30+
this.apiEndpoint = apiEndpoint;
31+
this.isSonarQubeCloud = isSonarQubeCloud;
32+
}
33+
34+
public boolean isSonarQubeCloud() {
35+
return isSonarQubeCloud;
36+
}
37+
38+
public String getApiEndpoint() {
39+
return apiEndpoint;
40+
}
41+
42+
public String getWebEndpoint() {
43+
return webEndpoint;
44+
}
45+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/*
2+
* SonarScanner Java Library
3+
* Copyright (C) 2011-2025 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.endpoint;
21+
22+
import java.util.Map;
23+
import java.util.Optional;
24+
import org.apache.commons.lang3.StringUtils;
25+
import org.sonarsource.scanner.lib.EnvironmentConfig;
26+
import org.sonarsource.scanner.lib.ScannerProperties;
27+
import org.sonarsource.scanner.lib.internal.MessageException;
28+
29+
import static java.lang.String.format;
30+
import static java.util.stream.Collectors.toList;
31+
32+
public class ScannerEndpointResolver {
33+
34+
private ScannerEndpointResolver() {
35+
}
36+
37+
public static ScannerEndpoint resolveEndpoint(Map<String, String> properties) {
38+
if (properties.containsKey(ScannerProperties.HOST_URL)) {
39+
return resolveEndpointFromSonarHostUrl(properties);
40+
}
41+
return resolveSonarQubeCloudEndpoint(properties);
42+
}
43+
44+
private static ScannerEndpoint resolveSonarQubeCloudEndpoint(Map<String, String> properties) {
45+
var hasCloudUrl = properties.containsKey(ScannerProperties.SONARQUBE_CLOUD_URL);
46+
if (hasCloudUrl) {
47+
return resolveCustomSonarQubeCloudEndpoint(properties);
48+
}
49+
failIfApiEndpointAloneDefined(properties);
50+
var regionCode = properties.get(ScannerProperties.SONAR_REGION);
51+
return OfficialSonarQubeCloudInstance.fromRegionCode(regionCode)
52+
.orElseThrow(() -> new MessageException(
53+
format("Invalid region '%s'. Valid regions are: %s. Please check the '%s' property or the '%s' environment variable.",
54+
regionCode, StringUtils.join(OfficialSonarQubeCloudInstance.getRegionCodes().stream().map(r -> "'" + r + "'").collect(toList()), ", "),
55+
ScannerProperties.SONAR_REGION, EnvironmentConfig.REGION_ENV_VARIABLE)))
56+
.getEndpoint();
57+
}
58+
59+
private static void failIfApiEndpointAloneDefined(Map<String, String> properties) {
60+
var hasApiUrl = properties.containsKey(ScannerProperties.API_BASE_URL);
61+
if (hasApiUrl) {
62+
throw new MessageException(format("Defining '%s' without '%s' is not supported.", ScannerProperties.API_BASE_URL, ScannerProperties.SONARQUBE_CLOUD_URL));
63+
}
64+
}
65+
66+
private static ScannerEndpoint resolveCustomSonarQubeCloudEndpoint(Map<String, String> properties) {
67+
var hasApiUrl = properties.containsKey(ScannerProperties.API_BASE_URL);
68+
var maybeCloudInstance = maybeResolveOfficialSonarQubeCloud(properties, ScannerProperties.SONARQUBE_CLOUD_URL);
69+
if (maybeCloudInstance.isPresent()) {
70+
return maybeCloudInstance.get();
71+
}
72+
if (!hasApiUrl) {
73+
throw new MessageException(format("Defining a custom '%s' without providing '%s' is not supported.", ScannerProperties.SONARQUBE_CLOUD_URL, ScannerProperties.API_BASE_URL));
74+
}
75+
return new ScannerEndpoint(
76+
properties.get(ScannerProperties.SONARQUBE_CLOUD_URL),
77+
properties.get(ScannerProperties.API_BASE_URL), true);
78+
}
79+
80+
private static MessageException inconsistentUrlAndRegion(String prop2) {
81+
return new MessageException(format("Inconsistent values for properties '%s' and '%s'. Please only specify one of the two properties.", ScannerProperties.SONAR_REGION, prop2));
82+
}
83+
84+
private static ScannerEndpoint resolveEndpointFromSonarHostUrl(Map<String, String> properties) {
85+
return maybeResolveOfficialSonarQubeCloud(properties, ScannerProperties.HOST_URL)
86+
.orElse(new SonarQubeServer(properties.get(ScannerProperties.HOST_URL)));
87+
}
88+
89+
private static Optional<ScannerEndpoint> maybeResolveOfficialSonarQubeCloud(Map<String, String> properties, String urlPropName) {
90+
var maybeCloudInstance = OfficialSonarQubeCloudInstance.fromWebEndpoint(properties.get(urlPropName));
91+
var hasRegion = properties.containsKey(ScannerProperties.SONAR_REGION);
92+
if (maybeCloudInstance.isPresent()) {
93+
if (hasRegion && OfficialSonarQubeCloudInstance.fromRegionCode(properties.get(ScannerProperties.SONAR_REGION)).filter(maybeCloudInstance.get()::equals).isEmpty()) {
94+
throw inconsistentUrlAndRegion(urlPropName);
95+
}
96+
return Optional.of(maybeCloudInstance.get().getEndpoint());
97+
}
98+
if (hasRegion) {
99+
throw inconsistentUrlAndRegion(urlPropName);
100+
}
101+
return Optional.empty();
102+
}
103+
104+
}

0 commit comments

Comments
 (0)