From 88ed3ff023418efc8933c3c87dc021b5938c584f Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Mon, 16 Sep 2019 13:22:26 +0200 Subject: [PATCH] Add uninstall method to plugin manager --- .../java/sonia/scm/plugin/PluginManager.java | 8 +++ .../scm/plugin/DefaultPluginManager.java | 37 ++++++++++++-- .../scm/plugin/DefaultPluginManagerTest.java | 50 +++++++++++++++++-- 3 files changed, 88 insertions(+), 7 deletions(-) diff --git a/scm-core/src/main/java/sonia/scm/plugin/PluginManager.java b/scm-core/src/main/java/sonia/scm/plugin/PluginManager.java index b7b8f69519..f84a975452 100644 --- a/scm-core/src/main/java/sonia/scm/plugin/PluginManager.java +++ b/scm-core/src/main/java/sonia/scm/plugin/PluginManager.java @@ -81,6 +81,14 @@ public interface PluginManager { */ void install(String name, boolean restartAfterInstallation); + /** + * Marks the plugin with the given name for uninstall. + * + * @param name plugin name + * @param restartAfterInstallation restart context after plugin has been marked to really uninstall the plugin + */ + void uninstall(String name, boolean restartAfterInstallation); + /** * Install all pending plugins and restart the scm context. */ diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginManager.java b/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginManager.java index 807eaac317..11e42891e4 100644 --- a/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginManager.java +++ b/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginManager.java @@ -33,8 +33,7 @@ package sonia.scm.plugin; -//~--- non-JDK imports -------------------------------------------------------- - +import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; import com.google.inject.Singleton; import org.slf4j.Logger; @@ -43,9 +42,11 @@ import sonia.scm.NotFoundException; import sonia.scm.event.ScmEventBus; import sonia.scm.lifecycle.RestartEvent; -//~--- JDK imports ------------------------------------------------------------ import javax.inject.Inject; +import java.io.IOException; +import java.nio.file.Files; import java.util.ArrayList; +import java.util.Collection; import java.util.List; import java.util.Optional; import java.util.Set; @@ -54,6 +55,8 @@ import java.util.stream.Collectors; import static sonia.scm.ContextEntry.ContextBuilder.entity; +//~--- JDK imports ------------------------------------------------------------ + /** * * @author Sebastian Sdorra @@ -67,7 +70,8 @@ public class DefaultPluginManager implements PluginManager { private final PluginLoader loader; private final PluginCenter center; private final PluginInstaller installer; - private final List pendingQueue = new ArrayList<>(); + private final Collection pendingQueue = new ArrayList<>(); + private final PluginDependencyTracker dependencyTracker = new PluginDependencyTracker(); @Inject public DefaultPluginManager(ScmEventBus eventBus, PluginLoader loader, PluginCenter center, PluginInstaller installer) { @@ -75,6 +79,16 @@ public class DefaultPluginManager implements PluginManager { this.loader = loader; this.center = center; this.installer = installer; + + this.computeRequiredPlugins(); + } + + @VisibleForTesting + synchronized void computeRequiredPlugins() { + loader.getInstalledPlugins() + .stream() + .map(InstalledPlugin::getDescriptor) + .forEach(dependencyTracker::addInstalled); } @Override @@ -153,6 +167,21 @@ public class DefaultPluginManager implements PluginManager { } } + @Override + public void uninstall(String name, boolean restartAfterInstallation) { + PluginPermissions.manage().check(); + InstalledPlugin installed = getInstalled(name) + .orElseThrow(() -> NotFoundException.notFound(entity(InstalledPlugin.class, name))); + + dependencyTracker.removeInstalled(installed.getDescriptor()); + + try { + Files.createFile(installed.getDirectory().resolve("uninstall")); + } catch (IOException e) { + throw new PluginException("could not mark plugin " + name + " in path " + installed.getDirectory() + " for uninstall", e); + } + } + @Override public void installPendingAndRestart() { PluginPermissions.manage().check(); diff --git a/scm-webapp/src/test/java/sonia/scm/plugin/DefaultPluginManagerTest.java b/scm-webapp/src/test/java/sonia/scm/plugin/DefaultPluginManagerTest.java index 16e1e2d73a..44799a03fb 100644 --- a/scm-webapp/src/test/java/sonia/scm/plugin/DefaultPluginManagerTest.java +++ b/scm-webapp/src/test/java/sonia/scm/plugin/DefaultPluginManagerTest.java @@ -10,26 +10,37 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Answers; +import org.junitpioneer.jupiter.TempDirectory; import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import sonia.scm.NotFoundException; +import sonia.scm.ScmConstraintViolationException; import sonia.scm.event.ScmEventBus; import sonia.scm.lifecycle.RestartEvent; +import java.nio.file.Path; import java.util.List; import java.util.Optional; +import static java.util.Collections.singleton; +import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.in; import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import static sonia.scm.plugin.PluginTestHelper.createAvailable; import static sonia.scm.plugin.PluginTestHelper.createInstalled; @ExtendWith(MockitoExtension.class) +@ExtendWith(TempDirectory.class) class DefaultPluginManagerTest { @Mock @@ -305,6 +316,34 @@ class DefaultPluginManagerTest { assertThat(available.get(0).isPending()).isTrue(); } + @Test + void shouldThrowExceptionWhenUninstallingUnknownPlugin() { + assertThrows(NotFoundException.class, () -> manager.uninstall("no-such-plugin", false)); + } + + @Test + void shouldUseDependencyTrackerForUninstall() { + InstalledPlugin mailPlugin = createInstalled("scm-mail-plugin"); + InstalledPlugin reviewPlugin = createInstalled("scm-review-plugin"); + when(reviewPlugin.getDescriptor().getDependencies()).thenReturn(singleton("scm-mail-plugin")); + + when(loader.getInstalledPlugins()).thenReturn(ImmutableList.of(mailPlugin, reviewPlugin)); + manager.computeRequiredPlugins(); + + assertThrows(ScmConstraintViolationException.class, () -> manager.uninstall("scm-mail-plugin", false)); + } + + @Test + void shouldCreateUninstallFile(@TempDirectory.TempDir Path temp) { + InstalledPlugin mailPlugin = createInstalled("scm-mail-plugin"); + when(mailPlugin.getDirectory()).thenReturn(temp); + + when(loader.getInstalledPlugins()).thenReturn(singletonList(mailPlugin)); + + manager.uninstall("scm-mail-plugin", false); + + assertThat(temp.resolve("uninstall")).exists(); + } } @Nested @@ -350,6 +389,11 @@ class DefaultPluginManagerTest { assertThrows(AuthorizationException.class, () -> manager.install("test", false)); } + @Test + void shouldThrowAuthorizationExceptionsForUninstallMethod() { + assertThrows(AuthorizationException.class, () -> manager.uninstall("test", false)); + } + @Test void shouldThrowAuthorizationExceptionsForInstallPendingAndRestart() { assertThrows(AuthorizationException.class, () -> manager.installPendingAndRestart());