From: Sébastien Lesaint Date: Wed, 20 Jun 2018 15:15:44 +0000 (+0200) Subject: LICENSE-96 implement support for staging and new server id format X-Git-Tag: 7.5~863 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=45f6d410d36e999607e306dcf4374d739d58677b;p=sonarqube.git LICENSE-96 implement support for staging and new server id format --- diff --git a/server/sonar-ce/src/main/java/org/sonar/ce/container/ComputeEngineContainerImpl.java b/server/sonar-ce/src/main/java/org/sonar/ce/container/ComputeEngineContainerImpl.java index 0d4bb529195..2b1519862ba 100644 --- a/server/sonar-ce/src/main/java/org/sonar/ce/container/ComputeEngineContainerImpl.java +++ b/server/sonar-ce/src/main/java/org/sonar/ce/container/ComputeEngineContainerImpl.java @@ -132,7 +132,6 @@ import org.sonar.server.permission.ws.template.DefaultTemplatesResolverImpl; import org.sonar.server.platform.DatabaseServerCompatibility; import org.sonar.server.platform.DefaultServerUpgradeStatus; import org.sonar.server.platform.ServerFileSystemImpl; -import org.sonar.server.platform.ServerIdManager; import org.sonar.server.platform.ServerImpl; import org.sonar.server.platform.ServerLifecycleNotifier; import org.sonar.server.log.ServerLogging; @@ -145,6 +144,7 @@ import org.sonar.server.platform.db.migration.version.DatabaseVersion; import org.sonar.server.platform.monitoring.DbSection; import org.sonar.server.platform.monitoring.OfficialDistribution; import org.sonar.server.platform.monitoring.cluster.ProcessInfoProvider; +import org.sonar.server.platform.serverid.ServerIdModule; import org.sonar.server.plugins.InstalledPluginReferentialFactory; import org.sonar.server.plugins.ServerExtensionInstaller; import org.sonar.server.property.InternalPropertiesImpl; @@ -360,7 +360,7 @@ public class ComputeEngineContainerImpl implements ComputeEngineContainer { private static void populateLevel3(ComponentContainer container) { container.add( new StartupMetadataProvider(), - ServerIdManager.class, + ServerIdModule.class, UriReader.class, ServerImpl.class, DefaultOrganizationProviderImpl.class, 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 d86e30af7fc..ddeaafc1cdd 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 @@ -22,8 +22,11 @@ package org.sonar.ce.container; import java.io.File; import java.io.IOException; import java.util.Date; +import java.util.Locale; import java.util.Properties; import java.util.stream.Collectors; +import org.apache.commons.codec.digest.DigestUtils; +import org.apache.commons.lang.StringUtils; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -40,7 +43,6 @@ 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; @@ -84,7 +86,7 @@ public class ComputeEngineContainerImplTest { // required persisted properties 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())); + insertInternalProperty(InternalProperties.SERVER_ID_CHECKSUM, DigestUtils.sha256Hex("a_server_id|" + cleanJdbcUrl())); underTest .start(new Props(properties)); @@ -110,6 +112,7 @@ public class ComputeEngineContainerImplTest { assertThat(picoContainer.getParent().getComponentAdapters()).hasSize( CONTAINER_ITSELF + 7 // level 3 + + 4 // content of ServerIdModule ); assertThat(picoContainer.getParent().getParent().getComponentAdapters()).hasSize( CONTAINER_ITSELF @@ -140,6 +143,10 @@ public class ComputeEngineContainerImplTest { assertThat(picoContainer.getLifecycleState().isDisposed()).isTrue(); } + private String cleanJdbcUrl() { + return StringUtils.lowerCase(StringUtils.substringBefore(db.getUrl(), "?"), Locale.ENGLISH); + } + private Properties getProperties() throws IOException { Properties properties = ProcessProperties.defaults(); File homeDir = tempFolder.newFolder(); 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 deleted file mode 100644 index 1e4e7e7c283..00000000000 --- a/server/sonar-server/src/main/java/org/sonar/server/platform/ServerIdChecksum.java +++ /dev/null @@ -1,99 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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 deleted file mode 100644 index 11bd09b77dc..00000000000 --- a/server/sonar-server/src/main/java/org/sonar/server/platform/ServerIdManager.java +++ /dev/null @@ -1,154 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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 java.text.ParseException; -import java.text.SimpleDateFormat; -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.isEmpty; -import static org.apache.commons.lang.StringUtils.isNotEmpty; -import static org.sonar.api.CoreProperties.SERVER_ID; -import static org.sonar.process.ProcessProperties.Property.JDBC_URL; -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(Configuration config, DbClient dbClient, SonarRuntime runtime, WebServer webServer, UuidFactory uuidFactory) { - this.config = config; - this.dbClient = dbClient; - this.runtime = runtime; - this.webServer = webServer; - this.uuidFactory = uuidFactory; - } - - @Override - public void start() { - try (DbSession dbSession = dbClient.openSession(false)) { - if (runtime.getSonarQubeSide() == SonarQubeSide.SERVER && webServer.isStartupLeader()) { - if (needsToBeDropped(dbSession)) { - dbClient.propertiesDao().deleteGlobalProperty(SERVER_ID, dbSession); - } - persistServerIdIfMissing(dbSession); - dbSession.commit(); - } else { - ensureServerIdIsValid(dbSession); - } - } - } - - 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)); - } - - /** - * Checks whether the specified value is a date according to the old format of the {@link CoreProperties#SERVER_ID}. - */ - private static boolean isDate(String value) { - try { - new SimpleDateFormat("yyyyMMddHHmmss").parse(value); - return true; - } catch (ParseException e) { - return false; - } - } - - private String computeChecksum(String serverId) { - String jdbcUrl = config.get(JDBC_URL.getKey()).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 - public void stop() { - // nothing to do - } -} diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel3.java b/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel3.java index 27e8550d0a3..30376168e21 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel3.java +++ b/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel3.java @@ -26,11 +26,11 @@ import org.sonar.core.util.DefaultHttpDownloader; import org.sonar.server.async.AsyncExecutionModule; import org.sonar.server.organization.DefaultOrganizationProviderImpl; import org.sonar.server.organization.OrganizationFlagsImpl; -import org.sonar.server.platform.ServerIdManager; import org.sonar.server.platform.ServerImpl; import org.sonar.server.platform.StartupMetadataPersister; import org.sonar.server.platform.WebCoreExtensionsInstaller; import org.sonar.server.platform.db.migration.NoopDatabaseMigrationImpl; +import org.sonar.server.platform.serverid.ServerIdModule; import org.sonar.server.setting.DatabaseSettingLoader; import org.sonar.server.setting.DatabaseSettingsEnabler; @@ -47,7 +47,7 @@ public class PlatformLevel3 extends PlatformLevel { addIfStartupLeader(StartupMetadataPersister.class); add( NoopDatabaseMigrationImpl.class, - ServerIdManager.class, + ServerIdModule.class, ServerImpl.class, DatabaseSettingLoader.class, DatabaseSettingsEnabler.class, diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/serverid/JdbcUrlSanitizer.java b/server/sonar-server/src/main/java/org/sonar/server/platform/serverid/JdbcUrlSanitizer.java new file mode 100644 index 00000000000..05c3ba816b8 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/platform/serverid/JdbcUrlSanitizer.java @@ -0,0 +1,87 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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.serverid; + +import java.util.Arrays; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import org.apache.commons.lang.StringUtils; +import org.sonar.api.utils.KeyValueFormat; + +public class JdbcUrlSanitizer { + private static final String SQLSERVER_PREFIX = "jdbc:sqlserver://"; + + public String sanitize(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/serverid/ServerIdChecksum.java b/server/sonar-server/src/main/java/org/sonar/server/platform/serverid/ServerIdChecksum.java new file mode 100644 index 00000000000..57cd9e30cf9 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/platform/serverid/ServerIdChecksum.java @@ -0,0 +1,42 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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.serverid; + +import org.apache.commons.codec.digest.DigestUtils; +import org.sonar.api.config.Configuration; + +import static org.sonar.process.ProcessProperties.Property.JDBC_URL; + +public class ServerIdChecksum { + + private final Configuration config; + private final JdbcUrlSanitizer jdbcUrlSanitizer; + + public ServerIdChecksum(Configuration config, JdbcUrlSanitizer jdbcUrlSanitizer) { + this.config = config; + this.jdbcUrlSanitizer = jdbcUrlSanitizer; + } + + public String computeFor(String serverId) { + String jdbcUrl = config.get(JDBC_URL.getKey()).orElseThrow(() -> new IllegalStateException("Missing JDBC URL")); + return DigestUtils.sha256Hex(serverId + "|" + jdbcUrlSanitizer.sanitize(jdbcUrl)); + } + +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/serverid/ServerIdFactory.java b/server/sonar-server/src/main/java/org/sonar/server/platform/serverid/ServerIdFactory.java new file mode 100644 index 00000000000..ab044181397 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/platform/serverid/ServerIdFactory.java @@ -0,0 +1,34 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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.serverid; + +import org.sonar.core.platform.ServerId; + +public interface ServerIdFactory { + /** + * Create a new ServerId from scratch. + */ + ServerId create(); + + /** + * Create a new ServerId from the current serverId. + */ + ServerId create(ServerId currentServerId); +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/serverid/ServerIdFactoryImpl.java b/server/sonar-server/src/main/java/org/sonar/server/platform/serverid/ServerIdFactoryImpl.java new file mode 100644 index 00000000000..a0898d9c74b --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/platform/serverid/ServerIdFactoryImpl.java @@ -0,0 +1,69 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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.serverid; + +import com.google.common.annotations.VisibleForTesting; +import java.nio.charset.StandardCharsets; +import java.util.Locale; +import java.util.zip.CRC32; +import org.apache.commons.lang.StringUtils; +import org.sonar.api.config.Configuration; +import org.sonar.core.platform.ServerId; +import org.sonar.core.util.UuidFactory; + +import static org.sonar.process.ProcessProperties.Property.JDBC_URL; + +public class ServerIdFactoryImpl implements ServerIdFactory { + + private final Configuration config; + private final UuidFactory uuidFactory; + private final JdbcUrlSanitizer jdbcUrlSanitizer; + + public ServerIdFactoryImpl(Configuration config, UuidFactory uuidFactory, JdbcUrlSanitizer jdbcUrlSanitizer) { + this.config = config; + this.uuidFactory = uuidFactory; + this.jdbcUrlSanitizer = jdbcUrlSanitizer; + } + + @Override + public ServerId create() { + return ServerId.of(computeDatabaseId(), uuidFactory.create()); + } + + @Override + public ServerId create(ServerId currentServerId) { + return ServerId.of(computeDatabaseId(), currentServerId.getDatasetId()); + } + + private String computeDatabaseId() { + String jdbcUrl = config.get(JDBC_URL.getKey()).orElseThrow(() -> new IllegalStateException("Missing JDBC URL")); + return crc32Hex(jdbcUrlSanitizer.sanitize(jdbcUrl)); + } + + @VisibleForTesting + static String crc32Hex(String str) { + CRC32 crc32 = new CRC32(); + crc32.update(str.getBytes(StandardCharsets.UTF_8)); + long hash = crc32.getValue(); + String s = Long.toHexString(hash).toUpperCase(Locale.ENGLISH); + return StringUtils.leftPad(s, ServerId.DATABASE_ID_LENGTH, "0"); + } + +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/serverid/ServerIdManager.java b/server/sonar-server/src/main/java/org/sonar/server/platform/serverid/ServerIdManager.java new file mode 100644 index 00000000000..7d2d3bb2caa --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/platform/serverid/ServerIdManager.java @@ -0,0 +1,163 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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.serverid; + +import java.util.Optional; +import org.picocontainer.Startable; +import org.sonar.api.SonarQubeSide; +import org.sonar.api.SonarRuntime; +import org.sonar.api.utils.log.Logger; +import org.sonar.api.utils.log.Loggers; +import org.sonar.core.platform.ServerId; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.property.PropertyDto; +import org.sonar.server.platform.WebServer; +import org.sonar.server.property.InternalProperties; + +import static com.google.common.base.Preconditions.checkState; +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.core.platform.ServerId.Format.DEPRECATED; +import static org.sonar.core.platform.ServerId.Format.NO_DATABASE_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 ServerIdChecksum serverIdChecksum; + private final ServerIdFactory serverIdFactory; + private final DbClient dbClient; + private final SonarRuntime runtime; + private final WebServer webServer; + + public ServerIdManager(ServerIdChecksum serverIdChecksum, ServerIdFactory serverIdFactory, DbClient dbClient, SonarRuntime runtime, WebServer webServer) { + this.serverIdChecksum = serverIdChecksum; + this.serverIdFactory = serverIdFactory; + this.dbClient = dbClient; + this.runtime = runtime; + this.webServer = webServer; + } + + @Override + public void start() { + try (DbSession dbSession = dbClient.openSession(false)) { + if (runtime.getSonarQubeSide() == SonarQubeSide.SERVER && webServer.isStartupLeader()) { + Optional checksum = dbClient.internalPropertiesDao().selectByKey(dbSession, SERVER_ID_CHECKSUM); + + ServerId serverId = readCurrentServerId(dbSession) + .map(currentServerId -> keepOrReplaceCurrentServerId(dbSession, currentServerId, checksum)) + .orElseGet(() -> createFirstServerId(dbSession)); + updateChecksum(dbSession, serverId); + + dbSession.commit(); + } else { + ensureServerIdIsValid(dbSession); + } + } + } + + private ServerId keepOrReplaceCurrentServerId(DbSession dbSession, ServerId currentServerId, Optional checksum) { + if (keepServerId(currentServerId, checksum)) { + return currentServerId; + } + + ServerId serverId = replaceCurrentServerId(currentServerId); + persistServerId(dbSession, serverId); + return serverId; + } + + private boolean keepServerId(ServerId serverId, Optional checksum) { + ServerId.Format format = serverId.getFormat(); + if (format == DEPRECATED || format == NO_DATABASE_ID) { + LOGGER.info("Server ID is changed to new format."); + return false; + } + + if (checksum.isPresent()) { + String expectedChecksum = serverIdChecksum.computeFor(serverId.toString()); + 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 false; + } + } + + // Existing server ID must be kept when upgrading to 6.7+. In that case the checksum does not exist. + return true; + } + + private ServerId replaceCurrentServerId(ServerId currentServerId) { + if (currentServerId.getFormat() == DEPRECATED) { + return serverIdFactory.create(); + } + return serverIdFactory.create(currentServerId); + } + + private ServerId createFirstServerId(DbSession dbSession) { + ServerId serverId = serverIdFactory.create(); + persistServerId(dbSession, serverId); + return serverId; + } + + private Optional readCurrentServerId(DbSession dbSession) { + PropertyDto dto = dbClient.propertiesDao().selectGlobalProperty(dbSession, SERVER_ID); + if (dto == null) { + return Optional.empty(); + } + + String value = dto.getValue(); + if (isEmpty(value)) { + return Optional.empty(); + } + + return Optional.of(ServerId.parse(value)); + } + + private void updateChecksum(DbSession dbSession, ServerId serverId) { + // checksum must be generated when it does not exist (upgrading to 6.7 or greater) + // or when server ID changed. + String checksum = serverIdChecksum.computeFor(serverId.toString()); + persistChecksum(dbSession, checksum); + } + + private void persistServerId(DbSession dbSession, ServerId serverId) { + dbClient.propertiesDao().saveProperty(dbSession, new PropertyDto().setKey(SERVER_ID).setValue(serverId.toString())); + } + + private void persistChecksum(DbSession dbSession, String checksump) { + dbClient.internalPropertiesDao().save(dbSession, InternalProperties.SERVER_ID_CHECKSUM, checksump); + } + + 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(serverIdChecksum.computeFor(id.getValue())), "Server ID is invalid"); + } + + @Override + public void stop() { + // nothing to do + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/serverid/ServerIdModule.java b/server/sonar-server/src/main/java/org/sonar/server/platform/serverid/ServerIdModule.java new file mode 100644 index 00000000000..3ef21171216 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/platform/serverid/ServerIdModule.java @@ -0,0 +1,35 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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.serverid; + +import org.sonar.core.platform.Module; + +public class ServerIdModule extends Module { + @Override + protected void configureModule() { + add( + ServerIdFactoryImpl.class, + JdbcUrlSanitizer.class, + ServerIdChecksum.class, + ServerIdManager.class + + ); + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/serverid/package-info.java b/server/sonar-server/src/main/java/org/sonar/server/platform/serverid/package-info.java new file mode 100644 index 00000000000..767e13e7270 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/platform/serverid/package-info.java @@ -0,0 +1,23 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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. + */ +@ParametersAreNonnullByDefault +package org.sonar.server.platform.serverid; + +import javax.annotation.ParametersAreNonnullByDefault; 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 deleted file mode 100644 index 7bfae035f90..00000000000 --- a/server/sonar-server/src/test/java/org/sonar/server/platform/ServerIdChecksumTest.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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 deleted file mode 100644 index 412d8019121..00000000000 --- a/server/sonar-server/src/test/java/org/sonar/server/platform/ServerIdManagerTest.java +++ /dev/null @@ -1,258 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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.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.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; -import org.sonar.core.util.UuidFactory; -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; -import static org.mockito.Mockito.when; -import static org.sonar.api.SonarQubeSide.COMPUTE_ENGINE; -import static org.sonar.api.SonarQubeSide.SERVER; - -public class ServerIdManagerTest { - - 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; - - @After - public void tearDown() { - if (underTest != null) { - underTest.stop(); - } - } - - @Test - public void web_leader_persists_new_server_id_if_missing() { - 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_persists_new_server_id_if_format_is_old_date() { - insertServerId("20161123150657"); - 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_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 web_leader_keeps_existing_server_id_if_valid() { - insertServerId(A_SERVER_ID); - insertChecksum(A_VALID_CHECKSUM); - when(webServer.isStartupLeader()).thenReturn(true); - - test(SERVER); - - 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 web_follower_does_not_fail_if_server_id_is_valid() { - insertServerId(A_SERVER_ID); - insertChecksum(A_VALID_CHECKSUM); - when(webServer.isStartupLeader()).thenReturn(false); - - test(SERVER); - - // no changes - verifyDb(A_SERVER_ID, A_VALID_CHECKSUM); - } - - @Test - public void web_follower_fails_if_server_id_is_missing() { - when(webServer.isStartupLeader()).thenReturn(false); - - expectMissingServerIdException(); - - test(SERVER); - } - - @Test - public void web_follower_fails_if_server_id_is_empty() { - insertServerId(""); - when(webServer.isStartupLeader()).thenReturn(false); - - expectEmptyServerIdException(); - - test(SERVER); - } - - @Test - public void web_follower_fails_if_server_id_is_invalid() { - insertServerId(A_SERVER_ID); - insertChecksum("boom"); - when(webServer.isStartupLeader()).thenReturn(false); - - expectInvalidServerIdException(); - - test(SERVER); - - // no changes - verifyDb(A_SERVER_ID, "boom"); - } - - @Test - public void compute_engine_does_not_fail_if_server_id_is_valid() { - insertServerId(A_SERVER_ID); - insertChecksum(A_VALID_CHECKSUM); - - test(COMPUTE_ENGINE); - - // no changes - verifyDb(A_SERVER_ID, A_VALID_CHECKSUM); - } - - @Test - public void compute_engine_fails_if_server_id_is_missing() { - expectMissingServerIdException(); - - test(COMPUTE_ENGINE); - } - - @Test - public void compute_engine_fails_if_server_id_is_empty() { - insertServerId(""); - - expectEmptyServerIdException(); - - test(COMPUTE_ENGINE); - } - - @Test - public void compute_engine_fails_if_server_id_is_invalid() { - insertServerId(A_SERVER_ID); - insertChecksum("boom"); - - expectInvalidServerIdException(); - - test(COMPUTE_ENGINE); - - // no changes - verifyDb(A_SERVER_ID, "boom"); - } - - private void expectEmptyServerIdException() { - expectedException.expect(IllegalStateException.class); - expectedException.expectMessage("Property sonar.core.id is empty in database"); - } - - 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/server/sonar-server/src/test/java/org/sonar/server/platform/serverid/JdbcUrlSanitizerTest.java b/server/sonar-server/src/test/java/org/sonar/server/platform/serverid/JdbcUrlSanitizerTest.java new file mode 100644 index 00000000000..6c0f642276f --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/platform/serverid/JdbcUrlSanitizerTest.java @@ -0,0 +1,88 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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.serverid; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class JdbcUrlSanitizerTest { + private JdbcUrlSanitizer underTest = new JdbcUrlSanitizer(); + + @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 void verifyJdbcUrl(String url, String expectedResult) { + assertThat(underTest.sanitize(url)).isEqualTo(expectedResult); + } + + +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/platform/serverid/ServerIdChecksumTest.java b/server/sonar-server/src/test/java/org/sonar/server/platform/serverid/ServerIdChecksumTest.java new file mode 100644 index 00000000000..b97cd8aa2af --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/platform/serverid/ServerIdChecksumTest.java @@ -0,0 +1,64 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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.serverid; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.sonar.api.config.internal.MapSettings; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.sonar.process.ProcessProperties.Property.JDBC_URL; + +public class ServerIdChecksumTest { + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + @Test + public void compute_throws_ISE_if_jdbcUrl_property_is_not_set() { + ServerIdChecksum underTest = new ServerIdChecksum(new MapSettings().asConfig(), null /*doesn't matter*/); + + expectedException.expect(IllegalStateException.class); + expectedException.expectMessage("Missing JDBC URL"); + + underTest.computeFor("foo"); + } + + @Test + public void test_checksum() { + assertThat(computeFor("id1", "url1")) + .isNotEmpty() + .isEqualTo(computeFor("id1", "url1")) + .isNotEqualTo(computeFor("id1", "url2")) + .isNotEqualTo(computeFor("id2", "url1")) + .isNotEqualTo(computeFor("id2", "url2")); + } + + private String computeFor(String serverId, String jdbcUrl) { + MapSettings settings = new MapSettings(); + JdbcUrlSanitizer jdbcUrlSanitizer = mock(JdbcUrlSanitizer.class); + when(jdbcUrlSanitizer.sanitize(jdbcUrl)).thenReturn("_" + jdbcUrl); + ServerIdChecksum underTest = new ServerIdChecksum(settings.asConfig(), jdbcUrlSanitizer); + settings.setProperty(JDBC_URL.getKey(), jdbcUrl); + return underTest.computeFor(serverId); + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/platform/serverid/ServerIdFactoryImplTest.java b/server/sonar-server/src/test/java/org/sonar/server/platform/serverid/ServerIdFactoryImplTest.java new file mode 100644 index 00000000000..fdeee077e9b --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/platform/serverid/ServerIdFactoryImplTest.java @@ -0,0 +1,119 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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.serverid; + +import com.tngtech.java.junit.dataprovider.DataProvider; +import com.tngtech.java.junit.dataprovider.DataProviderRunner; +import com.tngtech.java.junit.dataprovider.UseDataProvider; +import java.text.SimpleDateFormat; +import java.util.Date; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.sonar.api.config.Configuration; +import org.sonar.api.config.internal.MapSettings; +import org.sonar.core.platform.ServerId; +import org.sonar.core.util.UuidFactory; +import org.sonar.core.util.Uuids; + +import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.sonar.core.platform.ServerId.DATABASE_ID_LENGTH; +import static org.sonar.core.platform.ServerId.NOT_UUID_DATASET_ID_LENGTH; +import static org.sonar.core.platform.ServerId.UUID_DATASET_ID_LENGTH; +import static org.sonar.process.ProcessProperties.Property.JDBC_URL; +import static org.sonar.server.platform.serverid.ServerIdFactoryImpl.crc32Hex; + +@RunWith(DataProviderRunner.class) +public class ServerIdFactoryImplTest { + private static final ServerId A_SERVERID = ServerId.of(randomAlphabetic(DATABASE_ID_LENGTH), randomAlphabetic(UUID_DATASET_ID_LENGTH)); + + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + private MapSettings settings = new MapSettings(); + private Configuration config = settings.asConfig(); + private UuidFactory uuidFactory = mock(UuidFactory.class); + private JdbcUrlSanitizer jdbcUrlSanitizer = mock(JdbcUrlSanitizer.class); + private ServerIdFactoryImpl underTest = new ServerIdFactoryImpl(config, uuidFactory, jdbcUrlSanitizer); + + @Test + public void create_from_scratch_fails_with_ISE_if_JDBC_property_not_set() { + expectMissingJdbcUrlISE(); + + underTest.create(); + } + + @Test + public void create_from_scratch_creates_ServerId_from_JDBC_URL_and_new_uuid() { + String jdbcUrl = "jdbc"; + String uuid = Uuids.create(); + String sanitizedJdbcUrl = "sanitized_jdbc"; + settings.setProperty(JDBC_URL.getKey(), jdbcUrl); + when(uuidFactory.create()).thenReturn(uuid); + when(jdbcUrlSanitizer.sanitize(jdbcUrl)).thenReturn(sanitizedJdbcUrl); + + ServerId serverId = underTest.create(); + + assertThat(serverId.getDatabaseId().get()).isEqualTo(crc32Hex(sanitizedJdbcUrl)); + assertThat(serverId.getDatasetId()).isEqualTo(uuid); + } + + @Test + public void create_from_ServerId_fails_with_ISE_if_JDBC_property_not_set() { + expectMissingJdbcUrlISE(); + + underTest.create(A_SERVERID); + } + + @Test + @UseDataProvider("anyFormatServerId") + public void create_from_ServerId_creates_ServerId_from_JDBC_URL_and_serverId_datasetId(ServerId currentServerId) { + String jdbcUrl = "jdbc"; + String sanitizedJdbcUrl = "sanitized_jdbc"; + settings.setProperty(JDBC_URL.getKey(), jdbcUrl); + when(uuidFactory.create()).thenThrow(new IllegalStateException("UuidFactory.create() should not be called")); + when(jdbcUrlSanitizer.sanitize(jdbcUrl)).thenReturn(sanitizedJdbcUrl); + + ServerId serverId = underTest.create(currentServerId); + + assertThat(serverId.getDatabaseId().get()).isEqualTo(crc32Hex(sanitizedJdbcUrl)); + assertThat(serverId.getDatasetId()).isEqualTo(currentServerId.getDatasetId()); + } + + @DataProvider + public static Object[][] anyFormatServerId() { + return new Object[][] { + {ServerId.parse(new SimpleDateFormat("yyyyMMddHHmmss").format(new Date()))}, + {ServerId.parse(randomAlphabetic(NOT_UUID_DATASET_ID_LENGTH))}, + {ServerId.parse(randomAlphabetic(UUID_DATASET_ID_LENGTH))}, + {ServerId.of(randomAlphabetic(DATABASE_ID_LENGTH), randomAlphabetic(NOT_UUID_DATASET_ID_LENGTH))}, + {ServerId.of(randomAlphabetic(DATABASE_ID_LENGTH), randomAlphabetic(UUID_DATASET_ID_LENGTH))} + }; + } + + private void expectMissingJdbcUrlISE() { + expectedException.expect(IllegalStateException.class); + expectedException.expectMessage("Missing JDBC URL"); + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/platform/serverid/ServerIdManagerTest.java b/server/sonar-server/src/test/java/org/sonar/server/platform/serverid/ServerIdManagerTest.java new file mode 100644 index 00000000000..88bd2409d6e --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/platform/serverid/ServerIdManagerTest.java @@ -0,0 +1,361 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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.serverid; + +import com.tngtech.java.junit.dataprovider.DataProvider; +import com.tngtech.java.junit.dataprovider.DataProviderRunner; +import com.tngtech.java.junit.dataprovider.UseDataProvider; +import org.junit.After; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.sonar.api.CoreProperties; +import org.sonar.api.SonarQubeSide; +import org.sonar.api.internal.SonarRuntimeImpl; +import org.sonar.api.utils.System2; +import org.sonar.api.utils.Version; +import org.sonar.core.platform.ServerId; +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.platform.WebServer; +import org.sonar.server.property.InternalProperties; + +import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.sonar.api.SonarQubeSide.COMPUTE_ENGINE; +import static org.sonar.api.SonarQubeSide.SERVER; +import static org.sonar.core.platform.ServerId.DATABASE_ID_LENGTH; +import static org.sonar.core.platform.ServerId.NOT_UUID_DATASET_ID_LENGTH; +import static org.sonar.core.platform.ServerId.UUID_DATASET_ID_LENGTH; + +@RunWith(DataProviderRunner.class) +public class ServerIdManagerTest { + + private static final ServerId OLD_FORMAT_SERVER_ID = ServerId.parse("20161123150657"); + private static final ServerId NO_DATABASE_ID_SERVER_ID = ServerId.parse(randomAlphanumeric(UUID_DATASET_ID_LENGTH)); + private static final ServerId WITH_DATABASE_ID_SERVER_ID = ServerId.of(randomAlphanumeric(DATABASE_ID_LENGTH), randomAlphanumeric(NOT_UUID_DATASET_ID_LENGTH)); + private static final String CHECKSUM_1 = randomAlphanumeric(12); + + @Rule + public final DbTester dbTester = DbTester.create(System2.INSTANCE); + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + private ServerIdChecksum serverIdChecksum = mock(ServerIdChecksum.class); + private ServerIdFactory serverIdFactory = mock(ServerIdFactory.class); + private DbClient dbClient = dbTester.getDbClient(); + private DbSession dbSession = dbTester.getSession(); + private WebServer webServer = mock(WebServer.class); + private ServerIdManager underTest; + + @After + public void tearDown() { + if (underTest != null) { + underTest.stop(); + } + } + + @Test + public void web_leader_persists_new_server_id_if_missing() { + mockCreateNewServerId(WITH_DATABASE_ID_SERVER_ID); + mockChecksumOf(WITH_DATABASE_ID_SERVER_ID, CHECKSUM_1); + when(webServer.isStartupLeader()).thenReturn(true); + + test(SERVER); + + verifyDb(WITH_DATABASE_ID_SERVER_ID, CHECKSUM_1); + verifyCreateNewServerIdFromScratch(); + } + + @Test + public void web_leader_persists_new_server_id_if_format_is_old_date() { + insertServerId(OLD_FORMAT_SERVER_ID); + mockCreateNewServerId(WITH_DATABASE_ID_SERVER_ID); + mockChecksumOf(WITH_DATABASE_ID_SERVER_ID, CHECKSUM_1); + when(webServer.isStartupLeader()).thenReturn(true); + + test(SERVER); + + verifyDb(WITH_DATABASE_ID_SERVER_ID, CHECKSUM_1); + verifyCreateNewServerIdFromScratch(); + } + + @Test + public void web_leader_persists_new_server_id_if_value_is_empty() { + insertServerId(""); + mockCreateNewServerId(WITH_DATABASE_ID_SERVER_ID); + mockChecksumOf(WITH_DATABASE_ID_SERVER_ID, CHECKSUM_1); + when(webServer.isStartupLeader()).thenReturn(true); + + test(SERVER); + + verifyDb(WITH_DATABASE_ID_SERVER_ID, CHECKSUM_1); + verifyCreateNewServerIdFromScratch(); + } + + @Test + public void web_leader_keeps_existing_server_id_if_valid() { + insertServerId(WITH_DATABASE_ID_SERVER_ID); + insertChecksum(CHECKSUM_1); + mockChecksumOf(WITH_DATABASE_ID_SERVER_ID, CHECKSUM_1); + when(webServer.isStartupLeader()).thenReturn(true); + + test(SERVER); + + verifyDb(WITH_DATABASE_ID_SERVER_ID, CHECKSUM_1); + } + + @Test + public void web_leader_creates_server_id_from_scratch_if_checksum_fails_for_serverId_in_deprecated_format() { + ServerId currentServerId = OLD_FORMAT_SERVER_ID; + insertServerId(currentServerId); + insertChecksum("invalid"); + mockChecksumOf(currentServerId, "valid"); + mockCreateNewServerId(WITH_DATABASE_ID_SERVER_ID); + mockChecksumOf(WITH_DATABASE_ID_SERVER_ID, CHECKSUM_1); + when(webServer.isStartupLeader()).thenReturn(true); + + test(SERVER); + + verifyDb(WITH_DATABASE_ID_SERVER_ID, CHECKSUM_1); + verifyCreateNewServerIdFromScratch(); + } + + @Test + public void web_leader_creates_server_id_from_current_serverId_without_databaseId_if_checksum_fails() { + ServerId currentServerId = ServerId.parse(randomAlphanumeric(UUID_DATASET_ID_LENGTH)); + insertServerId(currentServerId); + insertChecksum("does_not_match_WITH_DATABASE_ID_SERVER_ID"); + mockChecksumOf(currentServerId, "matches_WITH_DATABASE_ID_SERVER_ID"); + mockCreateNewServerIdFrom(currentServerId, WITH_DATABASE_ID_SERVER_ID); + mockChecksumOf(WITH_DATABASE_ID_SERVER_ID, CHECKSUM_1); + when(webServer.isStartupLeader()).thenReturn(true); + + test(SERVER); + + verifyDb(WITH_DATABASE_ID_SERVER_ID, CHECKSUM_1); + verifyCreateNewServerIdFrom(currentServerId); + } + + @Test + public void web_leader_creates_server_id_from_current_serverId_with_databaseId_if_checksum_fails() { + ServerId currentServerId = ServerId.of(randomAlphanumeric(DATABASE_ID_LENGTH), randomAlphanumeric(UUID_DATASET_ID_LENGTH)); + insertServerId(currentServerId); + insertChecksum("does_not_match_WITH_DATABASE_ID_SERVER_ID"); + mockChecksumOf(currentServerId, "matches_WITH_DATABASE_ID_SERVER_ID"); + mockCreateNewServerIdFrom(currentServerId, WITH_DATABASE_ID_SERVER_ID); + mockChecksumOf(WITH_DATABASE_ID_SERVER_ID, CHECKSUM_1); + when(webServer.isStartupLeader()).thenReturn(true); + + test(SERVER); + + verifyDb(WITH_DATABASE_ID_SERVER_ID, CHECKSUM_1); + verifyCreateNewServerIdFrom(currentServerId); + } + + @Test + public void web_leader_generates_missing_checksum_for_current_serverId_with_databaseId() { + insertServerId(WITH_DATABASE_ID_SERVER_ID); + mockChecksumOf(WITH_DATABASE_ID_SERVER_ID, CHECKSUM_1); + when(webServer.isStartupLeader()).thenReturn(true); + + test(SERVER); + + verifyDb(WITH_DATABASE_ID_SERVER_ID, CHECKSUM_1); + } + + @Test + @UseDataProvider("allFormatsOfServerId") + public void web_follower_does_not_fail_if_server_id_matches_checksum(ServerId serverId) { + insertServerId(serverId); + insertChecksum(CHECKSUM_1); + mockChecksumOf(serverId, CHECKSUM_1); + when(webServer.isStartupLeader()).thenReturn(false); + + test(SERVER); + + // no changes + verifyDb(serverId, CHECKSUM_1); + } + + @Test + public void web_follower_fails_if_server_id_is_missing() { + when(webServer.isStartupLeader()).thenReturn(false); + + expectMissingServerIdException(); + + test(SERVER); + } + + @Test + public void web_follower_fails_if_server_id_is_empty() { + insertServerId(""); + when(webServer.isStartupLeader()).thenReturn(false); + + expectEmptyServerIdException(); + + test(SERVER); + } + + @Test + @UseDataProvider("allFormatsOfServerId") + public void web_follower_fails_if_checksum_does_not_match(ServerId serverId) { + String dbChecksum = "boom"; + insertServerId(serverId); + insertChecksum(dbChecksum); + mockChecksumOf(serverId, CHECKSUM_1); + when(webServer.isStartupLeader()).thenReturn(false); + + try { + test(SERVER); + fail("An ISE should have been raised"); + } + catch (IllegalStateException e) { + assertThat(e.getMessage()).isEqualTo("Server ID is invalid"); + // no changes + verifyDb(serverId, dbChecksum); + } + } + + @Test + @UseDataProvider("allFormatsOfServerId") + public void compute_engine_does_not_fail_if_server_id_is_valid(ServerId serverId) { + insertServerId(serverId); + insertChecksum(CHECKSUM_1); + mockChecksumOf(serverId, CHECKSUM_1); + + test(COMPUTE_ENGINE); + + // no changes + verifyDb(serverId, CHECKSUM_1); + } + + @Test + public void compute_engine_fails_if_server_id_is_missing() { + expectMissingServerIdException(); + + test(COMPUTE_ENGINE); + } + + @Test + public void compute_engine_fails_if_server_id_is_empty() { + insertServerId(""); + + expectEmptyServerIdException(); + + test(COMPUTE_ENGINE); + } + + @Test + @UseDataProvider("allFormatsOfServerId") + public void compute_engine_fails_if_server_id_is_invalid(ServerId serverId) { + String dbChecksum = "boom"; + insertServerId(serverId); + insertChecksum(dbChecksum); + mockChecksumOf(serverId, CHECKSUM_1); + + try { + test(SERVER); + fail("An ISE should have been raised"); + } + catch (IllegalStateException e) { + assertThat(e.getMessage()).isEqualTo("Server ID is invalid"); + // no changes + verifyDb(serverId, dbChecksum); + } + } + + @DataProvider + public static Object[][] allFormatsOfServerId() { + return new Object[][] { + {OLD_FORMAT_SERVER_ID}, + {NO_DATABASE_ID_SERVER_ID}, + {WITH_DATABASE_ID_SERVER_ID} + }; + } + + private void expectEmptyServerIdException() { + expectedException.expect(IllegalStateException.class); + expectedException.expectMessage("Property sonar.core.id is empty in database"); + } + + private void expectMissingServerIdException() { + expectedException.expect(IllegalStateException.class); + expectedException.expectMessage("Property sonar.core.id is missing in database"); + } + + private void verifyDb(ServerId expectedServerId, String expectedChecksum) { + assertThat(dbClient.propertiesDao().selectGlobalProperty(dbSession, CoreProperties.SERVER_ID)) + .extracting(PropertyDto::getValue) + .containsExactly(expectedServerId.toString()); + assertThat(dbClient.internalPropertiesDao().selectByKey(dbSession, InternalProperties.SERVER_ID_CHECKSUM)) + .hasValue(expectedChecksum); + } + + private void mockCreateNewServerId(ServerId newServerId) { + when(serverIdFactory.create()).thenReturn(newServerId); + when(serverIdFactory.create(any())).thenThrow(new IllegalStateException("new ServerId should not be created from current server id")); + } + + private void mockCreateNewServerIdFrom(ServerId currentServerId, ServerId newServerId) { + when(serverIdFactory.create()).thenThrow(new IllegalStateException("new ServerId should be created from current server id")); + when(serverIdFactory.create(eq(currentServerId))).thenReturn(newServerId); + } + + private void verifyCreateNewServerIdFromScratch() { + verify(serverIdFactory).create(); + } + + private void verifyCreateNewServerIdFrom(ServerId currentServerId) { + verify(serverIdFactory).create(currentServerId); + } + + private void mockChecksumOf(ServerId serverId, String checksum1) { + when(serverIdChecksum.computeFor(serverId.toString())).thenReturn(checksum1); + } + + private void insertServerId(ServerId serverId) { + insertServerId(serverId.toString()); + } + + private void insertServerId(String serverId) { + dbClient.propertiesDao().saveProperty(dbSession, new PropertyDto().setKey(CoreProperties.SERVER_ID).setValue(serverId.toString())); + 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(serverIdChecksum, serverIdFactory, dbClient, SonarRuntimeImpl.forSonarQube(Version.create(6, 7), side), webServer); + underTest.start(); + } +} diff --git a/sonar-core/src/main/java/org/sonar/core/platform/ServerId.java b/sonar-core/src/main/java/org/sonar/core/platform/ServerId.java new file mode 100644 index 00000000000..b30ee8130e8 --- /dev/null +++ b/sonar-core/src/main/java/org/sonar/core/platform/ServerId.java @@ -0,0 +1,164 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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.core.platform; + +import com.google.common.collect.ImmutableSet; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; +import org.sonar.api.CoreProperties; +import org.sonar.core.util.UuidFactory; + +import static com.google.common.base.Preconditions.checkArgument; +import static org.sonar.core.platform.ServerId.Format.DEPRECATED; +import static org.sonar.core.platform.ServerId.Format.NO_DATABASE_ID; +import static org.sonar.core.platform.ServerId.Format.WITH_DATABASE_ID; + +@Immutable +public final class ServerId { + + public static final char SPLIT_CHARACTER = '-'; + public static final int DATABASE_ID_LENGTH = 8; + public static final int DEPRECATED_SERVER_ID_LENGTH = 14; + public static final int NOT_UUID_DATASET_ID_LENGTH = 15; + public static final int UUID_DATASET_ID_LENGTH = 20; + private static final Set ALLOWED_LENGTHS = ImmutableSet.of( + DEPRECATED_SERVER_ID_LENGTH, + NOT_UUID_DATASET_ID_LENGTH, + NOT_UUID_DATASET_ID_LENGTH + 1 + DATABASE_ID_LENGTH, + UUID_DATASET_ID_LENGTH, + UUID_DATASET_ID_LENGTH + 1 + DATABASE_ID_LENGTH); + + public enum Format { + /* server id format before 6.1 (see SONAR-6992) */ + DEPRECATED, + /* server id format before 6.7.5 and 7.3 (see LICENSE-96) */ + NO_DATABASE_ID, + WITH_DATABASE_ID + } + + private final String databaseId; + private final String datasetId; + private final Format format; + + private ServerId(@Nullable String databaseId, String datasetId) { + this.databaseId = databaseId; + this.datasetId = datasetId; + this.format = computeFormat(databaseId, datasetId); + } + + public Optional getDatabaseId() { + return Optional.ofNullable(databaseId); + } + + public String getDatasetId() { + return datasetId; + } + + public Format getFormat() { + return format; + } + + private static Format computeFormat(@Nullable String databaseId, String datasetId) { + if (databaseId != null) { + return WITH_DATABASE_ID; + } + if (isDate(datasetId)) { + return DEPRECATED; + } + return NO_DATABASE_ID; + } + + public static ServerId parse(String serverId) { + String trimmed = serverId.trim(); + + int length = trimmed.length(); + checkArgument(length > 0, "serverId can't be empty"); + checkArgument(ALLOWED_LENGTHS.contains(length), "serverId does not have a supported length"); + if (length == DEPRECATED_SERVER_ID_LENGTH || length == UUID_DATASET_ID_LENGTH || length == NOT_UUID_DATASET_ID_LENGTH) { + return new ServerId(null, trimmed); + } + + int splitCharIndex = trimmed.indexOf(SPLIT_CHARACTER); + if (splitCharIndex == -1) { + return new ServerId(null, trimmed); + } + checkArgument(splitCharIndex == DATABASE_ID_LENGTH, "Unrecognized serverId format. Parts have wrong length"); + return of(trimmed.substring(0, splitCharIndex), trimmed.substring(splitCharIndex + 1)); + } + + public static ServerId of(@Nullable String databaseId, String datasetId) { + if (databaseId != null) { + int databaseIdLength = databaseId.length(); + checkArgument(databaseIdLength == DATABASE_ID_LENGTH, "Illegal databaseId length (%s)", databaseIdLength); + } + int datasetIdLength = datasetId.length(); + checkArgument(datasetIdLength == DEPRECATED_SERVER_ID_LENGTH + || datasetIdLength == NOT_UUID_DATASET_ID_LENGTH + || datasetIdLength == UUID_DATASET_ID_LENGTH, "Illegal datasetId length (%s)", datasetIdLength); + return new ServerId(databaseId, datasetId); + } + + public static ServerId create(UuidFactory uuidFactory) { + return new ServerId(null, uuidFactory.create()); + } + + /** + * Checks whether the specified value is a date according to the old format of the {@link CoreProperties#SERVER_ID}. + */ + private static boolean isDate(String value) { + try { + new SimpleDateFormat("yyyyMMddHHmmss").parse(value); + return true; + } catch (ParseException e) { + return false; + } + } + + @Override + public String toString() { + if (databaseId == null) { + return datasetId; + } + return databaseId + SPLIT_CHARACTER + datasetId; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ServerId serverId = (ServerId) o; + return Objects.equals(databaseId, serverId.databaseId) && + Objects.equals(datasetId, serverId.datasetId); + } + + @Override + public int hashCode() { + return Objects.hash(databaseId, datasetId); + } +} diff --git a/sonar-core/src/test/java/org/sonar/core/platform/ServerIdTest.java b/sonar-core/src/test/java/org/sonar/core/platform/ServerIdTest.java new file mode 100644 index 00000000000..f6f15d22bab --- /dev/null +++ b/sonar-core/src/test/java/org/sonar/core/platform/ServerIdTest.java @@ -0,0 +1,309 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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.core.platform; + +import com.tngtech.java.junit.dataprovider.DataProvider; +import com.tngtech.java.junit.dataprovider.DataProviderRunner; +import com.tngtech.java.junit.dataprovider.UseDataProvider; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Random; +import java.util.stream.IntStream; +import java.util.stream.Stream; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.sonar.core.util.UuidFactoryImpl; + +import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic; +import static org.apache.commons.lang.StringUtils.repeat; +import static org.assertj.core.api.Assertions.assertThat; +import static org.sonar.core.platform.ServerId.DATABASE_ID_LENGTH; +import static org.sonar.core.platform.ServerId.DEPRECATED_SERVER_ID_LENGTH; +import static org.sonar.core.platform.ServerId.NOT_UUID_DATASET_ID_LENGTH; +import static org.sonar.core.platform.ServerId.SPLIT_CHARACTER; +import static org.sonar.core.platform.ServerId.UUID_DATASET_ID_LENGTH; +import static org.sonar.core.platform.ServerId.Format.DEPRECATED; +import static org.sonar.core.platform.ServerId.Format.NO_DATABASE_ID; +import static org.sonar.core.platform.ServerId.Format.WITH_DATABASE_ID; + +@RunWith(DataProviderRunner.class) +public class ServerIdTest { + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + @Test + public void parse_throws_NPE_if_argument_is_null() { + expectedException.expect(NullPointerException.class); + + ServerId.parse(null); + } + + @Test + @UseDataProvider("emptyAfterTrim") + public void parse_throws_IAE_if_parameter_is_empty_after_trim(String serverId) { + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("serverId can't be empty"); + + ServerId.parse(serverId); + } + + @DataProvider + public static Object[][] emptyAfterTrim() { + return new Object[][] { + {""}, + {" "}, + {" "} + }; + } + + @Test + @UseDataProvider("wrongFormatWithDatabaseId") + public void parse_throws_IAE_if_split_char_is_at_wrong_position(String emptyDatabaseId) { + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("Unrecognized serverId format. Parts have wrong length"); + + ServerId.parse(emptyDatabaseId); + } + + @DataProvider + public static Object[][] wrongFormatWithDatabaseId() { + String onlySplitChar = repeat(SPLIT_CHARACTER + "", DATABASE_ID_LENGTH); + String startWithSplitChar = SPLIT_CHARACTER + randomAlphabetic(DATABASE_ID_LENGTH - 1); + + Stream databaseIds = Stream.of( + UuidFactoryImpl.INSTANCE.create(), + randomAlphabetic(NOT_UUID_DATASET_ID_LENGTH), + randomAlphabetic(UUID_DATASET_ID_LENGTH), + repeat(SPLIT_CHARACTER + "", NOT_UUID_DATASET_ID_LENGTH), + repeat(SPLIT_CHARACTER + "", UUID_DATASET_ID_LENGTH)); + + return databaseIds + .flatMap(datasetId -> Stream.of( + startWithSplitChar + SPLIT_CHARACTER + datasetId, + onlySplitChar + SPLIT_CHARACTER + datasetId, + startWithSplitChar + randomAlphabetic(1) + datasetId, + onlySplitChar + randomAlphabetic(1) + datasetId)) + .flatMap(serverId -> Stream.of( + serverId, + " " + serverId, + " " + serverId)) + .map(t -> new Object[] {t}) + .toArray(Object[][]::new); + } + + @Test + public void parse_parses_deprecated_format_serverId() { + String deprecated = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date()); + + ServerId serverId = ServerId.parse(deprecated); + + assertThat(serverId.getFormat()).isEqualTo(DEPRECATED); + assertThat(serverId.getDatasetId()).isEqualTo(deprecated); + assertThat(serverId.getDatabaseId()).isEmpty(); + assertThat(serverId.toString()).isEqualTo(deprecated); + } + + @Test + @UseDataProvider("validOldFormatServerIds") + public void parse_parses_no_databaseId_format_serverId(String noDatabaseId) { + ServerId serverId = ServerId.parse(noDatabaseId); + + assertThat(serverId.getFormat()).isEqualTo(NO_DATABASE_ID); + assertThat(serverId.getDatasetId()).isEqualTo(noDatabaseId); + assertThat(serverId.getDatabaseId()).isEmpty(); + assertThat(serverId.toString()).isEqualTo(noDatabaseId); + } + + @DataProvider + public static Object[][] validOldFormatServerIds() { + return new Object[][] { + {UuidFactoryImpl.INSTANCE.create()}, + {randomAlphabetic(NOT_UUID_DATASET_ID_LENGTH)}, + {repeat(SPLIT_CHARACTER + "", NOT_UUID_DATASET_ID_LENGTH)}, + {randomAlphabetic(UUID_DATASET_ID_LENGTH)}, + {repeat(SPLIT_CHARACTER + "", UUID_DATASET_ID_LENGTH)} + }; + } + + @Test + @UseDataProvider("validServerIdWithDatabaseId") + public void parse_parses_serverId_with_database_id(String databaseId, String datasetId) { + String rawServerId = databaseId + SPLIT_CHARACTER + datasetId; + + ServerId serverId = ServerId.parse(rawServerId); + + assertThat(serverId.getFormat()).isEqualTo(WITH_DATABASE_ID); + assertThat(serverId.getDatasetId()).isEqualTo(datasetId); + assertThat(serverId.getDatabaseId()).contains(databaseId); + assertThat(serverId.toString()).isEqualTo(rawServerId); + } + + @DataProvider + public static Object[][] validServerIdWithDatabaseId() { + return new Object[][] { + {randomAlphabetic(DATABASE_ID_LENGTH), randomAlphabetic(NOT_UUID_DATASET_ID_LENGTH)}, + {randomAlphabetic(DATABASE_ID_LENGTH), randomAlphabetic(UUID_DATASET_ID_LENGTH)}, + {randomAlphabetic(DATABASE_ID_LENGTH), repeat(SPLIT_CHARACTER + "", NOT_UUID_DATASET_ID_LENGTH)}, + {randomAlphabetic(DATABASE_ID_LENGTH), repeat(SPLIT_CHARACTER + "", UUID_DATASET_ID_LENGTH)}, + {randomAlphabetic(DATABASE_ID_LENGTH), UuidFactoryImpl.INSTANCE.create()}, + }; + } + + @Test + public void parse_does_not_support_deprecated_server_id_with_database_id() { + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("serverId does not have a supported length"); + + ServerId.parse(randomAlphabetic(DATABASE_ID_LENGTH) + SPLIT_CHARACTER + randomAlphabetic(DEPRECATED_SERVER_ID_LENGTH)); + } + + @Test + public void of_throws_NPE_if_datasetId_is_null() { + expectedException.expect(NullPointerException.class); + + ServerId.of(randomAlphabetic(DATABASE_ID_LENGTH), null); + } + + @Test + public void of_throws_IAE_if_datasetId_is_empty() { + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("Illegal datasetId length (0)"); + + ServerId.of(randomAlphabetic(DATABASE_ID_LENGTH), ""); + } + + @Test + public void of_throws_IAE_if_databaseId_is_empty() { + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("Illegal databaseId length (0)"); + + ServerId.of("", randomAlphabetic(UUID_DATASET_ID_LENGTH)); + } + + @Test + @UseDataProvider("datasetIdSupportedLengths") + public void of_accepts_null_databaseId(int datasetIdLength) { + String datasetId = randomAlphabetic(datasetIdLength); + ServerId serverId = ServerId.of(null, datasetId); + + assertThat(serverId.getDatabaseId()).isEmpty(); + assertThat(serverId.getDatasetId()).isEqualTo(datasetId); + } + + @Test + @UseDataProvider("illegalDatabaseIdLengths") + public void of_throws_IAE_if_databaseId_length_is_not_8(int illegalDatabaseIdLengths) { + String databaseId = randomAlphabetic(illegalDatabaseIdLengths); + String datasetId = randomAlphabetic(UUID_DATASET_ID_LENGTH); + + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("Illegal databaseId length (" + illegalDatabaseIdLengths + ")"); + + ServerId.of(databaseId, datasetId); + } + + @DataProvider + public static Object[][] illegalDatabaseIdLengths() { + return IntStream.range(1, 8 + new Random().nextInt(5)) + .filter(i -> i != DATABASE_ID_LENGTH) + .mapToObj(i -> new Object[] {i}) + .toArray(Object[][]::new); + } + + @Test + @UseDataProvider("illegalDatasetIdLengths") + public void of_throws_IAE_if_datasetId_length_is_not_8(int illegalDatasetIdLengths) { + String datasetId = randomAlphabetic(illegalDatasetIdLengths); + String databaseId = randomAlphabetic(DATABASE_ID_LENGTH); + + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("Illegal datasetId length (" + illegalDatasetIdLengths + ")"); + + ServerId.of(databaseId, datasetId); + } + + @DataProvider + public static Object[][] illegalDatasetIdLengths() { + return IntStream.range(1, UUID_DATASET_ID_LENGTH + new Random().nextInt(5)) + .filter(i -> i != UUID_DATASET_ID_LENGTH) + .filter(i -> i != NOT_UUID_DATASET_ID_LENGTH) + .filter(i -> i != DEPRECATED_SERVER_ID_LENGTH) + .mapToObj(i -> new Object[] {i}) + .toArray(Object[][]::new); + } + + @Test + @UseDataProvider("datasetIdSupportedLengths") + public void equals_is_based_on_databaseId_and_datasetId(int datasetIdLength) { + String databaseId = randomAlphabetic(DATABASE_ID_LENGTH - 1) + 'a'; + String otherDatabaseId = randomAlphabetic(DATABASE_ID_LENGTH - 1) + 'b'; + String datasetId = randomAlphabetic(datasetIdLength - 1) + 'a'; + String otherDatasetId = randomAlphabetic(datasetIdLength - 1) + 'b'; + + ServerId newServerId = ServerId.of(databaseId, datasetId); + assertThat(newServerId).isEqualTo(newServerId); + assertThat(newServerId).isEqualTo(ServerId.of(databaseId, datasetId)); + assertThat(newServerId).isNotEqualTo(new Object()); + assertThat(newServerId).isNotEqualTo(null); + assertThat(newServerId).isNotEqualTo(ServerId.of(otherDatabaseId, datasetId)); + assertThat(newServerId).isNotEqualTo(ServerId.of(databaseId, otherDatasetId)); + assertThat(newServerId).isNotEqualTo(ServerId.of(otherDatabaseId, otherDatasetId)); + + ServerId oldServerId = ServerId.parse(datasetId); + assertThat(oldServerId).isEqualTo(oldServerId); + assertThat(oldServerId).isEqualTo(ServerId.parse(datasetId)); + assertThat(oldServerId).isNotEqualTo(ServerId.parse(otherDatasetId)); + assertThat(oldServerId).isNotEqualTo(ServerId.of(databaseId, datasetId)); + } + + @Test + @UseDataProvider("datasetIdSupportedLengths") + public void hashcode_is_based_on_databaseId_and_datasetId(int datasetIdLength) { + String databaseId = randomAlphabetic(DATABASE_ID_LENGTH - 1) + 'a'; + String otherDatabaseId = randomAlphabetic(DATABASE_ID_LENGTH - 1) + 'b'; + String datasetId = randomAlphabetic(datasetIdLength - 1) + 'a'; + String otherDatasetId = randomAlphabetic(datasetIdLength - 1) + 'b'; + + ServerId newServerId = ServerId.of(databaseId, datasetId); + assertThat(newServerId.hashCode()).isEqualTo(newServerId.hashCode()); + assertThat(newServerId.hashCode()).isEqualTo(ServerId.of(databaseId, datasetId).hashCode()); + assertThat(newServerId.hashCode()).isNotEqualTo(new Object().hashCode()); + assertThat(newServerId.hashCode()).isNotEqualTo(null); + assertThat(newServerId.hashCode()).isNotEqualTo(ServerId.of(otherDatabaseId, datasetId).hashCode()); + assertThat(newServerId.hashCode()).isNotEqualTo(ServerId.of(databaseId, otherDatasetId).hashCode()); + assertThat(newServerId.hashCode()).isNotEqualTo(ServerId.of(otherDatabaseId, otherDatasetId).hashCode()); + + ServerId oldServerId = ServerId.parse(datasetId); + assertThat(oldServerId.hashCode()).isEqualTo(oldServerId.hashCode()); + assertThat(oldServerId.hashCode()).isEqualTo(ServerId.parse(datasetId).hashCode()); + assertThat(oldServerId.hashCode()).isNotEqualTo(ServerId.parse(otherDatasetId).hashCode()); + assertThat(oldServerId.hashCode()).isNotEqualTo(ServerId.of(databaseId, datasetId).hashCode()); + } + + @DataProvider + public static Object[][] datasetIdSupportedLengths() { + return new Object[][] { + {ServerId.NOT_UUID_DATASET_ID_LENGTH}, + {UUID_DATASET_ID_LENGTH}, + }; + } +}