From 6923dc82c9337e80d9aa0de8456accffa2313e24 Mon Sep 17 00:00:00 2001 From: =?utf8?q?S=C3=A9bastien=20Lesaint?= Date: Tue, 17 Oct 2017 17:36:50 +0200 Subject: [PATCH] SONAR-10002 store failure cause in case of error during auto install --- .../server/edition/ws/ApplyLicenseAction.java | 14 +- .../plugins/edition/EditionInstaller.java | 34 ++-- .../edition/ws/ApplyLicenseActionTest.java | 17 +- .../plugins/edition/EditionInstallerTest.java | 154 +++++++++++++++--- 4 files changed, 167 insertions(+), 52 deletions(-) diff --git a/server/sonar-server/src/main/java/org/sonar/server/edition/ws/ApplyLicenseAction.java b/server/sonar-server/src/main/java/org/sonar/server/edition/ws/ApplyLicenseAction.java index abdc676593d..9f9dd7e87d4 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/edition/ws/ApplyLicenseAction.java +++ b/server/sonar-server/src/main/java/org/sonar/server/edition/ws/ApplyLicenseAction.java @@ -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); diff --git a/server/sonar-server/src/main/java/org/sonar/server/plugins/edition/EditionInstaller.java b/server/sonar-server/src/main/java/org/sonar/server/plugins/edition/EditionInstaller.java index 5fac04557c2..057cbe0a8e2 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/plugins/edition/EditionInstaller.java +++ b/server/sonar-server/src/main/java/org/sonar/server/plugins/edition/EditionInstaller.java @@ -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 editionPluginKeys) { + public void install(License newLicense) { if (lock.tryLock()) { try { Optional 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 editionPluginKeys, UpdateCenter updateCenter) { - Map pluginInfosByKeys = pluginRepository.getPluginInfosByKeys(); - Set pluginsToRemove = pluginsToRemove(editionPluginKeys, pluginInfosByKeys.values()); - Set pluginsToInstall = pluginsToInstall(editionPluginKeys, pluginInfosByKeys.keySet()); - + private void asyncInstall(License newLicense, UpdateCenter updateCenter) { try { + Set editionPluginKeys = newLicense.getPluginKeys(); + Map pluginInfosByKeys = pluginRepository.getPluginInfosByKeys(); + Set pluginsToRemove = pluginsToRemove(editionPluginKeys, pluginInfosByKeys.values()); + Set 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 } } diff --git a/server/sonar-server/src/test/java/org/sonar/server/edition/ws/ApplyLicenseActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/edition/ws/ApplyLicenseActionTest.java index bb11461f447..5ec1227ae58 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/edition/ws/ApplyLicenseActionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/edition/ws/ApplyLicenseActionTest.java @@ -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, diff --git a/server/sonar-server/src/test/java/org/sonar/server/plugins/edition/EditionInstallerTest.java b/server/sonar-server/src/test/java/org/sonar/server/plugins/edition/EditionInstallerTest.java index b897d1b131f..0c6aa349d09 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/plugins/edition/EditionInstallerTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/plugins/edition/EditionInstallerTest.java @@ -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 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 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 pluginsByKey = Arrays.asList(installedPlugins).stream().collect(Collectors.toMap(p -> p.getKey(), p -> p)); + Map 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()); + } + } -- 2.39.5