Enable plugin management via CLI (#2087)

Co-authored-by: Konstantin Schaper <konstantin.schaper@cloudogu.com>
This commit is contained in:
Eduard Heimbuch
2022-07-19 09:02:00 +02:00
committed by GitHub
parent 67c083ee54
commit fc28da90b3
27 changed files with 1718 additions and 74 deletions

View File

@@ -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<AvailablePlugin> pending = pluginManager
.getAvailable()
.stream()
.filter(AvailablePlugin::isPending)
.collect(toList());
List<InstalledPlugin> installed = pluginManager.getInstalled();
Stream<AvailablePlugin> newPlugins = pending
.stream()
.filter(a -> !contains(installed, a));
Stream<InstalledPlugin> updatePlugins = installed
.stream()
.filter(i -> contains(pending, i));
Stream<InstalledPlugin> uninstallPlugins = installed
.stream()
.filter(InstalledPlugin::isMarkedForUninstall);
PendingPlugins pending = pluginManager.getPending();
Links.Builder linksBuilder = linkingTo().self(resourceLinks.pendingPluginCollection().self());
List<PluginDto> installDtos = newPlugins.map(mapper::mapAvailable).collect(toList());
List<PluginDto> updateDtos = updatePlugins.map(i -> mapper.mapInstalled(i, pending)).collect(toList());
List<PluginDto> uninstallDtos = uninstallPlugins.map(i -> mapper.mapInstalled(i, pending)).collect(toList());
List<PluginDto> installDtos = pending.getInstall().stream().map(mapper::mapAvailable).collect(toList());
List<PluginDto> updateDtos = pending.getUpdate().stream().map(p -> mapper.mapInstalled(p, pending.getInstall())).collect(toList());
List<PluginDto> 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<InstalledPlugin> installedPlugins, AvailablePlugin availablePlugin) {
return installedPlugins
.stream()
.anyMatch(installedPlugin -> haveSameName(installedPlugin, availablePlugin));
}
private boolean contains(Collection<AvailablePlugin> 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(

View File

@@ -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<AvailablePlugin> pendingPlugins = new ArrayList<>(plugins);
pendingInstallQueue.stream().map(PendingPluginInstallation::getPlugin).forEach(pendingPlugins::add);
return PluginInstallationContext.from(getInstalled(), pendingPlugins);
});
}
this.contextFactory = Objects.requireNonNullElseGet(contextFactory, () -> (plugins -> {
List<AvailablePlugin> 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<InstalledPlugin> 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 <T extends Plugin> Predicate<T> 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

View File

@@ -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 = "<name>", 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;
}
}

View File

@@ -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;
}
}

View File

@@ -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 {
}

View File

@@ -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<ListablePlugin> 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<ListablePlugin> getListablePlugins() {
List<InstalledPlugin> installedPlugins = manager.getInstalled();
List<AvailablePlugin> availablePlugins = manager.getAvailable();
PendingPlugins pendingPlugins = manager.getPending();
Set<ListablePlugin> 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> 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;
}
}
}

View File

@@ -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 = "<name>", 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;
}
}

View File

@@ -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();
}
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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 = "<name>", 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;
}
}