]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-10002 store failure cause in case of error during auto install
authorSébastien Lesaint <sebastien.lesaint@sonarsource.com>
Tue, 17 Oct 2017 15:36:50 +0000 (17:36 +0200)
committerGrégoire Aubert <gregoire.aubert@sonarsource.com>
Mon, 23 Oct 2017 15:01:13 +0000 (08:01 -0700)
server/sonar-server/src/main/java/org/sonar/server/edition/ws/ApplyLicenseAction.java
server/sonar-server/src/main/java/org/sonar/server/plugins/edition/EditionInstaller.java
server/sonar-server/src/test/java/org/sonar/server/edition/ws/ApplyLicenseActionTest.java
server/sonar-server/src/test/java/org/sonar/server/plugins/edition/EditionInstallerTest.java

index abdc676593dc0906811f5c8544f0d577313b7bb7..9f9dd7e87d4f8b8ef1b76f6eb559af231a23b8ff 100644 (file)
@@ -83,18 +83,14 @@ public class ApplyLicenseAction implements EditionsWsAction {
     String licenseParam = request.mandatoryParam(PARAM_LICENSE);
     License newLicense = License.parse(licenseParam).orElseThrow(() -> BadRequestException.create("The license provided is invalid"));
 
-    if (!editionInstaller.requiresInstallationChange(newLicense.getPluginKeys())) {
-      editionManagementState.newEditionWithoutInstall(newLicense.getEditionKey());
+    if (editionInstaller.requiresInstallationChange(newLicense.getPluginKeys())) {
+      editionInstaller.install(newLicense);
+    } else {
       checkState(licenseCommit != null,
         "Can't decide edition does not require install if LicenseCommit instance is null. " +
           "License-manager plugin should be installed.");
-      licenseCommit.update(newLicense.getContent());    } else {
-      boolean online = editionInstaller.install(newLicense.getPluginKeys());
-      if (online) {
-        editionManagementState.startAutomaticInstall(newLicense);
-      } else {
-        editionManagementState.startManualInstall(newLicense);
-      }
+      editionManagementState.newEditionWithoutInstall(newLicense.getEditionKey());
+      licenseCommit.update(newLicense.getContent());
     }
 
     WsUtils.writeProtobuf(buildResponse(), request, response);
index 5fac04557c2015078f8c9ed9596eb31df759c825..057cbe0a8e27620a8e5361c370b7c1f6e22c53b9 100644 (file)
@@ -26,13 +26,18 @@ import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.locks.ReentrantLock;
 import java.util.stream.Collectors;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
 import org.sonar.core.platform.PluginInfo;
+import org.sonar.server.edition.License;
 import org.sonar.server.edition.MutableEditionManagementState;
 import org.sonar.server.plugins.ServerPluginRepository;
 import org.sonar.server.plugins.UpdateCenterMatrixFactory;
 import org.sonar.updatecenter.common.UpdateCenter;
 
 public class EditionInstaller {
+  private static final Logger LOG = Loggers.get(EditionInstaller.class);
+
   private final ReentrantLock lock = new ReentrantLock();
   private final EditionInstallerExecutor executor;
   private final EditionPluginDownloader editionPluginDownloader;
@@ -54,19 +59,21 @@ public class EditionInstaller {
 
   /**
    * Refreshes the update center, and submits in a executor a task to download all the needed plugins (asynchronously).
-   * If the update center is disabled or if we are offline, the task is not submitted and false is returned. 
-   * @return true if a task was submitted to perform the download, false if update center is unavailable.
+   * If the update center is disabled or if we are offline, the task is not submitted and false is returned.
+   * In all case
+   *
    * @throws IllegalStateException if an installation is already in progress
    */
-  public boolean install(Set<String> editionPluginKeys) {
+  public void install(License newLicense) {
     if (lock.tryLock()) {
       try {
         Optional<UpdateCenter> updateCenter = updateCenterMatrixFactory.getUpdateCenter(true);
         if (!updateCenter.isPresent()) {
-          return false;
+          editionManagementState.startManualInstall(newLicense);
+          return;
         }
-        executor.execute(() -> asyncInstall(editionPluginKeys, updateCenter.get()));
-        return true;
+        editionManagementState.startAutomaticInstall(newLicense);
+        executor.execute(() -> asyncInstall(newLicense, updateCenter.get()));
       } catch (RuntimeException e) {
         lock.unlock();
         throw e;
@@ -97,18 +104,21 @@ public class EditionInstaller {
       || !pluginsToRemove(editionPluginKeys, pluginInfosByKeys.values()).isEmpty();
   }
 
-  private void asyncInstall(Set<String> editionPluginKeys, UpdateCenter updateCenter) {
-    Map<String, PluginInfo> pluginInfosByKeys = pluginRepository.getPluginInfosByKeys();
-    Set<String> pluginsToRemove = pluginsToRemove(editionPluginKeys, pluginInfosByKeys.values());
-    Set<String> pluginsToInstall = pluginsToInstall(editionPluginKeys, pluginInfosByKeys.keySet());
-
+  private void asyncInstall(License newLicense, UpdateCenter updateCenter) {
     try {
+      Set<String> editionPluginKeys = newLicense.getPluginKeys();
+      Map<String, PluginInfo> pluginInfosByKeys = pluginRepository.getPluginInfosByKeys();
+      Set<String> pluginsToRemove = pluginsToRemove(editionPluginKeys, pluginInfosByKeys.values());
+      Set<String> pluginsToInstall = pluginsToInstall(editionPluginKeys, pluginInfosByKeys.keySet());
+
       editionPluginDownloader.downloadEditionPlugins(pluginsToInstall, updateCenter);
       uninstallPlugins(pluginsToRemove);
       editionManagementState.automaticInstallReady();
+    } catch (Throwable t) {
+      LOG.error("Failed to install edition {} with plugins {}", newLicense.getEditionKey(), newLicense.getPluginKeys(), t);
+      editionManagementState.installFailed(t.getMessage());
     } finally {
       lock.unlock();
-      // TODO: catch exceptions and set error status
     }
   }
 
index bb11461f447e0da0b754b3bad59e77f308367c9f..5ec1227ae581394c64f34563d7c37c90e7ebd29b 100644 (file)
@@ -27,7 +27,6 @@ import java.io.StringWriter;
 import java.nio.charset.StandardCharsets;
 import java.util.Arrays;
 import java.util.Base64;
-import java.util.Collections;
 import java.util.Optional;
 import java.util.Properties;
 import org.junit.Rule;
@@ -53,9 +52,11 @@ import org.sonarqube.ws.MediaTypes;
 import org.sonarqube.ws.WsEditions;
 import org.sonarqube.ws.WsEditions.StatusResponse;
 
+import static java.util.Collections.singleton;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.mockito.Matchers.any;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 import static org.sonar.server.edition.EditionManagementState.PendingStatus.AUTOMATIC_IN_PROGRESS;
@@ -170,7 +171,7 @@ public class ApplyLicenseActionTest {
   public void apply_without_need_to_install() throws IOException {
     userSessionRule.logIn().setSystemAdministrator();
     setPendingLicense(NONE);
-    when(editionInstaller.requiresInstallationChange(Collections.singleton("plugin1"))).thenReturn(false);
+    when(editionInstaller.requiresInstallationChange(singleton("plugin1"))).thenReturn(false);
 
     TestRequest request = actionTester.newRequest()
       .setMediaType(MediaTypes.PROTOBUF)
@@ -185,8 +186,7 @@ public class ApplyLicenseActionTest {
   public void apply_offline() throws IOException {
     userSessionRule.logIn().setSystemAdministrator();
     setPendingLicense(PendingStatus.MANUAL_IN_PROGRESS);
-    when(editionInstaller.requiresInstallationChange(Collections.singleton("plugin1"))).thenReturn(true);
-    when(editionInstaller.install(Collections.singleton("plugin1"))).thenReturn(false);
+    when(editionInstaller.requiresInstallationChange(singleton("plugin1"))).thenReturn(true);
 
     TestRequest request = actionTester.newRequest()
       .setMediaType(MediaTypes.PROTOBUF)
@@ -195,15 +195,15 @@ public class ApplyLicenseActionTest {
     TestResponse response = request.execute();
 
     assertResponse(response, PENDING_EDITION_NAME, "", PendingStatus.MANUAL_IN_PROGRESS);
-    verify(mutableEditionManagementState).startManualInstall(any(License.class));
+    verify(mutableEditionManagementState, times(0)).startManualInstall(any(License.class));
+    verify(mutableEditionManagementState, times(0)).startAutomaticInstall(any(License.class));
   }
 
   @Test
   public void apply_successfully_auto_installation() throws IOException {
     userSessionRule.logIn().setSystemAdministrator();
     setPendingLicense(PendingStatus.AUTOMATIC_IN_PROGRESS);
-    when(editionInstaller.requiresInstallationChange(Collections.singleton("plugin1"))).thenReturn(true);
-    when(editionInstaller.install(Collections.singleton("plugin1"))).thenReturn(true);
+    when(editionInstaller.requiresInstallationChange(singleton("plugin1"))).thenReturn(true);
 
     TestRequest request = actionTester.newRequest()
       .setMediaType(MediaTypes.PROTOBUF)
@@ -212,7 +212,8 @@ public class ApplyLicenseActionTest {
     TestResponse response = request.execute();
 
     assertResponse(response, PENDING_EDITION_NAME, "", PendingStatus.AUTOMATIC_IN_PROGRESS);
-    verify(mutableEditionManagementState).startAutomaticInstall(any(License.class));
+    verify(mutableEditionManagementState, times(0)).startAutomaticInstall(any(License.class));
+    verify(mutableEditionManagementState, times(0)).startManualInstall(any(License.class));
   }
 
   private void assertResponse(TestResponse response, String expectedNextEditionKey, String expectedEditionKey,
index b897d1b131fec7c85c6f912b0652f8a47770343b..0c6aa349d0976b0a2917df322f3844af0f6828c6 100644 (file)
@@ -21,28 +21,42 @@ package org.sonar.server.plugins.edition;
 
 import com.google.common.base.Optional;
 import java.util.Arrays;
-import java.util.Collections;
 import java.util.HashSet;
 import java.util.Map;
 import java.util.Set;
-import java.util.stream.Collectors;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.sonar.api.utils.log.LogTester;
+import org.sonar.api.utils.log.LoggerLevel;
 import org.sonar.core.platform.PluginInfo;
+import org.sonar.server.edition.License;
 import org.sonar.server.edition.MutableEditionManagementState;
 import org.sonar.server.plugins.ServerPluginRepository;
 import org.sonar.server.plugins.UpdateCenterMatrixFactory;
 import org.sonar.updatecenter.common.UpdateCenter;
 
+import static java.util.Collections.singleton;
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Matchers.any;
 import static org.mockito.Matchers.anyBoolean;
+import static org.mockito.Matchers.anySetOf;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoMoreInteractions;
 import static org.mockito.Mockito.verifyZeroInteractions;
 import static org.mockito.Mockito.when;
+import static org.sonar.core.util.stream.MoreCollectors.uniqueIndex;
 
 public class EditionInstallerTest {
+  @Rule
+  public LogTester logTester = new LogTester();
+
   private static final String PLUGIN_KEY = "key";
 
   private EditionPluginDownloader downloader = mock(EditionPluginDownloader.class);
@@ -52,13 +66,17 @@ public class EditionInstallerTest {
   private UpdateCenter updateCenter = mock(UpdateCenter.class);
   private MutableEditionManagementState editionManagementState = mock(MutableEditionManagementState.class);
 
-  private EditionInstallerExecutor executor = new EditionInstallerExecutor() {
+  private EditionInstallerExecutor synchronousExecutor = new EditionInstallerExecutor() {
     public void execute(Runnable r) {
       r.run();
     }
   };
+  private EditionInstallerExecutor mockedExecutor = mock(EditionInstallerExecutor.class);
 
-  private EditionInstaller installer = new EditionInstaller(downloader, uninstaller, pluginRepository, executor, updateCenterMatrixFactory, editionManagementState);
+  private EditionInstaller underTestSynchronousExecutor = new EditionInstaller(downloader, uninstaller, pluginRepository, synchronousExecutor, updateCenterMatrixFactory,
+    editionManagementState);
+  private EditionInstaller underTestMockedExecutor = new EditionInstaller(downloader, uninstaller, pluginRepository, mockedExecutor, updateCenterMatrixFactory,
+    editionManagementState);
 
   @Before
   public void setUp() {
@@ -67,8 +85,78 @@ public class EditionInstallerTest {
 
   @Test
   public void launch_task_download_plugins() {
-    assertThat(installer.install(Collections.singleton(PLUGIN_KEY))).isTrue();
-    verify(downloader).downloadEditionPlugins(Collections.singleton(PLUGIN_KEY), updateCenter);
+    underTestSynchronousExecutor.install(licenseWithPluginKeys(PLUGIN_KEY));
+
+    verify(downloader).downloadEditionPlugins(singleton(PLUGIN_KEY), updateCenter);
+  }
+
+  @Test
+  public void editionManagementState_is_changed_before_running_background_thread() {
+    PluginInfo commercial1 = createPluginInfo("p1", true);
+    PluginInfo commercial2 = createPluginInfo("p2", true);
+    PluginInfo open1 = createPluginInfo("p3", false);
+    mockPluginRepository(commercial1, commercial2, open1);
+    License newLicense = licenseWithPluginKeys("p1", "p4");
+
+    underTestMockedExecutor.install(newLicense);
+
+    verify(editionManagementState).startAutomaticInstall(newLicense);
+    verify(editionManagementState, times(0)).automaticInstallReady();
+    verifyNoMoreInteractions(editionManagementState);
+    ArgumentCaptor<Runnable> runnableCaptor = ArgumentCaptor.forClass(Runnable.class);
+    verify(mockedExecutor).execute(runnableCaptor.capture());
+
+    reset(editionManagementState);
+    runnableCaptor.getValue().run();
+
+    verify(editionManagementState).automaticInstallReady();
+    verifyNoMoreInteractions(editionManagementState);
+  }
+
+  @Test
+  public void editionManagementState_is_changed_to_automatic_failure_when_read_existing_plugins_fails() {
+    RuntimeException fakeException = new RuntimeException("Faking getPluginInfosByKeys throwing an exception");
+    when(pluginRepository.getPluginInfosByKeys())
+      .thenThrow(fakeException);
+    License newLicense = licenseWithPluginKeys("p1", "p4");
+
+    underTestSynchronousExecutor.install(newLicense);
+
+    verifyMoveToAutomaticFailureAndLogsError(newLicense, fakeException.getMessage());
+  }
+
+  @Test
+  public void editionManagementState_is_changed_to_automatic_failure_when_downloader_fails() {
+    PluginInfo commercial1 = createPluginInfo("p1", true);
+    PluginInfo commercial2 = createPluginInfo("p2", true);
+    PluginInfo open1 = createPluginInfo("p3", false);
+    mockPluginRepository(commercial1, commercial2, open1);
+    RuntimeException fakeException = new RuntimeException("Faking downloadEditionPlugins throwing an exception");
+    doThrow(fakeException)
+      .when(downloader)
+      .downloadEditionPlugins(anySetOf(String.class), any(UpdateCenter.class));
+    License newLicense = licenseWithPluginKeys("p1", "p4");
+
+    underTestSynchronousExecutor.install(newLicense);
+
+    verifyMoveToAutomaticFailureAndLogsError(newLicense, fakeException.getMessage());
+  }
+
+  @Test
+  public void editionManagementState_is_changed_to_automatic_failure_when_uninstaller_fails() {
+    PluginInfo commercial1 = createPluginInfo("p1", true);
+    PluginInfo commercial2 = createPluginInfo("p2", true);
+    PluginInfo open1 = createPluginInfo("p3", false);
+    mockPluginRepository(commercial1, commercial2, open1);
+    RuntimeException fakeException = new RuntimeException("Faking uninstall throwing an exception");
+    doThrow(fakeException)
+      .when(uninstaller)
+      .uninstall(anyString());
+    License newLicense = licenseWithPluginKeys("p1", "p4");
+
+    underTestSynchronousExecutor.install(newLicense);
+
+    verifyMoveToAutomaticFailureAndLogsError(newLicense, fakeException.getMessage());
   }
 
   @Test
@@ -77,14 +165,14 @@ public class EditionInstallerTest {
     PluginInfo commercial2 = createPluginInfo("p2", true);
     PluginInfo open1 = createPluginInfo("p3", false);
     mockPluginRepository(commercial1, commercial2, open1);
+    License newLicense = licenseWithPluginKeys("p1", "p4");
 
-    Set<String> editionPlugins = new HashSet<>();
-    editionPlugins.add("p1");
-    editionPlugins.add("p4");
-    installer.install(editionPlugins);
+    underTestSynchronousExecutor.install(newLicense);
 
+    verify(editionManagementState).startAutomaticInstall(newLicense);
     verify(editionManagementState).automaticInstallReady();
-    verify(downloader).downloadEditionPlugins(Collections.singleton("p4"), updateCenter);
+    verifyNoMoreInteractions(editionManagementState);
+    verify(downloader).downloadEditionPlugins(singleton("p4"), updateCenter);
     verify(uninstaller).uninstall("p2");
     verifyNoMoreInteractions(uninstaller);
     verifyNoMoreInteractions(downloader);
@@ -97,7 +185,7 @@ public class EditionInstallerTest {
     PluginInfo open1 = createPluginInfo("p3", false);
     mockPluginRepository(commercial1, commercial2, open1);
 
-    installer.uninstall();
+    underTestSynchronousExecutor.uninstall();
 
     verify(uninstaller).uninstall("p2");
     verify(uninstaller).uninstall("p1");
@@ -107,28 +195,31 @@ public class EditionInstallerTest {
   }
 
   @Test
-  public void do_nothing_if_offline() {
+  public void move_to_manualInstall_state_when_offline() {
     mockPluginRepository(createPluginInfo("p1", true));
-    executor = mock(EditionInstallerExecutor.class);
+    synchronousExecutor = mock(EditionInstallerExecutor.class);
     when(updateCenterMatrixFactory.getUpdateCenter(true)).thenReturn(Optional.absent());
-    installer = new EditionInstaller(downloader, uninstaller, pluginRepository, executor, updateCenterMatrixFactory, editionManagementState);
-    assertThat(installer.install(Collections.singleton("p1"))).isFalse();
+    underTestSynchronousExecutor = new EditionInstaller(downloader, uninstaller, pluginRepository, synchronousExecutor, updateCenterMatrixFactory, editionManagementState);
+    License newLicense = licenseWithPluginKeys("p1");
+
+    underTestSynchronousExecutor.install(newLicense);
 
-    verifyZeroInteractions(executor);
+    verifyZeroInteractions(synchronousExecutor);
     verifyZeroInteractions(uninstaller);
     verifyZeroInteractions(downloader);
-    verifyZeroInteractions(editionManagementState);
+    verify(editionManagementState).startManualInstall(newLicense);
+    verifyNoMoreInteractions(editionManagementState);
   }
 
   @Test
   public void is_offline() {
     when(updateCenterMatrixFactory.getUpdateCenter(false)).thenReturn(Optional.absent());
-    assertThat(installer.isOffline()).isTrue();
+    assertThat(underTestSynchronousExecutor.isOffline()).isTrue();
   }
 
   @Test
   public void is_not_offline() {
-    assertThat(installer.isOffline()).isFalse();
+    assertThat(underTestSynchronousExecutor.isOffline()).isFalse();
   }
 
   @Test
@@ -142,7 +233,9 @@ public class EditionInstallerTest {
     editionPlugins.add("p1");
     editionPlugins.add("p4");
 
-    assertThat(installer.requiresInstallationChange(editionPlugins)).isTrue();
+    boolean flag = underTestSynchronousExecutor.requiresInstallationChange(editionPlugins);
+
+    assertThat(flag).isTrue();
     verifyZeroInteractions(downloader);
     verifyZeroInteractions(uninstaller);
     verifyZeroInteractions(editionManagementState);
@@ -159,14 +252,16 @@ public class EditionInstallerTest {
     editionPlugins.add("p1");
     editionPlugins.add("p2");
 
-    assertThat(installer.requiresInstallationChange(editionPlugins)).isFalse();
+    boolean flag = underTestSynchronousExecutor.requiresInstallationChange(editionPlugins);
+
+    assertThat(flag).isFalse();
     verifyZeroInteractions(downloader);
     verifyZeroInteractions(uninstaller);
     verifyZeroInteractions(editionManagementState);
   }
 
   private void mockPluginRepository(PluginInfo... installedPlugins) {
-    Map<String, PluginInfo> pluginsByKey = Arrays.asList(installedPlugins).stream().collect(Collectors.toMap(p -> p.getKey(), p -> p));
+    Map<String, PluginInfo> pluginsByKey = Arrays.stream(installedPlugins).collect(uniqueIndex(PluginInfo::getKey));
     when(pluginRepository.getPluginInfosByKeys()).thenReturn(pluginsByKey);
     when(pluginRepository.getPluginInfos()).thenReturn(Arrays.asList(installedPlugins));
   }
@@ -180,4 +275,17 @@ public class EditionInstallerTest {
     return info;
   }
 
+  private static License licenseWithPluginKeys(String... pluginKeys) {
+    return new License("edition-key", Arrays.asList(pluginKeys), "foo");
+  }
+
+  private void verifyMoveToAutomaticFailureAndLogsError(License newLicense, String expectedErrorMessage) {
+    verify(editionManagementState).startAutomaticInstall(newLicense);
+    verify(editionManagementState).installFailed(expectedErrorMessage);
+    verifyNoMoreInteractions(editionManagementState);
+    assertThat(logTester.logs()).hasSize(1);
+    assertThat(logTester.logs(LoggerLevel.ERROR))
+      .containsOnly("Failed to install edition " + newLicense.getEditionKey() + " with plugins " + newLicense.getPluginKeys());
+  }
+
 }