From 95b2fa725d206945b131c4a327498169d43b50e7 Mon Sep 17 00:00:00 2001 From: Simon Brandhof Date: Mon, 6 Nov 2017 17:58:08 +0100 Subject: [PATCH] LICENSE-82 reset server ID on DB URL changes --- .../ComputeEngineContainerImplTest.java | 19 +- .../src/test/java/org/sonar/db/DbTester.java | 5 + .../sonar/server/platform/BackendCleanup.java | 3 +- .../server/platform/ServerIdChecksum.java | 99 +++++++++ .../server/platform/ServerIdManager.java | 92 ++++++-- .../server/property/InternalProperties.java | 1 + .../server/platform/ServerIdChecksumTest.java | 95 ++++++++ .../server/platform/ServerIdManagerTest.java | 202 ++++++++++++------ .../src/main/assembly/conf/sonar.properties | 6 +- 9 files changed, 436 insertions(+), 86 deletions(-) create mode 100644 server/sonar-server/src/main/java/org/sonar/server/platform/ServerIdChecksum.java create mode 100644 server/sonar-server/src/test/java/org/sonar/server/platform/ServerIdChecksumTest.java diff --git a/server/sonar-ce/src/test/java/org/sonar/ce/container/ComputeEngineContainerImplTest.java b/server/sonar-ce/src/test/java/org/sonar/ce/container/ComputeEngineContainerImplTest.java index e70437c795b..dbb3ee1eafe 100644 --- a/server/sonar-ce/src/test/java/org/sonar/ce/container/ComputeEngineContainerImplTest.java +++ b/server/sonar-ce/src/test/java/org/sonar/ce/container/ComputeEngineContainerImplTest.java @@ -24,7 +24,6 @@ import java.io.IOException; import java.util.Date; import java.util.Properties; import java.util.stream.Collectors; -import org.apache.commons.dbcp.BasicDataSource; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -42,6 +41,8 @@ import org.sonar.db.property.PropertyDto; import org.sonar.process.ProcessId; import org.sonar.process.ProcessProperties; import org.sonar.process.Props; +import org.sonar.server.platform.ServerIdChecksum; +import org.sonar.server.property.InternalProperties; import static java.lang.String.valueOf; import static org.assertj.core.api.Assertions.assertThat; @@ -59,7 +60,7 @@ public class ComputeEngineContainerImplTest { @Rule public TemporaryFolder tempFolder = new TemporaryFolder(); @Rule - public DbTester dbTester = DbTester.create(System2.INSTANCE); + public DbTester db = DbTester.create(System2.INSTANCE); private ComputeEngineContainerImpl underTest; @@ -79,8 +80,9 @@ public class ComputeEngineContainerImplTest { Properties properties = getProperties(); // required persisted properties - insertProperty(CoreProperties.SERVER_ID, "a_startup_id"); + insertProperty(CoreProperties.SERVER_ID, "a_server_id"); insertProperty(CoreProperties.SERVER_STARTTIME, DateUtils.formatDateTime(new Date())); + insertInternalProperty(InternalProperties.SERVER_ID_CHECKSUM, ServerIdChecksum.of("a_server_id", db.getUrl())); underTest .start(new Props(properties)); @@ -141,7 +143,7 @@ public class ComputeEngineContainerImplTest { properties.setProperty(PATH_TEMP, tmpDir.getAbsolutePath()); properties.setProperty(PROPERTY_PROCESS_INDEX, valueOf(ProcessId.COMPUTE_ENGINE.getIpcIndex())); properties.setProperty(PROPERTY_SHARED_PATH, tmpDir.getAbsolutePath()); - properties.setProperty(DatabaseProperties.PROP_URL, ((BasicDataSource) dbTester.database().getDataSource()).getUrl()); + properties.setProperty(DatabaseProperties.PROP_URL, db.getUrl()); properties.setProperty(DatabaseProperties.PROP_USER, "sonar"); properties.setProperty(DatabaseProperties.PROP_PASSWORD, "sonar"); return properties; @@ -149,7 +151,12 @@ public class ComputeEngineContainerImplTest { private void insertProperty(String key, String value) { PropertyDto dto = new PropertyDto().setKey(key).setValue(value); - dbTester.getDbClient().propertiesDao().saveProperty(dbTester.getSession(), dto); - dbTester.commit(); + db.getDbClient().propertiesDao().saveProperty(db.getSession(), dto); + db.commit(); + } + + private void insertInternalProperty(String key, String value) { + db.getDbClient().internalPropertiesDao().save(db.getSession(), key, value); + db.commit(); } } diff --git a/server/sonar-db-dao/src/test/java/org/sonar/db/DbTester.java b/server/sonar-db-dao/src/test/java/org/sonar/db/DbTester.java index 8122f1b0c60..9e9fe02e8f9 100644 --- a/server/sonar-db-dao/src/test/java/org/sonar/db/DbTester.java +++ b/server/sonar-db-dao/src/test/java/org/sonar/db/DbTester.java @@ -24,6 +24,7 @@ import java.sql.SQLException; import java.util.List; import java.util.Map; import javax.annotation.Nullable; +import org.apache.commons.dbcp.BasicDataSource; import org.apache.commons.lang.StringUtils; import org.picocontainer.containers.TransientPicoContainer; import org.sonar.api.utils.System2; @@ -300,6 +301,10 @@ public class DbTester extends AbstractDbTester { return db.getDatabase(); } + public String getUrl() { + return ((BasicDataSource) db.getDatabase().getDataSource()).getUrl(); + } + public DatabaseCommands getCommands() { return db.getCommands(); } diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/BackendCleanup.java b/server/sonar-server/src/main/java/org/sonar/server/platform/BackendCleanup.java index 9274f1e53bc..b28ec9b4132 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/platform/BackendCleanup.java +++ b/server/sonar-server/src/main/java/org/sonar/server/platform/BackendCleanup.java @@ -228,8 +228,9 @@ public class BackendCleanup { * Internal property {@link InternalProperties#DEFAULT_ORGANIZATION} must never be deleted. */ private static void truncateInternalProperties(String tableName, Statement ddlStatement, Connection connection) throws SQLException { - try (PreparedStatement preparedStatement = connection.prepareStatement("delete from internal_properties where kee <> ?")) { + try (PreparedStatement preparedStatement = connection.prepareStatement("delete from internal_properties where kee not in (?,?)")) { preparedStatement.setString(1, InternalProperties.DEFAULT_ORGANIZATION); + preparedStatement.setString(2, InternalProperties.SERVER_ID_CHECKSUM); preparedStatement.execute(); // commit is useless on some databases connection.commit(); diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/ServerIdChecksum.java b/server/sonar-server/src/main/java/org/sonar/server/platform/ServerIdChecksum.java new file mode 100644 index 00000000000..66ca4f448aa --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/platform/ServerIdChecksum.java @@ -0,0 +1,99 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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 org.sonar.server.platform; + +import com.google.common.annotations.VisibleForTesting; +import java.util.Arrays; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import org.apache.commons.codec.digest.DigestUtils; +import org.apache.commons.lang.StringUtils; +import org.sonar.api.utils.KeyValueFormat; + +public class ServerIdChecksum { + + private static final String SQLSERVER_PREFIX = "jdbc:sqlserver://"; + + private ServerIdChecksum() { + // only static methods + } + + public static String of(String serverId, String jdbcUrl) { + return DigestUtils.sha256Hex(serverId + "|" + sanitizeJdbcUrl(jdbcUrl)); + } + + @VisibleForTesting + static String sanitizeJdbcUrl(String jdbcUrl) { + String result; + if (jdbcUrl.startsWith(SQLSERVER_PREFIX)) { + result = sanitizeSqlServerUrl(jdbcUrl); + } else { + // remove query parameters, they don't aim to reference the schema + result = StringUtils.substringBefore(jdbcUrl, "?"); + } + return StringUtils.lowerCase(result, Locale.ENGLISH); + } + + /** + * Deal with this strange URL format: + * https://docs.microsoft.com/en-us/sql/connect/jdbc/building-the-connection-url + * https://docs.microsoft.com/en-us/sql/connect/jdbc/setting-the-connection-properties + */ + private static String sanitizeSqlServerUrl(String jdbcUrl) { + StringBuilder result = new StringBuilder(); + result.append(SQLSERVER_PREFIX); + + String host; + if (jdbcUrl.contains(";")) { + host = StringUtils.substringBetween(jdbcUrl, SQLSERVER_PREFIX, ";"); + } else { + host = StringUtils.substringAfter(jdbcUrl, SQLSERVER_PREFIX); + } + + String queryString = StringUtils.substringAfter(jdbcUrl, ";"); + Map parameters = KeyValueFormat.parse(queryString); + Optional server = firstValue(parameters, "serverName", "servername", "server"); + if (server.isPresent()) { + result.append(server.get()); + } else { + result.append(StringUtils.substringBefore(host, ":")); + } + + Optional port = firstValue(parameters, "portNumber", "port"); + if (port.isPresent()) { + result.append(':').append(port.get()); + } else if (host.contains(":")) { + result.append(':').append(StringUtils.substringAfter(host, ":")); + } + + Optional database = firstValue(parameters, "databaseName", "database"); + database.ifPresent(s -> result.append('/').append(s)); + return result.toString(); + } + + private static Optional firstValue(Map map, String... keys) { + return Arrays.stream(keys) + .map(map::get) + .filter(Objects::nonNull) + .findFirst(); + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/ServerIdManager.java b/server/sonar-server/src/main/java/org/sonar/server/platform/ServerIdManager.java index f211e84e6db..fe465c595b1 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/platform/ServerIdManager.java +++ b/server/sonar-server/src/main/java/org/sonar/server/platform/ServerIdManager.java @@ -21,27 +21,37 @@ package org.sonar.server.platform; import java.text.ParseException; import java.text.SimpleDateFormat; -import javax.annotation.Nullable; +import java.util.Optional; import org.picocontainer.Startable; import org.sonar.api.CoreProperties; import org.sonar.api.SonarQubeSide; import org.sonar.api.SonarRuntime; +import org.sonar.api.config.Configuration; +import org.sonar.api.utils.log.Logger; +import org.sonar.api.utils.log.Loggers; import org.sonar.core.util.UuidFactory; import org.sonar.db.DbClient; import org.sonar.db.DbSession; import org.sonar.db.property.PropertyDto; +import org.sonar.server.property.InternalProperties; import static com.google.common.base.Preconditions.checkState; -import static org.apache.commons.lang.StringUtils.isBlank; +import static org.apache.commons.lang.StringUtils.isEmpty; +import static org.apache.commons.lang.StringUtils.isNotEmpty; import static org.sonar.api.CoreProperties.SERVER_ID; +import static org.sonar.server.property.InternalProperties.SERVER_ID_CHECKSUM; public class ServerIdManager implements Startable { + private static final Logger LOGGER = Loggers.get(ServerIdManager.class); + + private final Configuration config; private final DbClient dbClient; private final SonarRuntime runtime; private final WebServer webServer; private final UuidFactory uuidFactory; - public ServerIdManager(DbClient dbClient, SonarRuntime runtime, WebServer webServer, UuidFactory uuidFactory) { + public ServerIdManager(Configuration config, DbClient dbClient, SonarRuntime runtime, WebServer webServer, UuidFactory uuidFactory) { + this.config = config; this.dbClient = dbClient; this.runtime = runtime; this.webServer = webServer; @@ -51,24 +61,62 @@ public class ServerIdManager implements Startable { @Override public void start() { try (DbSession dbSession = dbClient.openSession(false)) { - PropertyDto dto = dbClient.propertiesDao().selectGlobalProperty(dbSession, SERVER_ID); if (runtime.getSonarQubeSide() == SonarQubeSide.SERVER && webServer.isStartupLeader()) { - persistServerIdIfMissingOrOldFormatted(dbSession, dto); + if (needsToBeDropped(dbSession)) { + dbClient.propertiesDao().deleteGlobalProperty(SERVER_ID, dbSession); + } + persistServerIdIfMissing(dbSession); + dbSession.commit(); } else { - ensureServerIdIsSet(dto); + ensureServerIdIsValid(dbSession); } } } - /** - * Insert or update {@link CoreProperties#SERVER_ID} property in DB to a UUID if it doesn't exist or if it's a date - * (per the old format of {@link CoreProperties#SERVER_ID} before 6.1). - */ - private void persistServerIdIfMissingOrOldFormatted(DbSession dbSession, @Nullable PropertyDto dto) { - if (dto == null || dto.getValue().isEmpty() || isDate(dto.getValue())) { - dbClient.propertiesDao().saveProperty(dbSession, new PropertyDto().setKey(SERVER_ID).setValue(uuidFactory.create())); - dbSession.commit(); + private boolean needsToBeDropped(DbSession dbSession) { + PropertyDto dto = dbClient.propertiesDao().selectGlobalProperty(dbSession, SERVER_ID); + if (dto == null) { + // does not exist, no need to drop + return false; + } + + if (isEmpty(dto.getValue())) { + return true; + } + + if (isDate(dto.getValue())) { + LOGGER.info("Server ID is changed to new format."); + return true; + } + + Optional checksum = dbClient.internalPropertiesDao().selectByKey(dbSession, SERVER_ID_CHECKSUM); + if (checksum.isPresent()) { + String expectedChecksum = computeChecksum(dto.getValue()); + if (!expectedChecksum.equals(checksum.get())) { + LOGGER.warn("Server ID is reset because it is not valid anymore. Database URL probably changed. The new server ID affects SonarSource licensed products."); + return true; + } } + + // Existing server ID must be kept when upgrading to 6.7+. In that case the checksum does + // not exist. + + return false; + } + + private void persistServerIdIfMissing(DbSession dbSession) { + String serverId; + PropertyDto idDto = dbClient.propertiesDao().selectGlobalProperty(dbSession, SERVER_ID); + if (idDto == null) { + serverId = uuidFactory.create(); + dbClient.propertiesDao().saveProperty(dbSession, new PropertyDto().setKey(SERVER_ID).setValue(serverId)); + } else { + serverId = idDto.getValue(); + } + + // checksum must be generated when it does not exist (upgrading to 6.7 or greater) + // or when server ID changed. + dbClient.internalPropertiesDao().save(dbSession, InternalProperties.SERVER_ID_CHECKSUM, computeChecksum(serverId)); } /** @@ -83,9 +131,19 @@ public class ServerIdManager implements Startable { } } - private static void ensureServerIdIsSet(@Nullable PropertyDto dto) { - checkState(dto != null, "Property %s is missing in database", SERVER_ID); - checkState(!isBlank(dto.getValue()), "Property %s is set but empty in database", SERVER_ID); + private String computeChecksum(String serverId) { + String jdbcUrl = config.get("sonar.jdbc.url").orElseThrow(() -> new IllegalStateException("Missing JDBC URL")); + return ServerIdChecksum.of(serverId, jdbcUrl); + } + + private void ensureServerIdIsValid(DbSession dbSession) { + PropertyDto id = dbClient.propertiesDao().selectGlobalProperty(dbSession, SERVER_ID); + checkState(id != null, "Property %s is missing in database", SERVER_ID); + checkState(isNotEmpty(id.getValue()), "Property %s is empty in database", SERVER_ID); + + Optional checksum = dbClient.internalPropertiesDao().selectByKey(dbSession, SERVER_ID_CHECKSUM); + checkState(checksum.isPresent(), "Internal property %s is missing in database", SERVER_ID_CHECKSUM); + checkState(checksum.get().equals(computeChecksum(id.getValue())), "Server ID is invalid"); } @Override diff --git a/server/sonar-server/src/main/java/org/sonar/server/property/InternalProperties.java b/server/sonar-server/src/main/java/org/sonar/server/property/InternalProperties.java index 1d84ad9441a..7e40f06cda1 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/property/InternalProperties.java +++ b/server/sonar-server/src/main/java/org/sonar/server/property/InternalProperties.java @@ -34,6 +34,7 @@ public interface InternalProperties { String ORGANIZATION_ENABLED = "organization.enabled"; + String SERVER_ID_CHECKSUM = "server.idChecksum"; /** * Read the value of the specified property. * diff --git a/server/sonar-server/src/test/java/org/sonar/server/platform/ServerIdChecksumTest.java b/server/sonar-server/src/test/java/org/sonar/server/platform/ServerIdChecksumTest.java new file mode 100644 index 00000000000..dada79edc40 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/platform/ServerIdChecksumTest.java @@ -0,0 +1,95 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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 org.sonar.server.platform; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ServerIdChecksumTest { + + @Test + public void test_checksum() { + assertThat(ServerIdChecksum.of("id1", "url1")) + .isNotEmpty() + .isEqualTo(ServerIdChecksum.of("id1", "url1")) + .isNotEqualTo(ServerIdChecksum.of("id1", "url2")) + .isNotEqualTo(ServerIdChecksum.of("id2", "url1")) + .isNotEqualTo(ServerIdChecksum.of("id2", "url2")); + } + + @Test + public void sanitize_h2_url() { + verifyJdbcUrl("jdbc:h2:tcp://dbserv:8084/~/sample", "jdbc:h2:tcp://dbserv:8084/~/sample"); + verifyJdbcUrl("jdbc:h2:tcp://localhost/mem:test", "jdbc:h2:tcp://localhost/mem:test"); + verifyJdbcUrl("jdbc:h2:tcp://localhost/mem:TEST", "jdbc:h2:tcp://localhost/mem:test"); + } + + @Test + public void sanitize_mysql_url() { + verifyJdbcUrl("jdbc:mysql://127.0.0.1:3306/sonarqube?useUnicode=true&characterEncoding=utf8", "jdbc:mysql://127.0.0.1:3306/sonarqube"); + verifyJdbcUrl("jdbc:mysql://127.0.0.1:3306/sonarqube", "jdbc:mysql://127.0.0.1:3306/sonarqube"); + verifyJdbcUrl("jdbc:mysql://127.0.0.1:3306/SONARQUBE", "jdbc:mysql://127.0.0.1:3306/sonarqube"); + } + + @Test + public void sanitize_oracle_url() { + verifyJdbcUrl("sonar.jdbc.url=jdbc:oracle:thin:@localhost:1521/XE", "sonar.jdbc.url=jdbc:oracle:thin:@localhost:1521/xe"); + verifyJdbcUrl("sonar.jdbc.url=jdbc:oracle:thin:@localhost/XE", "sonar.jdbc.url=jdbc:oracle:thin:@localhost/xe"); + verifyJdbcUrl("sonar.jdbc.url=jdbc:oracle:thin:@localhost/XE?foo", "sonar.jdbc.url=jdbc:oracle:thin:@localhost/xe"); + verifyJdbcUrl("sonar.jdbc.url=jdbc:oracle:thin:@LOCALHOST/XE?foo", "sonar.jdbc.url=jdbc:oracle:thin:@localhost/xe"); + } + + @Test + public void sanitize_sqlserver_url() { + // see examples listed at https://docs.microsoft.com/en-us/sql/connect/jdbc/building-the-connection-url + verifyJdbcUrl("jdbc:sqlserver://localhost;user=MyUserName;password=*****;", "jdbc:sqlserver://localhost"); + verifyJdbcUrl("jdbc:sqlserver://;servername=server_name;integratedSecurity=true;authenticationScheme=JavaKerberos", "jdbc:sqlserver://server_name"); + verifyJdbcUrl("jdbc:sqlserver://localhost;integratedSecurity=true;", "jdbc:sqlserver://localhost"); + verifyJdbcUrl("jdbc:sqlserver://localhost;databaseName=AdventureWorks;integratedSecurity=true;", "jdbc:sqlserver://localhost/adventureworks"); + verifyJdbcUrl("jdbc:sqlserver://localhost:1433;databaseName=AdventureWorks;integratedSecurity=true;", "jdbc:sqlserver://localhost:1433/adventureworks"); + verifyJdbcUrl("jdbc:sqlserver://localhost;databaseName=AdventureWorks;integratedSecurity=true;applicationName=MyApp;", "jdbc:sqlserver://localhost/adventureworks"); + verifyJdbcUrl("jdbc:sqlserver://localhost;instanceName=instance1;integratedSecurity=true;", "jdbc:sqlserver://localhost"); + verifyJdbcUrl("jdbc:sqlserver://;serverName=3ffe:8311:eeee:f70f:0:5eae:10.203.31.9\\\\instance1;integratedSecurity=true;", "jdbc:sqlserver://3ffe:8311:eeee:f70f:0:5eae:10.203.31.9\\\\instance1"); + + // test parameter aliases + verifyJdbcUrl("jdbc:sqlserver://;server=server_name", "jdbc:sqlserver://server_name"); + verifyJdbcUrl("jdbc:sqlserver://;server=server_name;portNumber=1234", "jdbc:sqlserver://server_name:1234"); + verifyJdbcUrl("jdbc:sqlserver://;server=server_name;port=1234", "jdbc:sqlserver://server_name:1234"); + + // case-insensitive + verifyJdbcUrl("jdbc:sqlserver://LOCALHOST;user=MyUserName;password=*****;", "jdbc:sqlserver://localhost"); + + } + + @Test + public void sanitize_postgres_url() { + verifyJdbcUrl("jdbc:postgresql://localhost/sonar", "jdbc:postgresql://localhost/sonar"); + verifyJdbcUrl("jdbc:postgresql://localhost:1234/sonar", "jdbc:postgresql://localhost:1234/sonar"); + verifyJdbcUrl("jdbc:postgresql://localhost:1234/sonar?foo", "jdbc:postgresql://localhost:1234/sonar"); + + // case-insensitive + verifyJdbcUrl("jdbc:postgresql://localhost:1234/SONAR?foo", "jdbc:postgresql://localhost:1234/sonar"); + } + + private static void verifyJdbcUrl(String url, String expectedResult) { + assertThat(ServerIdChecksum.sanitizeJdbcUrl(url)).isEqualTo(expectedResult); + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/platform/ServerIdManagerTest.java b/server/sonar-server/src/test/java/org/sonar/server/platform/ServerIdManagerTest.java index f4138d89e5d..5c594a0dbb3 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/platform/ServerIdManagerTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/platform/ServerIdManagerTest.java @@ -19,12 +19,14 @@ */ package org.sonar.server.platform; +import org.junit.After; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; import org.sonar.api.CoreProperties; import org.sonar.api.SonarQubeSide; -import org.sonar.api.SonarRuntime; +import org.sonar.api.config.Configuration; +import org.sonar.api.config.internal.MapSettings; import org.sonar.api.internal.SonarRuntimeImpl; import org.sonar.api.utils.System2; import org.sonar.api.utils.Version; @@ -33,6 +35,7 @@ import org.sonar.db.DbClient; import org.sonar.db.DbSession; import org.sonar.db.DbTester; import org.sonar.db.property.PropertyDto; +import org.sonar.server.property.InternalProperties; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; @@ -41,136 +44,215 @@ import static org.sonar.api.SonarQubeSide.COMPUTE_ENGINE; import static org.sonar.api.SonarQubeSide.SERVER; public class ServerIdManagerTest { - private static final Version SOME_VERSION = Version.create(5, 6); - private static final String SOME_UUID = "some uuid"; + + private static final String A_SERVER_ID = "uuid"; + private static final String A_JDBC_URL = "jdbc:postgres:foo"; + private static final String A_VALID_CHECKSUM = ServerIdChecksum.of(A_SERVER_ID, A_JDBC_URL); @Rule public final DbTester dbTester = DbTester.create(System2.INSTANCE); @Rule public ExpectedException expectedException = ExpectedException.none(); + private Configuration config = new MapSettings().setProperty("sonar.jdbc.url", A_JDBC_URL).asConfig(); private DbClient dbClient = dbTester.getDbClient(); private DbSession dbSession = dbTester.getSession(); private WebServer webServer = mock(WebServer.class); private UuidFactory uuidFactory = mock(UuidFactory.class); + private ServerIdManager underTest; - private static SonarRuntime runtimeFor(SonarQubeSide side) { - return SonarRuntimeImpl.forSonarQube(SOME_VERSION, side); + @After + public void tearDown() { + if (underTest != null) { + underTest.stop(); + } } @Test - public void start_persists_new_serverId_if_server_startupLeader_and_serverId_does_not_exist() { - when(uuidFactory.create()).thenReturn(SOME_UUID); + public void web_leader_persists_new_server_id_if_missing() { + when(uuidFactory.create()).thenReturn(A_SERVER_ID); when(webServer.isStartupLeader()).thenReturn(true); - new ServerIdManager(dbClient, runtimeFor(SERVER), webServer, uuidFactory) - .start(); + test(SERVER); - assertThat(dbClient.propertiesDao().selectGlobalProperty(dbSession, CoreProperties.SERVER_ID)) - .extracting(PropertyDto::getValue) - .containsOnly(SOME_UUID); + verifyDb(A_SERVER_ID, A_VALID_CHECKSUM); } @Test - public void start_persists_new_serverId_if_server_startupLeader_and_serverId_is_an_old_date() { - insertPropertyCoreId("20161123150657"); - when(uuidFactory.create()).thenReturn(SOME_UUID); + public void web_leader_persists_new_server_id_if_format_is_old_date() { + insertServerId("20161123150657"); + when(uuidFactory.create()).thenReturn(A_SERVER_ID); when(webServer.isStartupLeader()).thenReturn(true); - new ServerIdManager(dbClient, runtimeFor(SERVER), webServer, uuidFactory) - .start(); + test(SERVER); - assertThat(dbClient.propertiesDao().selectGlobalProperty(dbSession, CoreProperties.SERVER_ID)) - .extracting(PropertyDto::getValue) - .containsOnly(SOME_UUID); + verifyDb(A_SERVER_ID, A_VALID_CHECKSUM); } - private void insertPropertyCoreId(String value) { - dbClient.propertiesDao().saveProperty(dbSession, new PropertyDto().setKey(CoreProperties.SERVER_ID).setValue(value)); - dbSession.commit(); + @Test + public void web_leader_persists_new_server_id_if_value_is_empty() { + insertServerId(""); + when(uuidFactory.create()).thenReturn(A_SERVER_ID); + when(webServer.isStartupLeader()).thenReturn(true); + + test(SERVER); + + verifyDb(A_SERVER_ID, A_VALID_CHECKSUM); } @Test - public void start_persists_new_serverId_if_server_startupLeader_and_serverId_is_empty() { - insertPropertyCoreId(""); - when(uuidFactory.create()).thenReturn(SOME_UUID); + public void web_leader_keeps_existing_server_id_if_valid() { + insertServerId(A_SERVER_ID); + insertChecksum(A_VALID_CHECKSUM); when(webServer.isStartupLeader()).thenReturn(true); - new ServerIdManager(dbClient, runtimeFor(SERVER), webServer, uuidFactory) - .start(); + test(SERVER); - assertThat(dbClient.propertiesDao().selectGlobalProperty(dbSession, CoreProperties.SERVER_ID)) - .extracting(PropertyDto::getValue) - .containsOnly(SOME_UUID); + verifyDb(A_SERVER_ID, A_VALID_CHECKSUM); + } + + @Test + public void web_leader_resets_server_id_if_invalid() { + insertServerId("foo"); + insertChecksum("invalid"); + when(uuidFactory.create()).thenReturn(A_SERVER_ID); + when(webServer.isStartupLeader()).thenReturn(true); + + test(SERVER); + + verifyDb(A_SERVER_ID, A_VALID_CHECKSUM); + } + + @Test + public void web_leader_generates_missing_checksum() { + insertServerId(A_SERVER_ID); + when(webServer.isStartupLeader()).thenReturn(true); + + test(SERVER); + + verifyDb(A_SERVER_ID, A_VALID_CHECKSUM); } @Test - public void start_fails_with_ISE_if_serverId_is_null_and_server_is_not_startupLeader() { + public void web_follower_does_not_fail_if_server_id_is_valid() { + insertServerId(A_SERVER_ID); + insertChecksum(A_VALID_CHECKSUM); when(webServer.isStartupLeader()).thenReturn(false); - ServerIdManager underTest = new ServerIdManager(dbClient, runtimeFor(SERVER), webServer, uuidFactory); + test(SERVER); - expectMissingCoreIdException(); - - underTest.start(); + // no changes + verifyDb(A_SERVER_ID, A_VALID_CHECKSUM); } @Test - public void start_fails_with_ISE_if_serverId_is_empty_and_server_is_not_startupLeader() { - insertPropertyCoreId(""); + public void web_follower_fails_if_server_id_is_missing() { when(webServer.isStartupLeader()).thenReturn(false); - ServerIdManager underTest = new ServerIdManager(dbClient, runtimeFor(SERVER), webServer, uuidFactory); + expectMissingServerIdException(); - expectEmptyCoreIdException(); + test(SERVER); + } - underTest.start(); + @Test + public void web_follower_fails_if_server_id_is_empty() { + insertServerId(""); + when(webServer.isStartupLeader()).thenReturn(false); + + expectEmptyServerIdException(); + + test(SERVER); } @Test - public void start_fails_with_ISE_if_serverId_is_null_and_not_server() { + public void web_follower_fails_if_server_id_is_invalid() { + insertServerId(A_SERVER_ID); + insertChecksum("boom"); when(webServer.isStartupLeader()).thenReturn(false); - ServerIdManager underTest = new ServerIdManager(dbClient, runtimeFor(COMPUTE_ENGINE), webServer, uuidFactory); + expectInvalidServerIdException(); - expectMissingCoreIdException(); + test(SERVER); - underTest.start(); + // no changes + verifyDb(A_SERVER_ID, "boom"); } @Test - public void start_fails_with_ISE_if_serverId_is_empty_and_not_server() { - insertPropertyCoreId(""); + public void compute_engine_does_not_fail_if_server_id_is_valid() { + insertServerId(A_SERVER_ID); + insertChecksum(A_VALID_CHECKSUM); - ServerIdManager underTest = new ServerIdManager(dbClient, runtimeFor(COMPUTE_ENGINE), webServer, uuidFactory); + test(COMPUTE_ENGINE); - expectEmptyCoreIdException(); + // no changes + verifyDb(A_SERVER_ID, A_VALID_CHECKSUM); + } - underTest.start(); + @Test + public void compute_engine_fails_if_server_id_is_missing() { + expectMissingServerIdException(); + + test(COMPUTE_ENGINE); } @Test - public void start_does_not_fail_if_serverId_exists_and_server_is_not_startupLeader() { - insertPropertyCoreId(SOME_UUID); - when(webServer.isStartupLeader()).thenReturn(false); + public void compute_engine_fails_if_server_id_is_empty() { + insertServerId(""); + + expectEmptyServerIdException(); - new ServerIdManager(dbClient, runtimeFor(SERVER), webServer, uuidFactory).start(); + test(COMPUTE_ENGINE); } @Test - public void start_does_not_fail_if_serverId_exists_and_not_server() { - insertPropertyCoreId(SOME_UUID); + public void compute_engine_fails_if_server_id_is_invalid() { + insertServerId(A_SERVER_ID); + insertChecksum("boom"); + + expectInvalidServerIdException(); + + test(COMPUTE_ENGINE); - new ServerIdManager(dbClient, runtimeFor(COMPUTE_ENGINE), webServer, uuidFactory).start(); + // no changes + verifyDb(A_SERVER_ID, "boom"); } - private void expectEmptyCoreIdException() { + private void expectEmptyServerIdException() { expectedException.expect(IllegalStateException.class); - expectedException.expectMessage("Property sonar.core.id is set but empty in database"); + expectedException.expectMessage("Property sonar.core.id is empty in database"); } - private void expectMissingCoreIdException() { + private void expectMissingServerIdException() { expectedException.expect(IllegalStateException.class); expectedException.expectMessage("Property sonar.core.id is missing in database"); } + + private void expectInvalidServerIdException() { + expectedException.expect(IllegalStateException.class); + expectedException.expectMessage("Server ID is invalid"); + } + + private void verifyDb(String expectedServerId, String expectedChecksum) { + assertThat(dbClient.propertiesDao().selectGlobalProperty(dbSession, CoreProperties.SERVER_ID)) + .extracting(PropertyDto::getValue) + .containsExactly(expectedServerId); + assertThat(dbClient.internalPropertiesDao().selectByKey(dbSession, InternalProperties.SERVER_ID_CHECKSUM)) + .hasValue(expectedChecksum); + } + + private void insertServerId(String value) { + dbClient.propertiesDao().saveProperty(dbSession, new PropertyDto().setKey(CoreProperties.SERVER_ID).setValue(value)); + dbSession.commit(); + } + + private void insertChecksum(String value) { + dbClient.internalPropertiesDao().save(dbSession, InternalProperties.SERVER_ID_CHECKSUM, value); + dbSession.commit(); + } + + private void test(SonarQubeSide side) { + underTest = new ServerIdManager(config, dbClient, SonarRuntimeImpl.forSonarQube(Version.create(6, 7), side), webServer, uuidFactory); + underTest.start(); + } } diff --git a/sonar-application/src/main/assembly/conf/sonar.properties b/sonar-application/src/main/assembly/conf/sonar.properties index c0b96b43810..2ef061a637f 100644 --- a/sonar-application/src/main/assembly/conf/sonar.properties +++ b/sonar-application/src/main/assembly/conf/sonar.properties @@ -5,8 +5,10 @@ #-------------------------------------------------------------------------------------------------- # DATABASE # -# IMPORTANT: the embedded H2 database is used by default. It is recommended for tests but not for -# production use. Supported databases are MySQL, Oracle, PostgreSQL and Microsoft SQLServer. +# IMPORTANT: +# - The embedded H2 database is used by default. It is recommended for tests but not for +# production use. Supported databases are MySQL, Oracle, PostgreSQL and Microsoft SQLServer. +# - Changes to database connection URL (sonar.jdbc.url) can affect SonarSource licensed products. # User credentials. # Permissions to create tables, indices and triggers must be granted to JDBC user. -- 2.39.5