]> source.dussan.org Git - sonarqube.git/commitdiff
LICENSE-82 reset server ID on DB URL changes
authorSimon Brandhof <simon.brandhof@sonarsource.com>
Mon, 6 Nov 2017 16:58:08 +0000 (17:58 +0100)
committerJulien Lancelot <julien.lancelot@sonarsource.com>
Wed, 8 Nov 2017 09:07:01 +0000 (10:07 +0100)
server/sonar-ce/src/test/java/org/sonar/ce/container/ComputeEngineContainerImplTest.java
server/sonar-db-dao/src/test/java/org/sonar/db/DbTester.java
server/sonar-server/src/main/java/org/sonar/server/platform/BackendCleanup.java
server/sonar-server/src/main/java/org/sonar/server/platform/ServerIdChecksum.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/platform/ServerIdManager.java
server/sonar-server/src/main/java/org/sonar/server/property/InternalProperties.java
server/sonar-server/src/test/java/org/sonar/server/platform/ServerIdChecksumTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/platform/ServerIdManagerTest.java
sonar-application/src/main/assembly/conf/sonar.properties

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