diff --git a/gradle/changelog/cli_plugin_management.yaml b/gradle/changelog/cli_plugin_management.yaml new file mode 100644 index 0000000000..a07b3aa59b --- /dev/null +++ b/gradle/changelog/cli_plugin_management.yaml @@ -0,0 +1,2 @@ +- type: added + description: Enable plugin management via CLI ([#2087](https://github.com/scm-manager/scm-manager/pull/2087)) diff --git a/scm-core/src/main/java/sonia/scm/plugin/PendingPlugins.java b/scm-core/src/main/java/sonia/scm/plugin/PendingPlugins.java new file mode 100644 index 0000000000..2f8b1591cd --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/plugin/PendingPlugins.java @@ -0,0 +1,76 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package sonia.scm.plugin; + +import java.util.List; + +import static com.google.common.collect.Iterables.contains; +import static java.util.stream.Collectors.toList; + +public class PendingPlugins { + + private final List install; + private final List update; + private final List uninstall; + + public PendingPlugins(List availablePlugins, List installedPlugins) { + List pending = availablePlugins + .stream() + .filter(AvailablePlugin::isPending) + .collect(toList()); + + this.install = pending + .stream() + .filter(a -> !contains(installedPlugins, a)).collect(toList()); + this.update = installedPlugins + .stream() + .filter(i -> contains(pending, i)).collect(toList()); + this.uninstall = installedPlugins + .stream() + .filter(InstalledPlugin::isMarkedForUninstall).collect(toList()); + } + + public List getInstall() { + return install; + } + + public List getUpdate() { + return update; + } + + public List getUninstall() { + return uninstall; + } + + public boolean isPending(String name) { + return uninstall.stream().anyMatch(p -> p.getDescriptor().getInformation().getName().equals(name)) + || update.stream().anyMatch(p -> p.getDescriptor().getInformation().getName().equals(name)) + || install.stream().anyMatch(p -> p.getDescriptor().getInformation().getName().equals(name)); + } + + public boolean existPendingChanges() { + return !uninstall.isEmpty() || !update.isEmpty() || !install.isEmpty(); + } +} 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 977867ad24..4720375e0f 100644 --- a/scm-core/src/main/java/sonia/scm/plugin/PluginManager.java +++ b/scm-core/src/main/java/sonia/scm/plugin/PluginManager.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.plugin; import java.util.List; @@ -89,6 +89,14 @@ public interface PluginManager { */ List getUpdatable(); + /** + * Returns all pending plugins. + * + * @return a list of pending plugins. + * @since 2.38.0 + */ + PendingPlugins getPending(); + /** * Installs the plugin with the given name from the list of available plugins. * diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PendingPluginResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PendingPluginResource.java index e5b2551b0c..68dc07629a 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PendingPluginResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PendingPluginResource.java @@ -32,8 +32,7 @@ import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import sonia.scm.lifecycle.Restarter; -import sonia.scm.plugin.AvailablePlugin; -import sonia.scm.plugin.InstalledPlugin; +import sonia.scm.plugin.PendingPlugins; import sonia.scm.plugin.PluginManager; import sonia.scm.plugin.PluginPermissions; import sonia.scm.web.VndMediaType; @@ -44,9 +43,7 @@ import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.core.Response; -import java.util.Collection; import java.util.List; -import java.util.stream.Stream; import static de.otto.edison.hal.Link.link; import static de.otto.edison.hal.Links.linkingTo; @@ -94,28 +91,13 @@ public class PendingPluginResource { ) ) public Response getPending() { - List pending = pluginManager - .getAvailable() - .stream() - .filter(AvailablePlugin::isPending) - .collect(toList()); - List installed = pluginManager.getInstalled(); - - Stream newPlugins = pending - .stream() - .filter(a -> !contains(installed, a)); - Stream updatePlugins = installed - .stream() - .filter(i -> contains(pending, i)); - Stream uninstallPlugins = installed - .stream() - .filter(InstalledPlugin::isMarkedForUninstall); + PendingPlugins pending = pluginManager.getPending(); Links.Builder linksBuilder = linkingTo().self(resourceLinks.pendingPluginCollection().self()); - List installDtos = newPlugins.map(mapper::mapAvailable).collect(toList()); - List updateDtos = updatePlugins.map(i -> mapper.mapInstalled(i, pending)).collect(toList()); - List uninstallDtos = uninstallPlugins.map(i -> mapper.mapInstalled(i, pending)).collect(toList()); + List installDtos = pending.getInstall().stream().map(mapper::mapAvailable).collect(toList()); + List updateDtos = pending.getUpdate().stream().map(p -> mapper.mapInstalled(p, pending.getInstall())).collect(toList()); + List uninstallDtos = pending.getUninstall().stream().map(i -> mapper.mapInstalled(i, pending.getInstall())).collect(toList()); if ( PluginPermissions.write().isPermitted() && @@ -135,22 +117,6 @@ public class PendingPluginResource { return Response.ok(new HalRepresentation(linksBuilder.build(), embedded.build())).build(); } - private boolean contains(Collection installedPlugins, AvailablePlugin availablePlugin) { - return installedPlugins - .stream() - .anyMatch(installedPlugin -> haveSameName(installedPlugin, availablePlugin)); - } - - private boolean contains(Collection availablePlugins, InstalledPlugin installedPlugin) { - return availablePlugins - .stream() - .anyMatch(availablePlugin -> haveSameName(installedPlugin, availablePlugin)); - } - - private boolean haveSameName(InstalledPlugin installedPlugin, AvailablePlugin availablePlugin) { - return installedPlugin.getDescriptor().getInformation().getName().equals(availablePlugin.getDescriptor().getInformation().getName()); - } - @POST @Path("/execute") @Operation( 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 1af6b177cc..256f73eb9a 100644 --- a/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginManager.java +++ b/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginManager.java @@ -41,6 +41,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.function.Function; @@ -87,15 +88,11 @@ public class DefaultPluginManager implements PluginManager { this.eventBus = eventBus; this.pluginSetConfigStore = pluginSetConfigStore; - if (contextFactory != null) { - this.contextFactory = contextFactory; - } else { - this.contextFactory = (plugins -> { - List pendingPlugins = new ArrayList<>(plugins); - pendingInstallQueue.stream().map(PendingPluginInstallation::getPlugin).forEach(pendingPlugins::add); - return PluginInstallationContext.from(getInstalled(), pendingPlugins); - }); - } + this.contextFactory = Objects.requireNonNullElseGet(contextFactory, () -> (plugins -> { + List pendingPlugins = new ArrayList<>(plugins); + pendingInstallQueue.stream().map(PendingPluginInstallation::getPlugin).forEach(pendingPlugins::add); + return PluginInstallationContext.from(getInstalled(), pendingPlugins); + })); this.computeInstallationDependencies(); } @@ -192,6 +189,7 @@ public class DefaultPluginManager implements PluginManager { @Override public List getUpdatable() { + PluginPermissions.read().check(); return getInstalled() .stream() .filter(p -> isUpdatable(p.getDescriptor().getInformation().getName())) @@ -199,6 +197,12 @@ public class DefaultPluginManager implements PluginManager { .collect(Collectors.toList()); } + @Override + public PendingPlugins getPending() { + PluginPermissions.read().check(); + return new PendingPlugins(getAvailable(), getInstalled()); + } + private Predicate filterByName(String name) { return plugin -> name.equals(plugin.getDescriptor().getInformation().getName()); } @@ -361,7 +365,7 @@ public class DefaultPluginManager implements PluginManager { } private boolean isUpdatable(String name) { - return getAvailable(name).isPresent() && !getPending(name).isPresent(); + return getAvailable(name).isPresent() && getPending(name).isEmpty(); } @Override diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/cli/PluginAddCommand.java b/scm-webapp/src/main/java/sonia/scm/plugin/cli/PluginAddCommand.java new file mode 100644 index 0000000000..aa1efc0e95 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/plugin/cli/PluginAddCommand.java @@ -0,0 +1,90 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package sonia.scm.plugin.cli; + +import com.cronutils.utils.VisibleForTesting; +import picocli.CommandLine; +import sonia.scm.cli.ParentCommand; +import sonia.scm.plugin.PluginManager; + +import javax.inject.Inject; + +@ParentCommand(value = PluginCommand.class) +@CommandLine.Command(name = "add") +class PluginAddCommand implements Runnable { + + @CommandLine.Parameters(index = "0", paramLabel = "", descriptionKey = "scm.plugin.name") + private String name; + + @CommandLine.Option(names = {"--apply", "-a"}, descriptionKey = "scm.plugin.apply") + private boolean apply; + + @CommandLine.Mixin + private final PluginTemplateRenderer templateRenderer; + private final PluginManager manager; + @CommandLine.Spec + private CommandLine.Model.CommandSpec spec; + + @Inject + PluginAddCommand(PluginTemplateRenderer templateRenderer, PluginManager manager) { + this.templateRenderer = templateRenderer; + this.manager = manager; + } + + @Override + public void run() { + if (manager.getInstalled(name).isPresent()) { + templateRenderer.renderPluginAlreadyInstalledError(); + return; + } + if (manager.getAvailable(name).isEmpty()) { + templateRenderer.renderPluginNotAvailableError(); + return; + } + + try { + manager.install(name, apply); + } catch (Exception e) { + templateRenderer.renderPluginCouldNotBeAdded(name); + throw e; + } + templateRenderer.renderPluginAdded(name); + if (!apply) { + templateRenderer.renderServerRestartRequired(); + } else { + templateRenderer.renderServerRestartTriggered(); + } + } + + @VisibleForTesting + void setName(String name) { + this.name = name; + } + + @VisibleForTesting + void setApply(boolean apply) { + this.apply = apply; + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/cli/PluginApplyCommand.java b/scm-webapp/src/main/java/sonia/scm/plugin/cli/PluginApplyCommand.java new file mode 100644 index 0000000000..4da85c5ca3 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/plugin/cli/PluginApplyCommand.java @@ -0,0 +1,71 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package sonia.scm.plugin.cli; + +import com.cronutils.utils.VisibleForTesting; +import picocli.CommandLine; +import sonia.scm.cli.ParentCommand; +import sonia.scm.plugin.PluginManager; + +import javax.inject.Inject; + +@ParentCommand(value = PluginCommand.class) +@CommandLine.Command(name = "apply") +class PluginApplyCommand implements Runnable { + + @CommandLine.Option(names = {"--yes", "-y"}, descriptionKey = "scm.plugin.restart") + private boolean restart; + + @CommandLine.Mixin + private final PluginTemplateRenderer templateRenderer; + private final PluginManager manager; + @CommandLine.Spec + private CommandLine.Model.CommandSpec spec; + + @Inject + PluginApplyCommand(PluginTemplateRenderer templateRenderer, PluginManager manager) { + this.templateRenderer = templateRenderer; + this.manager = manager; + } + + @Override + public void run() { + if (!restart) { + templateRenderer.renderConfirmServerRestart(); + return; + } + if (manager.getPending().existPendingChanges()) { + manager.executePendingAndRestart(); + templateRenderer.renderServerRestartTriggered(); + } else { + templateRenderer.renderSkipServerRestart(); + } + } + + @VisibleForTesting + void setRestart(boolean restart) { + this.restart = restart; + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/cli/PluginCommand.java b/scm-webapp/src/main/java/sonia/scm/plugin/cli/PluginCommand.java new file mode 100644 index 0000000000..c723379864 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/plugin/cli/PluginCommand.java @@ -0,0 +1,31 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package sonia.scm.plugin.cli; + +import picocli.CommandLine; + +@CommandLine.Command(name = "plugin") +public class PluginCommand { +} diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/cli/PluginListCommand.java b/scm-webapp/src/main/java/sonia/scm/plugin/cli/PluginListCommand.java new file mode 100644 index 0000000000..b85ea088dd --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/plugin/cli/PluginListCommand.java @@ -0,0 +1,163 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package sonia.scm.plugin.cli; + +import com.cronutils.utils.VisibleForTesting; +import lombok.Getter; +import lombok.Setter; +import picocli.CommandLine; +import sonia.scm.cli.ParentCommand; +import sonia.scm.cli.Table; +import sonia.scm.cli.TemplateRenderer; +import sonia.scm.plugin.AvailablePlugin; +import sonia.scm.plugin.InstalledPlugin; +import sonia.scm.plugin.PendingPlugins; +import sonia.scm.plugin.PluginDescriptor; +import sonia.scm.plugin.PluginManager; + +import javax.inject.Inject; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +@ParentCommand(value = PluginCommand.class) +@CommandLine.Command(name = "list", aliases = "ls") +class PluginListCommand implements Runnable { + + @CommandLine.Mixin + private final TemplateRenderer templateRenderer; + private final PluginManager manager; + @CommandLine.Spec + private CommandLine.Model.CommandSpec spec; + + @CommandLine.Option(names = {"--short", "-s"}) + private boolean useShortTemplate; + + private static final String TABLE_TEMPLATE = String.join("\n", + "{{#rows}}", + "{{#cols}}{{#row.first}}{{#upper}}{{value}}{{/upper}}{{/row.first}}{{^row.first}}{{value}}{{/row.first}}{{^last}} {{/last}}{{/cols}}", + "{{/rows}}" + ); + + private static final String SHORT_TEMPLATE = String.join("\n", + "{{#plugins}}", + "{{name}}", + "{{/plugins}}" + ); + + @Inject + public PluginListCommand(TemplateRenderer templateRenderer, PluginManager manager) { + this.templateRenderer = templateRenderer; + this.manager = manager; + } + + @Override + public void run() { + Collection plugins = getListablePlugins(); + if (useShortTemplate) { + templateRenderer.renderToStdout(SHORT_TEMPLATE, Map.of("plugins", plugins)); + } else { + Table table = templateRenderer.createTable(); + String yes = spec.resourceBundle().getString("yes"); + table.addHeader("scm.plugin.name", "scm.plugin.displayName", "scm.plugin.availableVersion", "scm.plugin.installedVersion", "scm.plugin.pending"); + + for (ListablePlugin plugin : plugins) { + table.addRow( + plugin.getName(), + plugin.getDisplayName(), + plugin.getAvailableVersion(), + plugin.getInstalledVersion(), + plugin.isPending() ? yes : "" + ); + } + templateRenderer.renderToStdout(TABLE_TEMPLATE, Map.of("rows", table, "plugins", plugins)); + } + } + + private List getListablePlugins() { + List installedPlugins = manager.getInstalled(); + List availablePlugins = manager.getAvailable(); + PendingPlugins pendingPlugins = manager.getPending(); + + Set plugins = new HashSet<>(); + for (PluginDescriptor pluginDesc : installedPlugins.stream().map(InstalledPlugin::getDescriptor).collect(Collectors.toList())) { + ListablePlugin listablePlugin = new ListablePlugin(pendingPlugins, pluginDesc, true); + setAvailableVersion(listablePlugin); + plugins.add(listablePlugin); + } + + for (PluginDescriptor pluginDesc : availablePlugins.stream().map(AvailablePlugin::getDescriptor).collect(Collectors.toList())) { + if (plugins.stream().noneMatch(p -> p.name.equals(pluginDesc.getInformation().getName()))) { + plugins.add(new ListablePlugin(pendingPlugins, pluginDesc, false)); + } + } + + return plugins.stream().sorted((a, b) -> a.name.compareToIgnoreCase(b.name)).collect(Collectors.toList()); + } + + private void setAvailableVersion(ListablePlugin listablePlugin) { + Optional availablePlugin = manager.getAvailable().stream().filter(p -> p.getDescriptor().getInformation().getName().equals(listablePlugin.name)).findFirst(); + if (availablePlugin.isPresent() && !availablePlugin.get().getDescriptor().getInformation().getVersion().equals(listablePlugin.installedVersion)) { + listablePlugin.setAvailableVersion(availablePlugin.get().getDescriptor().getInformation().getVersion()); + } + } + + @VisibleForTesting + void setSpec(CommandLine.Model.CommandSpec spec) { + this.spec = spec; + } + + @VisibleForTesting + void setUseShortTemplate(boolean useShortTemplate) { + this.useShortTemplate = useShortTemplate; + } + + @Getter + @Setter + static class ListablePlugin { + private String name; + private String displayName; + private String installedVersion; + private String availableVersion; + private boolean pending; + private boolean installed; + + ListablePlugin(PendingPlugins pendingPlugins, PluginDescriptor descriptor, boolean installed) { + this.name = descriptor.getInformation().getName(); + this.displayName = descriptor.getInformation().getDisplayName(); + if (installed) { + this.installedVersion = descriptor.getInformation().getVersion(); + } else { + this.availableVersion = descriptor.getInformation().getVersion(); + } + this.pending = pendingPlugins.isPending(descriptor.getInformation().getName()); + this.installed = installed; + } + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/cli/PluginRemoveCommand.java b/scm-webapp/src/main/java/sonia/scm/plugin/cli/PluginRemoveCommand.java new file mode 100644 index 0000000000..35eb640ad2 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/plugin/cli/PluginRemoveCommand.java @@ -0,0 +1,87 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package sonia.scm.plugin.cli; + +import com.cronutils.utils.VisibleForTesting; +import picocli.CommandLine; +import sonia.scm.cli.ParentCommand; +import sonia.scm.plugin.PluginManager; + +import javax.inject.Inject; + +@ParentCommand(value = PluginCommand.class) +@CommandLine.Command(name = "remove", aliases = "rm") +class PluginRemoveCommand implements Runnable { + + @CommandLine.Parameters(index = "0", paramLabel = "", descriptionKey = "scm.plugin.name") + private String name; + + @CommandLine.Option(names = {"--apply", "-a"}, descriptionKey = "scm.plugin.apply") + private boolean apply; + + @CommandLine.Mixin + private final PluginTemplateRenderer templateRenderer; + private final PluginManager manager; + @CommandLine.Spec + private CommandLine.Model.CommandSpec spec; + + @Inject + PluginRemoveCommand(PluginTemplateRenderer templateRenderer, PluginManager manager) { + this.templateRenderer = templateRenderer; + this.manager = manager; + } + + + @Override + public void run() { + if (manager.getInstalled(name).isEmpty()) { + templateRenderer.renderPluginNotInstalledError(); + return; + } + + try { + manager.uninstall(name, apply); + } catch (Exception e) { + templateRenderer.renderPluginCouldNotBeRemoved(name); + throw e; + } + templateRenderer.renderPluginRemoved(name); + if (!apply) { + templateRenderer.renderServerRestartRequired(); + } else { + templateRenderer.renderServerRestartTriggered(); + } + } + + @VisibleForTesting + void setName(String name) { + this.name = name; + } + + @VisibleForTesting + void setApply(boolean apply) { + this.apply = apply; + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/cli/PluginResetChangesCommand.java b/scm-webapp/src/main/java/sonia/scm/plugin/cli/PluginResetChangesCommand.java new file mode 100644 index 0000000000..a43c44d09c --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/plugin/cli/PluginResetChangesCommand.java @@ -0,0 +1,58 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package sonia.scm.plugin.cli; + +import picocli.CommandLine; +import sonia.scm.cli.ParentCommand; +import sonia.scm.plugin.PluginManager; + +import javax.inject.Inject; + +@ParentCommand(value = PluginCommand.class) +@CommandLine.Command(name = "cancel-pending", aliases = "reset") +class PluginResetChangesCommand implements Runnable { + + @CommandLine.Mixin + private final PluginTemplateRenderer templateRenderer; + private final PluginManager manager; + @CommandLine.Spec + private CommandLine.Model.CommandSpec spec; + + @Inject + PluginResetChangesCommand(PluginTemplateRenderer templateRenderer, PluginManager manager) { + this.templateRenderer = templateRenderer; + this.manager = manager; + } + + @Override + public void run() { + if (manager.getPending().existPendingChanges()) { + manager.cancelPending(); + templateRenderer.renderPluginsReseted(); + } else { + templateRenderer.renderNoPendingPlugins(); + } + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/cli/PluginTemplateRenderer.java b/scm-webapp/src/main/java/sonia/scm/plugin/cli/PluginTemplateRenderer.java new file mode 100644 index 0000000000..0f99d4d3c8 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/plugin/cli/PluginTemplateRenderer.java @@ -0,0 +1,157 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package sonia.scm.plugin.cli; + +import sonia.scm.cli.CliContext; +import sonia.scm.cli.ExitCode; +import sonia.scm.cli.TemplateRenderer; +import sonia.scm.template.TemplateEngineFactory; + +import javax.inject.Inject; +import java.util.Collections; +import java.util.Map; + +class PluginTemplateRenderer extends TemplateRenderer { + + private static final String PLUGIN_NOT_AVAILABLE_ERROR_TEMPLATE = "{{i18n.scmPluginNotAvailable}}"; + private static final String PLUGIN_NOT_INSTALLED_ERROR_TEMPLATE = "{{i18n.scmPluginNotInstalled}}"; + private static final String PLUGIN_ALREADY_INSTALLED_ERROR_TEMPLATE = "{{i18n.scmPluginAlreadyInstalled}}"; + private static final String PLUGIN_NOT_REMOVED_ERROR_TEMPLATE = "{{i18n.scmPluginNotRemoved}}"; + private static final String PLUGIN_NOT_ADDED_ERROR_TEMPLATE = "{{i18n.scmPluginNotAdded}}"; + private static final String PLUGIN_NOT_UPDATABLE_ERROR_TEMPLATE = "{{i18n.scmPluginNotUpdatable}}"; + private static final String PLUGIN_UPDATE_ERROR_TEMPLATE = "{{i18n.scmPluginsUpdateFailed}}"; + private static final String PLUGIN_ADDED_TEMPLATE = "{{i18n.scmPluginAdded}}"; + private static final String PLUGIN_REMOVED_TEMPLATE = "{{i18n.scmPluginRemoved}}"; + private static final String PLUGIN_UPDATED_TEMPLATE = "{{i18n.scmPluginUpdated}}"; + private static final String ALL_PLUGINS_UPDATED_TEMPLATE = "{{i18n.scmPluginsUpdated}}"; + private static final String SERVER_RESTART_REQUIRED_TEMPLATE = "{{i18n.scmServerRestartRequired}}"; + private static final String SERVER_RESTART_TRIGGERED_TEMPLATE = "{{i18n.scmServerRestartTriggered}}"; + private static final String SERVER_RESTART_SKIPPED_TEMPLATE = "{{i18n.scmServerRestartSkipped}}"; + private static final String SERVER_RESTART_CONFIRMATION_TEMPLATE = "{{i18n.scmServerRestartConfirmation}}"; + private static final String PLUGINS_NOT_PENDING_ERROR_TEMPLATE = "{{i18n.scmPluginsNotPending}}"; + private static final String ALL_PENDING_PLUGINS_CANCELLED = "{{i18n.scmPendingPluginsCancelled}}"; + + private static final String PLUGIN = "plugin"; + private final CliContext context; + + @Inject + PluginTemplateRenderer(CliContext context, TemplateEngineFactory templateEngineFactory) { + super(context, templateEngineFactory); + this.context = context; + } + + public void renderPluginAdded(String pluginName) { + renderToStdout(PLUGIN_ADDED_TEMPLATE, Map.of(PLUGIN, pluginName)); + context.getStdout().println(); + } + + public void renderPluginRemoved(String pluginName) { + renderToStdout(PLUGIN_REMOVED_TEMPLATE, Map.of(PLUGIN, pluginName)); + context.getStdout().println(); + } + + public void renderPluginUpdated(String pluginName) { + renderToStdout(PLUGIN_UPDATED_TEMPLATE, Map.of(PLUGIN, pluginName)); + context.getStdout().println(); + } + + public void renderAllPluginsUpdated() { + renderToStdout(ALL_PLUGINS_UPDATED_TEMPLATE, Collections.emptyMap()); + context.getStdout().println(); + } + + public void renderPluginCouldNotBeRemoved(String pluginName) { + renderToStderr(PLUGIN_NOT_REMOVED_ERROR_TEMPLATE, Map.of(PLUGIN, pluginName)); + context.getStderr().println(); + context.exit(ExitCode.USAGE); + } + + public void renderPluginCouldNotBeAdded(String pluginName) { + renderToStderr(PLUGIN_NOT_ADDED_ERROR_TEMPLATE, Map.of(PLUGIN, pluginName)); + context.getStderr().println(); + context.exit(ExitCode.SERVER_ERROR); + } + + public void renderPluginNotUpdatable(String pluginName) { + renderToStderr(PLUGIN_NOT_UPDATABLE_ERROR_TEMPLATE, Map.of(PLUGIN, pluginName)); + context.getStderr().println(); + context.exit(ExitCode.USAGE); + } + + public void renderServerRestartRequired() { + renderToStdout(SERVER_RESTART_REQUIRED_TEMPLATE, Collections.emptyMap()); + context.getStdout().println(); + } + + public void renderServerRestartTriggered() { + renderToStdout(SERVER_RESTART_TRIGGERED_TEMPLATE, Collections.emptyMap()); + context.getStdout().println(); + } + + public void renderSkipServerRestart() { + renderToStdout(SERVER_RESTART_SKIPPED_TEMPLATE, Collections.emptyMap()); + context.getStdout().println(); + } + + public void renderPluginsReseted() { + renderToStdout(ALL_PENDING_PLUGINS_CANCELLED, Collections.emptyMap()); + context.getStdout().println(); + } + + public void renderNoPendingPlugins() { + renderToStderr(PLUGINS_NOT_PENDING_ERROR_TEMPLATE, Collections.emptyMap()); + context.getStderr().println(); + } + + public void renderConfirmServerRestart() { + renderToStderr(SERVER_RESTART_CONFIRMATION_TEMPLATE, Collections.emptyMap()); + context.getStderr().println(); + context.exit(ExitCode.USAGE); + } + + public void renderPluginNotAvailableError() { + renderToStderr(PLUGIN_NOT_AVAILABLE_ERROR_TEMPLATE, Collections.emptyMap()); + context.getStderr().println(); + context.exit(ExitCode.USAGE); + } + + public void renderPluginsUpdateError() { + renderToStderr(PLUGIN_UPDATE_ERROR_TEMPLATE, Collections.emptyMap()); + context.getStderr().println(); + context.exit(ExitCode.SERVER_ERROR); + } + + public void renderPluginNotInstalledError() { + renderToStderr(PLUGIN_NOT_INSTALLED_ERROR_TEMPLATE, Collections.emptyMap()); + context.getStderr().println(); + context.exit(ExitCode.USAGE); + } + + public void renderPluginAlreadyInstalledError() { + renderToStderr(PLUGIN_ALREADY_INSTALLED_ERROR_TEMPLATE, Collections.emptyMap()); + context.getStderr().println(); + context.exit(ExitCode.USAGE); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/cli/PluginUpdateAllCommand.java b/scm-webapp/src/main/java/sonia/scm/plugin/cli/PluginUpdateAllCommand.java new file mode 100644 index 0000000000..5f3653f8cc --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/plugin/cli/PluginUpdateAllCommand.java @@ -0,0 +1,74 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package sonia.scm.plugin.cli; + +import com.cronutils.utils.VisibleForTesting; +import picocli.CommandLine; +import sonia.scm.cli.ParentCommand; +import sonia.scm.plugin.PluginManager; + +import javax.inject.Inject; + +@ParentCommand(value = PluginCommand.class) +@CommandLine.Command(name = "update-all") +class PluginUpdateAllCommand implements Runnable { + + @CommandLine.Option(names = {"--apply", "-a"}, descriptionKey = "scm.plugin.apply") + private boolean apply; + + @CommandLine.Mixin + private final PluginTemplateRenderer templateRenderer; + private final PluginManager manager; + @CommandLine.Spec + private CommandLine.Model.CommandSpec spec; + + @Inject + PluginUpdateAllCommand(PluginTemplateRenderer templateRenderer, PluginManager manager) { + this.templateRenderer = templateRenderer; + this.manager = manager; + } + + @Override + public void run() { + try { + manager.updateAll(); + } catch (Exception e) { + templateRenderer.renderPluginsUpdateError(); + throw e; + } + templateRenderer.renderAllPluginsUpdated(); + if (!apply) { + templateRenderer.renderServerRestartRequired(); + } else { + manager.executePendingAndRestart(); + templateRenderer.renderServerRestartTriggered(); + } + } + + @VisibleForTesting + void setApply(boolean apply) { + this.apply = apply; + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/cli/PluginUpdateCommand.java b/scm-webapp/src/main/java/sonia/scm/plugin/cli/PluginUpdateCommand.java new file mode 100644 index 0000000000..4a3eec6d2e --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/plugin/cli/PluginUpdateCommand.java @@ -0,0 +1,83 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package sonia.scm.plugin.cli; + +import com.cronutils.utils.VisibleForTesting; +import picocli.CommandLine; +import sonia.scm.cli.ParentCommand; +import sonia.scm.plugin.PluginManager; + +import javax.inject.Inject; +import java.util.Objects; + +@ParentCommand(value = PluginCommand.class) +@CommandLine.Command(name = "update") +class PluginUpdateCommand implements Runnable { + + @CommandLine.Parameters(index = "0", paramLabel = "", descriptionKey = "scm.plugin.name") + private String name; + + @CommandLine.Option(names = {"--apply", "-a"}, descriptionKey = "scm.plugin.apply") + private boolean apply; + + @CommandLine.Mixin + private final PluginTemplateRenderer templateRenderer; + private final PluginManager manager; + @CommandLine.Spec + private CommandLine.Model.CommandSpec spec; + + @Inject + PluginUpdateCommand(PluginTemplateRenderer templateRenderer, PluginManager manager) { + this.templateRenderer = templateRenderer; + this.manager = manager; + } + + @Override + public void run() { + if (manager.getInstalled(name).isEmpty()) { + templateRenderer.renderPluginNotInstalledError(); + return; + } + if (manager.getUpdatable().stream().noneMatch(p -> Objects.equals(p.getDescriptor().getInformation().getName(), name))) { + templateRenderer.renderPluginNotUpdatable(name); + return; + } + manager.install(name, apply); + templateRenderer.renderPluginUpdated(name); + if (!apply) { + templateRenderer.renderServerRestartRequired(); + } else { + templateRenderer.renderServerRestartTriggered(); + } + } + @VisibleForTesting + void setName(String name) { + this.name = name; + } + @VisibleForTesting + void setApply(boolean apply) { + this.apply = apply; + } +} diff --git a/scm-webapp/src/main/resources/sonia/scm/cli/i18n.properties b/scm-webapp/src/main/resources/sonia/scm/cli/i18n.properties index a346eacff0..7e90be4835 100644 --- a/scm-webapp/src/main/resources/sonia/scm/cli/i18n.properties +++ b/scm-webapp/src/main/resources/sonia/scm/cli/i18n.properties @@ -189,3 +189,57 @@ scm.group.remove-member.name = Name of the group scm.group.remove-member.members = Members to remove from the group groupNotFound = Could not find group + +## Plugin +scm.plugin.usage.description.0 = Resource command for all plugin-management-related actions +scmPluginNotAvailable = This plugin is not available. Please check your input. +scmPluginNotInstalled = This plugin is not installed. Please check your input. +scmPluginAlreadyInstalled = This plugin is already installed. +scmPluginAdded = The plugin was added for installation. +scmPluginRemoved = The plugin was removed. +scmPluginUpdated = The plugin was updated. +scmPluginsUpdated = All available plugins were updated. +scmPluginNotRemoved = This plugin could not be removed. Other installed plugins could depend on this plugin. +scmPluginNotAdded = This plugin could not be added. Please check your server logs for further information. +scmPluginNotUpdatable = This plugin could not be updated. +scmPluginsUpdateFailed = Plugin updated failed. Please check your server logs for further information. +scmServerRestartRequired = Please restart your server to make the plugin changes effective. +scmServerRestartTriggered = Server restart: Applying the changes... +scmServerRestartSkipped = Server restart skipped: Nothing to apply. +scmServerRestartConfirmation = Please confirm the server restart using --yes. +scmPluginsNotPending = No pending plugin changes found. +scmPendingPluginsCancelled = All plugin changes were cancelled. + +### List plugins +scm.plugin.name = Name +scm.plugin.displayName = Display Name +scm.plugin.installedVersion = Installed +scm.plugin.availableVersion = Available +scm.plugin.pending = Pending? +scm.plugin.list.usage.description.0 = List all plugins with versions +scm.plugin.list.short = Show only the plugin names + +### Add plugin +scm.plugin.add.usage.description.0 = Installs the plugin with the required dependencies +scm.plugin.add.apply = Apply all plugin changes now by restarting the server + +### Remove plugin +scm.plugin.remove.usage.description.0 = Deletes the plugin +scm.plugin.remove.apply = Apply all plugin changes now by restarting the server + +### Update plugin +scm.plugin.update.usage.description.0 = Updates the installed plugin +scm.plugin.update.apply = Apply all plugin changes now by restarting the server + +### Update all plugins +scm.plugin.update-all.usage.description.0 = Updates all installed plugins +scm.plugin.update-all.apply = Apply all plugins changes now by restarting the server + +### Apply plugin changes +scm.plugin.apply.usage.description.0 = Applies the pending plugin changes by restarting the server +scm.plugin.restart = Confirms server restart + +### Apply plugin changes +scm.plugin.cancel-pending.usage.description.0 = Cancel all pending plugin changes + + diff --git a/scm-webapp/src/main/resources/sonia/scm/cli/i18n_de.properties b/scm-webapp/src/main/resources/sonia/scm/cli/i18n_de.properties index 828ceb5efb..85a60c99d8 100644 --- a/scm-webapp/src/main/resources/sonia/scm/cli/i18n_de.properties +++ b/scm-webapp/src/main/resources/sonia/scm/cli/i18n_de.properties @@ -199,3 +199,56 @@ scm.group.remove-member.name = Name der Gruppe scm.group.remove-member.members = Zu löschende Mitglieder groupNotFound = Gruppe konnte nicht gefunden werden + +## Plugin +scm.plugin.usage.description.0 = Ressourcenkommando für alle Pluginverwaltungsaktionen +scmPluginNotAvailable = Dieses Plugin ist nicht verfügbar. Bitte prüfen Sie Ihre Eingabe. +scmPluginNotInstalled = Dieses Plugin ist nicht installiert. Bitte prüfen Sie Ihre Eingabe. +scmPluginAlreadyInstalled = Dieses Plugin ist bereits installiert. +scmPluginAdded = Das Plugin wurde hinzugefügt. +scmPluginRemoved = Das Plugin wurde entfernt. +scmPluginUpdated = Das Plugin wurde aktualisiert. +scmPluginsUpdated = Alle verfügbaren Plugins wurden aktualisiert. +scmPluginNotRemoved = Dieses Plugin konnte nicht entfernt werden. Andere installierte Plugins könnten von diesem Plugin abhängen. +scmPluginNotAdded = Dieses Plugin konnte nicht hinzugefügt werden. Weitere Informationen finden Sie in den Server Logs. +scmPluginNotUpdatable = Dieses Plugin konnte nicht aktualisiert werden. +scmPluginsUpdateFailed = Plugin Aktualisierungen fehlgeschlagen. Weitere Informationen finden Sie in den Server Logs. +scmServerRestartRequired = Bitte starten Sie Ihren Server neu, um die Plugin Änderungen wirksam zu machen. +scmServerRestartTriggered = Server Neustart: Änderungen werden übernommen... +scmServerRestartSkipped = Server Neustart übersprungen: Keine Änderungen gefunden. +scmServerRestartConfirmation = Bitte bestätigen Sie den Server Neustart mit --yes. +scmPluginsNotPending = Keine ausstehenden Plugin Änderungen gefunden. +scmPendingPluginsCancelled = Alle ausstehenden Plugin Änderungen wurden zurückgesetzt. + +### List plugins +scm.plugin.name = Name +scm.plugin.displayName = Anzeigename +scm.plugin.installedVersion = Installiert +scm.plugin.availableVersion = Verfügbar +scm.plugin.pending = Änderung ausstehend +scm.plugin.list.usage.description.0 = Listet alle Plugins mit Versionen auf +scm.plugin.list.short = Zeigt nur die Plugin Namen an + +### Add plugin +scm.plugin.add.usage.description.0 = Installiert das Plugin inklusive Abhängigkeiten +scm.plugin.add.apply = Macht alle Plugin Änderungen sofort wirksam (Server Neustart!) + +### Remove plugin +scm.plugin.remove.usage.description.0 = Entfernt das Plugin +scm.plugin.remove.apply = Macht alle Plugin Änderungen sofort wirksam (Server Neustart!) + +### Update plugin +scm.plugin.update.usage.description.0 = Aktualisiert das installierte Plugin +scm.plugin.update.apply = Macht alle Plugin Änderungen sofort wirksam (Server Neustart!) + +### Update all plugins +scm.plugin.update-all.usage.description.0 = Aktualisiert alle installierten Plugins +scm.plugin.update-all.apply = Macht alle Plugin Änderungen sofort wirksam (Server Neustart!) + +### Apply plugin changes +scm.plugin.apply.usage.description.0 = Macht alle ausstehenden Plugin Änderungen wirksam (Server Neustart!) +scm.plugin.restart = Server Neustart bestätigen + +### Apply plugin changes +scm.plugin.cancel-pending.usage.description.0 = Setzt alle ausstehenden Plugin Änderungen zurück + diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PendingPluginResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PendingPluginResourceTest.java index 1e7ef453b5..22d957afca 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PendingPluginResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PendingPluginResourceTest.java @@ -43,6 +43,7 @@ import sonia.scm.plugin.AvailablePlugin; import sonia.scm.plugin.AvailablePluginDescriptor; import sonia.scm.plugin.InstalledPlugin; import sonia.scm.plugin.InstalledPluginDescriptor; +import sonia.scm.plugin.PendingPlugins; import sonia.scm.plugin.PluginInformation; import sonia.scm.plugin.PluginManager; import sonia.scm.web.RestDispatcher; @@ -65,7 +66,7 @@ import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) class PendingPluginResourceTest { - private RestDispatcher dispatcher = new RestDispatcher(); + private final RestDispatcher dispatcher = new RestDispatcher(); @SuppressWarnings("unused") ResourceLinks resourceLinks = ResourceLinksMock.createMock(create("/")); @@ -125,23 +126,22 @@ class PendingPluginResourceTest { @Test void shouldGetEmptyPluginListsWithoutInstallLinkWhenNoPendingPluginsPresent() throws URISyntaxException, UnsupportedEncodingException { - AvailablePlugin availablePlugin = createAvailablePlugin("not-pending-plugin"); - when(availablePlugin.isPending()).thenReturn(false); - when(pluginManager.getAvailable()).thenReturn(singletonList(availablePlugin)); + PendingPlugins pendingPlugins = mock(PendingPlugins.class); + when(pluginManager.getPending()).thenReturn(pendingPlugins); MockHttpRequest request = MockHttpRequest.get("/v2/plugins/pending"); dispatcher.invoke(request, response); assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_OK); assertThat(response.getContentAsString()).contains("\"_links\":{\"self\":{\"href\":\"/v2/plugins/pending\"}}"); - assertThat(response.getContentAsString()).doesNotContain("not-pending-plugin"); } @Test void shouldGetPendingAvailablePluginListWithInstallAndCancelLink() throws URISyntaxException, UnsupportedEncodingException { AvailablePlugin availablePlugin = createAvailablePlugin("pending-available-plugin"); - when(availablePlugin.isPending()).thenReturn(true); - when(pluginManager.getAvailable()).thenReturn(singletonList(availablePlugin)); + PendingPlugins pendingPlugins = mock(PendingPlugins.class); + when(pluginManager.getPending()).thenReturn(pendingPlugins); + when(pendingPlugins.getInstall()).thenReturn(singletonList(availablePlugin)); MockHttpRequest request = MockHttpRequest.get("/v2/plugins/pending"); dispatcher.invoke(request, response); @@ -155,10 +155,11 @@ class PendingPluginResourceTest { @Test void shouldGetPendingUpdatePluginListWithInstallLink() throws URISyntaxException, UnsupportedEncodingException { AvailablePlugin availablePlugin = createAvailablePlugin("available-plugin"); - when(availablePlugin.isPending()).thenReturn(true); - when(pluginManager.getAvailable()).thenReturn(singletonList(availablePlugin)); InstalledPlugin installedPlugin = createInstalledPlugin("available-plugin"); - when(pluginManager.getInstalled()).thenReturn(singletonList(installedPlugin)); + PendingPlugins pendingPlugins = mock(PendingPlugins.class); + when(pluginManager.getPending()).thenReturn(pendingPlugins); + when(pendingPlugins.getUpdate()).thenReturn(singletonList(installedPlugin)); + when(pendingPlugins.getInstall()).thenReturn(singletonList(availablePlugin)); MockHttpRequest request = MockHttpRequest.get("/v2/plugins/pending"); dispatcher.invoke(request, response); @@ -170,10 +171,10 @@ class PendingPluginResourceTest { @Test void shouldGetPendingUninstallPluginListWithInstallLink() throws URISyntaxException, UnsupportedEncodingException { - when(pluginManager.getAvailable()).thenReturn(emptyList()); InstalledPlugin installedPlugin = createInstalledPlugin("uninstalled-plugin"); - when(installedPlugin.isMarkedForUninstall()).thenReturn(true); - when(pluginManager.getInstalled()).thenReturn(singletonList(installedPlugin)); + PendingPlugins pendingPlugins = mock(PendingPlugins.class); + when(pendingPlugins.getUninstall()).thenReturn(singletonList(installedPlugin)); + when(pluginManager.getPending()).thenReturn(pendingPlugins); MockHttpRequest request = MockHttpRequest.get("/v2/plugins/pending"); dispatcher.invoke(request, response); @@ -187,10 +188,10 @@ class PendingPluginResourceTest { void shouldNotReturnExecuteLinkIfRestartIsNotSupported() throws URISyntaxException, UnsupportedEncodingException { when(restarter.isSupported()).thenReturn(false); - when(pluginManager.getAvailable()).thenReturn(emptyList()); InstalledPlugin installedPlugin = createInstalledPlugin("uninstalled-plugin"); - when(installedPlugin.isMarkedForUninstall()).thenReturn(true); - when(pluginManager.getInstalled()).thenReturn(singletonList(installedPlugin)); + PendingPlugins pendingPlugins = mock(PendingPlugins.class); + when(pluginManager.getPending()).thenReturn(pendingPlugins); + when(pendingPlugins.getUninstall()).thenReturn(singletonList(installedPlugin)); MockHttpRequest request = MockHttpRequest.get("/v2/plugins/pending"); dispatcher.invoke(request, response); @@ -239,8 +240,9 @@ class PendingPluginResourceTest { @Test void shouldGetPendingAvailablePluginListWithoutInstallAndCancelLink() throws URISyntaxException, UnsupportedEncodingException { AvailablePlugin availablePlugin = createAvailablePlugin("pending-available-plugin"); - when(availablePlugin.isPending()).thenReturn(true); - when(pluginManager.getAvailable()).thenReturn(singletonList(availablePlugin)); + PendingPlugins pendingPlugins = mock(PendingPlugins.class); + when(pluginManager.getPending()).thenReturn(pendingPlugins); + when(pendingPlugins.getInstall()).thenReturn(singletonList(availablePlugin)); MockHttpRequest request = MockHttpRequest.get("/v2/plugins/pending"); dispatcher.invoke(request, response); diff --git a/scm-webapp/src/test/java/sonia/scm/cli/TemplateTestRenderer.java b/scm-webapp/src/test/java/sonia/scm/cli/TemplateTestRenderer.java index f262c466d6..0c8f16e210 100644 --- a/scm-webapp/src/test/java/sonia/scm/cli/TemplateTestRenderer.java +++ b/scm-webapp/src/test/java/sonia/scm/cli/TemplateTestRenderer.java @@ -108,7 +108,7 @@ public class TemplateTestRenderer { return templateEngineFactory; } - public CommandLine.Model.CommandSpec getMockedSpeck() { + public CommandLine.Model.CommandSpec getMockedSpec() { ResourceBundle resourceBundle = getResourceBundle(); CommandLine.Model.CommandSpec mock = mock(CommandLine.Model.CommandSpec.class); lenient().when(mock.resourceBundle()).thenReturn(resourceBundle); diff --git a/scm-webapp/src/test/java/sonia/scm/group/cli/GroupListCommandTest.java b/scm-webapp/src/test/java/sonia/scm/group/cli/GroupListCommandTest.java index d9765bf62d..93ff8052c7 100644 --- a/scm-webapp/src/test/java/sonia/scm/group/cli/GroupListCommandTest.java +++ b/scm-webapp/src/test/java/sonia/scm/group/cli/GroupListCommandTest.java @@ -51,7 +51,7 @@ class GroupListCommandTest { @BeforeEach void initCommand() { command = new GroupListCommand(testRenderer.createTemplateRenderer(), manager, beanMapper); - command.setSpec(testRenderer.getMockedSpeck()); + command.setSpec(testRenderer.getMockedSpec()); } @BeforeEach diff --git a/scm-webapp/src/test/java/sonia/scm/plugin/cli/PluginAddCommandTest.java b/scm-webapp/src/test/java/sonia/scm/plugin/cli/PluginAddCommandTest.java new file mode 100644 index 0000000000..5d00d78e1c --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/plugin/cli/PluginAddCommandTest.java @@ -0,0 +1,118 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package sonia.scm.plugin.cli; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.plugin.PluginManager; +import sonia.scm.plugin.PluginTestHelper; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +class PluginAddCommandTest { + + @Mock + private PluginTemplateRenderer templateRenderer; + @Mock + private PluginManager manager; + + @InjectMocks + private PluginAddCommand command; + + @Test + void shouldAddPlugin() { + String pluginName = "scm-test-plugin"; + doReturn(Optional.empty()).when(manager).getInstalled(pluginName); + doReturn(Optional.of(PluginTestHelper.createAvailable(pluginName))).when(manager).getAvailable(pluginName); + + command.setName(pluginName); + command.run(); + + verify(manager).install(pluginName, false); + verify(templateRenderer).renderPluginAdded(pluginName); + verify(templateRenderer).renderServerRestartRequired(); + } + + @Test + void shouldAddPluginWithRestart() { + String pluginName = "scm-test-plugin"; + doReturn(Optional.empty()).when(manager).getInstalled(pluginName); + doReturn(Optional.of(PluginTestHelper.createAvailable(pluginName))).when(manager).getAvailable(pluginName); + + command.setName(pluginName); + command.setApply(true); + command.run(); + + verify(manager).install(pluginName, true); + verify(templateRenderer).renderPluginAdded(pluginName); + verify(templateRenderer).renderServerRestartTriggered(); + } + + @Test + void shouldRenderErrorIfPluginAlreadyInstalled() { + String pluginName = "scm-test-plugin"; + doReturn(Optional.of(PluginTestHelper.createInstalled(pluginName))).when(manager).getInstalled(pluginName); + + command.setName(pluginName); + command.run(); + + verify(templateRenderer).renderPluginAlreadyInstalledError(); + } + + @Test + void shouldRenderErrorIfPluginNotAvailable() { + String pluginName = "scm-test-plugin"; + doReturn(Optional.empty()).when(manager).getInstalled(pluginName); + doReturn(Optional.empty()).when(manager).getAvailable(pluginName); + + command.setName(pluginName); + command.run(); + + verify(templateRenderer).renderPluginNotAvailableError(); + } + + @Test + void shouldRenderErrorIfPluginInstallationFailed() { + String pluginName = "scm-test-plugin"; + doReturn(Optional.empty()).when(manager).getInstalled(pluginName); + doReturn(Optional.of(PluginTestHelper.createAvailable(pluginName))).when(manager).getAvailable(pluginName); + doThrow(RuntimeException.class).when(manager).install(pluginName, false); + + command.setName(pluginName); + assertThrows(RuntimeException.class, () -> command.run()); + + verify(templateRenderer).renderPluginCouldNotBeAdded(pluginName); + } + +} diff --git a/scm-webapp/src/test/java/sonia/scm/plugin/cli/PluginApplyCommandTest.java b/scm-webapp/src/test/java/sonia/scm/plugin/cli/PluginApplyCommandTest.java new file mode 100644 index 0000000000..62a22125dc --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/plugin/cli/PluginApplyCommandTest.java @@ -0,0 +1,84 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package sonia.scm.plugin.cli; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.plugin.PendingPlugins; +import sonia.scm.plugin.PluginManager; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class PluginApplyCommandTest { + + @Mock + private PluginTemplateRenderer templateRenderer; + @Mock + private PluginManager manager; + + @InjectMocks + private PluginApplyCommand command; + + @Test + void shouldRestartServer() { + command.setRestart(true); + PendingPlugins pendingPlugins = mock(PendingPlugins.class); + when(manager.getPending()).thenReturn(pendingPlugins); + when(pendingPlugins.existPendingChanges()).thenReturn(true); + + command.run(); + + verify(manager).executePendingAndRestart(); + verify(templateRenderer).renderServerRestartTriggered(); + } + + @Test + void shouldNotRestartServerIfFlagIsMissing() { + command.run(); + + verify(manager, never()).executePendingAndRestart(); + verify(templateRenderer).renderConfirmServerRestart(); + } + + @Test + void shouldNotRestartServerIfNoPendingChanges() { + command.setRestart(true); + PendingPlugins pendingPlugins = mock(PendingPlugins.class); + when(manager.getPending()).thenReturn(pendingPlugins); + when(pendingPlugins.existPendingChanges()).thenReturn(false); + + command.run(); + + verify(manager, never()).executePendingAndRestart(); + verify(templateRenderer).renderSkipServerRestart(); + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/plugin/cli/PluginListCommandTest.java b/scm-webapp/src/test/java/sonia/scm/plugin/cli/PluginListCommandTest.java new file mode 100644 index 0000000000..6c4e8a4b7a --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/plugin/cli/PluginListCommandTest.java @@ -0,0 +1,89 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package sonia.scm.plugin.cli; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.cli.TemplateTestRenderer; +import sonia.scm.plugin.PendingPlugins; +import sonia.scm.plugin.PluginManager; +import sonia.scm.plugin.PluginTestHelper; + +import static java.util.List.of; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; + +@ExtendWith(MockitoExtension.class) +class PluginListCommandTest { + + private final TemplateTestRenderer templateTestRenderer = new TemplateTestRenderer(); + @Mock + private PluginManager manager; + + private PluginListCommand command; + + @BeforeEach + void initCommand() { + command = new PluginListCommand(templateTestRenderer.createTemplateRenderer(), manager); + command.setSpec(templateTestRenderer.getMockedSpec()); + PendingPlugins pendingPlugins = mock(PendingPlugins.class); + doReturn(pendingPlugins).when(manager).getPending(); + } + + @Test + void shouldListPlugins() { + doReturn(of(PluginTestHelper.createInstalled("scm-test-plugin"))).when(manager).getInstalled(); + doReturn(of( + PluginTestHelper.createAvailable("scm-review-plugin"), + PluginTestHelper.createAvailable("scm-test-plugin", "1.1.0")) + ).when(manager).getAvailable(); + + command.run(); + + assertThat(templateTestRenderer.getStdOut()) + .contains("NAME DISPLAY NAME AVAILABLE INSTALLED PENDING?") + .contains("scm-review-plugin 1.0") + .contains("scm-test-plugin 1.1.0 1.0"); + } + + @Test + void shouldListPluginsAsShortList() { + doReturn(of(PluginTestHelper.createInstalled("scm-test-plugin"), PluginTestHelper.createInstalled("scm-archive-plugin"))).when(manager).getInstalled(); + doReturn(of( + PluginTestHelper.createAvailable("scm-review-plugin"), + PluginTestHelper.createAvailable("scm-test-plugin", "1.1.0")) + ).when(manager).getAvailable(); + command.setUseShortTemplate(true); + + command.run(); + + assertThat(templateTestRenderer.getStdOut()) + .isEqualTo("scm-archive-plugin\nscm-review-plugin\nscm-test-plugin\n"); + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/plugin/cli/PluginRemoveCommandTest.java b/scm-webapp/src/test/java/sonia/scm/plugin/cli/PluginRemoveCommandTest.java new file mode 100644 index 0000000000..a13ae84ffb --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/plugin/cli/PluginRemoveCommandTest.java @@ -0,0 +1,107 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package sonia.scm.plugin.cli; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.plugin.PluginManager; +import sonia.scm.plugin.PluginTestHelper; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +class PluginRemoveCommandTest { + + @Mock + private PluginTemplateRenderer templateRenderer; + @Mock + private PluginManager manager; + + @InjectMocks + private PluginRemoveCommand command; + + @Test + void shouldRemovePlugin() { + String pluginName = "scm-test-plugin"; + command.setName(pluginName); + doReturn(Optional.of(PluginTestHelper.createInstalled(pluginName))).when(manager).getInstalled(pluginName); + + command.run(); + + verify(manager).uninstall(pluginName, false); + verify(templateRenderer).renderPluginRemoved(pluginName); + verify(templateRenderer).renderServerRestartRequired(); + } + + @Test + void shouldRemovePluginWithRestart() { + String pluginName = "scm-test-plugin"; + command.setName(pluginName); + command.setApply(true); + doReturn(Optional.of(PluginTestHelper.createInstalled(pluginName))).when(manager).getInstalled(pluginName); + + command.run(); + + verify(manager).uninstall(pluginName, true); + verify(templateRenderer).renderPluginRemoved(pluginName); + verify(templateRenderer).renderServerRestartTriggered(); + } + + @Test + void shouldRenderErrorIfPluginNotInstalled() { + String pluginName = "scm-test-plugin"; + command.setName(pluginName); + doReturn(Optional.empty()).when(manager).getInstalled(pluginName); + + command.run(); + + verify(manager, never()).uninstall(eq(pluginName), anyBoolean()); + verify(templateRenderer).renderPluginNotInstalledError(); + } + + @Test + void shouldRenderErrorIfPluginCouldNotBeRemoved() { + String pluginName = "scm-test-plugin"; + command.setName(pluginName); + doThrow(RuntimeException.class).when(manager).uninstall(pluginName, false); + doReturn(Optional.of(PluginTestHelper.createInstalled(pluginName))).when(manager).getInstalled(pluginName); + + assertThrows(RuntimeException.class, () -> command.run()); + + verify(templateRenderer).renderPluginCouldNotBeRemoved(pluginName); + } + +} diff --git a/scm-webapp/src/test/java/sonia/scm/plugin/cli/PluginResetChangesCommandTest.java b/scm-webapp/src/test/java/sonia/scm/plugin/cli/PluginResetChangesCommandTest.java new file mode 100644 index 0000000000..8b51c98fb2 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/plugin/cli/PluginResetChangesCommandTest.java @@ -0,0 +1,74 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package sonia.scm.plugin.cli; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.plugin.PendingPlugins; +import sonia.scm.plugin.PluginManager; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class PluginResetChangesCommandTest { + + @Mock + private PluginTemplateRenderer templateRenderer; + @Mock + private PluginManager manager; + + @InjectMocks + private PluginResetChangesCommand command; + + @Test + void shouldCancelPendingPlugins() { + PendingPlugins pendingPlugins = mock(PendingPlugins.class); + when(manager.getPending()).thenReturn(pendingPlugins); + when(pendingPlugins.existPendingChanges()).thenReturn(true); + + command.run(); + + verify(manager).cancelPending(); + verify(templateRenderer).renderPluginsReseted(); + } + + @Test + void shouldRenderErrorIfNoPendingPlugins() { + PendingPlugins pendingPlugins = mock(PendingPlugins.class); + when(manager.getPending()).thenReturn(pendingPlugins); + when(pendingPlugins.existPendingChanges()).thenReturn(false); + + command.run(); + + verify(manager, never()).cancelPending(); + verify(templateRenderer).renderNoPendingPlugins(); + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/plugin/cli/PluginUpdateAllCommandTest.java b/scm-webapp/src/test/java/sonia/scm/plugin/cli/PluginUpdateAllCommandTest.java new file mode 100644 index 0000000000..1998cc4a3c --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/plugin/cli/PluginUpdateAllCommandTest.java @@ -0,0 +1,79 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package sonia.scm.plugin.cli; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.plugin.PluginManager; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +class PluginUpdateAllCommandTest { + + @Mock + private PluginTemplateRenderer templateRenderer; + @Mock + private PluginManager pluginManager; + + @InjectMocks + private PluginUpdateAllCommand command; + + @Test + void shouldUpdateAll() { + command.run(); + + verify(pluginManager).updateAll(); + verify(templateRenderer).renderAllPluginsUpdated(); + verify(templateRenderer).renderServerRestartRequired(); + } + + @Test + void shouldUpdateAllWithRestart() { + command.setApply(true); + + command.run(); + + verify(pluginManager).updateAll(); + verify(pluginManager).executePendingAndRestart(); + verify(templateRenderer).renderAllPluginsUpdated(); + verify(templateRenderer).renderServerRestartTriggered(); + } + + + @Test + void shouldRenderErrorIfUpdateFailed() { + doThrow(RuntimeException.class).when(pluginManager).updateAll(); + + assertThrows(RuntimeException.class, () -> command.run()); + + verify(templateRenderer).renderPluginsUpdateError(); + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/plugin/cli/PluginUpdateCommandTest.java b/scm-webapp/src/test/java/sonia/scm/plugin/cli/PluginUpdateCommandTest.java new file mode 100644 index 0000000000..cd1d73eb5b --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/plugin/cli/PluginUpdateCommandTest.java @@ -0,0 +1,114 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package sonia.scm.plugin.cli; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.plugin.PluginManager; +import sonia.scm.plugin.PluginTestHelper; + +import java.util.Optional; + +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +class PluginUpdateCommandTest { + + + @Mock + private PluginTemplateRenderer templateRenderer; + @Mock + private PluginManager manager; + + @InjectMocks + private PluginUpdateCommand command; + + @Test + void shouldUpdateSinglePlugin() { + String pluginName = "scm-test-plugin"; + doReturn(Optional.of(PluginTestHelper.createInstalled(pluginName))).when(manager).getInstalled(pluginName); + doReturn(singletonList(PluginTestHelper.createInstalled(pluginName))).when(manager).getUpdatable(); + + command.setName(pluginName); + + command.run(); + + verify(manager).install(pluginName, false); + verify(templateRenderer).renderPluginUpdated(pluginName); + verify(templateRenderer).renderServerRestartRequired(); + } + + @Test + void shouldUpdateSinglePluginWithRestart() { + String pluginName = "scm-test-plugin"; + doReturn(Optional.of(PluginTestHelper.createInstalled(pluginName))).when(manager).getInstalled(pluginName); + doReturn(singletonList(PluginTestHelper.createInstalled(pluginName))).when(manager).getUpdatable(); + + command.setName(pluginName); + command.setApply(true); + + command.run(); + + verify(manager).install(pluginName, true); + verify(templateRenderer).renderPluginUpdated(pluginName); + verify(templateRenderer).renderServerRestartTriggered(); + } + + @Test + void shouldRenderErrorIfPluginNotInstalled() { + String pluginName = "scm-test-plugin"; + doReturn(Optional.empty()).when(manager).getInstalled(pluginName); + + command.setName(pluginName); + + command.run(); + + verify(manager, never()).install(eq(pluginName), anyBoolean()); + verify(templateRenderer).renderPluginNotInstalledError(); + } + + @Test + void shouldRenderErrorIfPluginNotUpdatable() { + String pluginName = "scm-test-plugin"; + doReturn(Optional.of(PluginTestHelper.createInstalled(pluginName))).when(manager).getInstalled(pluginName); + doReturn(emptyList()).when(manager).getUpdatable(); + + command.setName(pluginName); + + command.run(); + + verify(manager, never()).install(eq(pluginName), anyBoolean()); + verify(templateRenderer).renderPluginNotUpdatable(pluginName); + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/user/cli/UserListCommandTest.java b/scm-webapp/src/test/java/sonia/scm/user/cli/UserListCommandTest.java index ed0a315d54..dfdaecb8c8 100644 --- a/scm-webapp/src/test/java/sonia/scm/user/cli/UserListCommandTest.java +++ b/scm-webapp/src/test/java/sonia/scm/user/cli/UserListCommandTest.java @@ -69,7 +69,7 @@ class UserListCommandTest { @Test void shouldRenderShortTableInEnglish() { testRenderer.setLocale("en"); - command.setSpec(testRenderer.getMockedSpeck()); + command.setSpec(testRenderer.getMockedSpec()); command.setUseShortTemplate(true); command.run(); @@ -81,7 +81,7 @@ class UserListCommandTest { @Test void shouldRenderShortTableInGerman() { testRenderer.setLocale("de"); - command.setSpec(testRenderer.getMockedSpeck()); + command.setSpec(testRenderer.getMockedSpec()); command.setUseShortTemplate(true); command.run(); @@ -93,7 +93,7 @@ class UserListCommandTest { @Test void shouldRenderLongTableInEnglish() { testRenderer.setLocale("en"); - command.setSpec(testRenderer.getMockedSpeck()); + command.setSpec(testRenderer.getMockedSpec()); command.run(); @@ -107,7 +107,7 @@ class UserListCommandTest { @Test void shouldRenderLongTableInGerman() { testRenderer.setLocale("de"); - command.setSpec(testRenderer.getMockedSpeck()); + command.setSpec(testRenderer.getMockedSpec()); command.run();