mirror of https://github.com/halo-dev/halo
refactor: optimizing plugin upgrade steps (#3838)
#### What type of PR is this? /kind improvement /area core /milestone 2.5.x #### What this PR does / why we need it: 优化插件的升级流程 how to test it? 1. 测试正常的插件升级是否正常 2. 测试插件升级失败后插件是否会被卸载的问题 3. 测试没有 version 的插件安装是否能成功 4. 在插件目录不会多出一个名为 `{升级插件名称}-null.jar` 的文件 #### Which issue(s) this PR fixes: Fixes #3839 #### Does this PR introduce a user-facing ```release-note 优化插件的升级流程 ```pull/3867/head^2
parent
795d4f9261
commit
8619d96f6a
|
@ -136,9 +136,4 @@ public class Plugin extends AbstractExtension {
|
|||
|
||||
private String website;
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public String generateFileName() {
|
||||
return String.format("%s-%s.jar", getMetadata().getName(), spec.getVersion());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,23 +1,27 @@
|
|||
package run.halo.app.core.extension.reconciler;
|
||||
|
||||
import static org.pf4j.util.FileUtils.isJarFile;
|
||||
import static run.halo.app.extension.MetadataUtil.nullSafeAnnotations;
|
||||
import static run.halo.app.extension.MetadataUtil.nullSafeLabels;
|
||||
import static run.halo.app.plugin.PluginConst.DELETE_STAGE;
|
||||
import static run.halo.app.plugin.PluginConst.PLUGIN_PATH;
|
||||
import static run.halo.app.plugin.PluginConst.RELOAD_ANNO;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.BooleanUtils;
|
||||
|
@ -43,7 +47,6 @@ import run.halo.app.core.extension.theme.SettingUtils;
|
|||
import run.halo.app.extension.ExtensionClient;
|
||||
import run.halo.app.extension.GroupVersionKind;
|
||||
import run.halo.app.extension.Metadata;
|
||||
import run.halo.app.extension.MetadataUtil;
|
||||
import run.halo.app.extension.Unstructured;
|
||||
import run.halo.app.extension.controller.Controller;
|
||||
import run.halo.app.extension.controller.ControllerBuilder;
|
||||
|
@ -57,8 +60,8 @@ import run.halo.app.infra.utils.YamlUnstructuredLoader;
|
|||
import run.halo.app.plugin.HaloPluginManager;
|
||||
import run.halo.app.plugin.PluginConst;
|
||||
import run.halo.app.plugin.PluginExtensionLoaderUtils;
|
||||
import run.halo.app.plugin.PluginNotFoundException;
|
||||
import run.halo.app.plugin.PluginStartingError;
|
||||
import run.halo.app.plugin.YamlPluginFinder;
|
||||
import run.halo.app.plugin.event.PluginCreatedEvent;
|
||||
import run.halo.app.plugin.resources.BundleResourceUtils;
|
||||
|
||||
|
@ -84,26 +87,52 @@ public class PluginReconciler implements Reconciler<Request> {
|
|||
|
||||
@Override
|
||||
public Result reconcile(Request request) {
|
||||
return client.fetch(Plugin.class, request.name())
|
||||
.map(plugin -> {
|
||||
if (plugin.getMetadata().getDeletionTimestamp() != null) {
|
||||
cleanUpResourcesAndRemoveFinalizer(request.name());
|
||||
try {
|
||||
return client.fetch(Plugin.class, request.name())
|
||||
.map(plugin -> {
|
||||
if (plugin.getMetadata().getDeletionTimestamp() != null) {
|
||||
cleanUpResourcesAndRemoveFinalizer(request.name());
|
||||
return Result.doNotRetry();
|
||||
}
|
||||
addFinalizerIfNecessary(plugin);
|
||||
|
||||
// if true returned, it means it is not ready
|
||||
if (readinessDetection(request.name())) {
|
||||
return new Result(true, null);
|
||||
}
|
||||
|
||||
reconcilePluginState(plugin.getMetadata().getName());
|
||||
return Result.doNotRetry();
|
||||
}
|
||||
addFinalizerIfNecessary(plugin);
|
||||
})
|
||||
.orElse(Result.doNotRetry());
|
||||
} catch (DoNotRetryException e) {
|
||||
persistenceFailureStatus(request.name(), e);
|
||||
return Result.doNotRetry();
|
||||
}
|
||||
}
|
||||
|
||||
// if true returned, it means it is not ready
|
||||
if (readinessDetection(request.name())) {
|
||||
return new Result(true, null);
|
||||
private void updatePluginPathAnno(String name) {
|
||||
// TODO do it in a better way
|
||||
client.fetch(Plugin.class, name).ifPresent(plugin -> {
|
||||
Map<String, String> annotations = nullSafeAnnotations(plugin);
|
||||
String oldPluginPath = annotations.get(PLUGIN_PATH);
|
||||
String pluginPath = oldPluginPath;
|
||||
if (StringUtils.isBlank(oldPluginPath)) {
|
||||
URI loadLocation = plugin.statusNonNull().getLoadLocation();
|
||||
if (loadLocation == null) {
|
||||
throw new DoNotRetryException("Can not determine plugin path: " + name);
|
||||
}
|
||||
|
||||
reconcilePluginState(plugin.getMetadata().getName());
|
||||
return Result.doNotRetry();
|
||||
})
|
||||
.orElse(Result.doNotRetry());
|
||||
pluginPath = loadLocation.getPath();
|
||||
}
|
||||
annotations.put(PLUGIN_PATH, pluginPath);
|
||||
if (!StringUtils.equals(pluginPath, oldPluginPath)) {
|
||||
client.update(plugin);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
boolean readinessDetection(String name) {
|
||||
updatePluginPathAnno(name);
|
||||
return client.fetch(Plugin.class, name)
|
||||
.map(plugin -> {
|
||||
if (waitForSettingCreation(plugin)) {
|
||||
|
@ -115,8 +144,8 @@ public class PluginReconciler implements Reconciler<Request> {
|
|||
generateAccessibleLogoUrl(plugin);
|
||||
|
||||
// update phase
|
||||
PluginWrapper pluginWrapper = getPluginWrapper(name);
|
||||
Plugin.PluginStatus status = plugin.statusNonNull();
|
||||
PluginWrapper pluginWrapper = getPluginWrapper(name);
|
||||
status.setPhase(pluginWrapper.getPluginState());
|
||||
updateStatus(plugin.getMetadata().getName(), status);
|
||||
return false;
|
||||
|
@ -186,7 +215,7 @@ public class PluginReconciler implements Reconciler<Request> {
|
|||
Optional<Setting> settingOption = lookupPluginSetting(pluginName, settingName)
|
||||
.map(setting -> {
|
||||
// This annotation is added to prevent it from being deleted when stopped.
|
||||
Map<String, String> settingAnnotations = MetadataUtil.nullSafeAnnotations(setting);
|
||||
Map<String, String> settingAnnotations = nullSafeAnnotations(setting);
|
||||
settingAnnotations.put(DELETE_STAGE, PluginConst.DeleteStage.UNINSTALL.name());
|
||||
return setting;
|
||||
})
|
||||
|
@ -232,7 +261,7 @@ public class PluginReconciler implements Reconciler<Request> {
|
|||
stateTransition(name, currentState -> {
|
||||
boolean termination = false;
|
||||
switch (currentState) {
|
||||
case CREATED -> ensurePluginLoaded();
|
||||
case CREATED -> getPluginWrapper(name);
|
||||
case STARTED -> termination = true;
|
||||
// plugin can be started when it is stopped or failed
|
||||
case RESOLVED, STOPPED, FAILED -> doStart(name);
|
||||
|
@ -247,7 +276,7 @@ public class PluginReconciler implements Reconciler<Request> {
|
|||
stateTransition(name, currentState -> {
|
||||
boolean termination = false;
|
||||
switch (currentState) {
|
||||
case CREATED -> ensurePluginLoaded();
|
||||
case CREATED -> getPluginWrapper(name);
|
||||
case RESOLVED, STARTED -> doStop(name);
|
||||
case FAILED, STOPPED -> termination = true;
|
||||
default -> {
|
||||
|
@ -283,10 +312,8 @@ public class PluginReconciler implements Reconciler<Request> {
|
|||
if (currentState != desiredState) {
|
||||
log.error("Plugin [{}] state transition failed: {}", name,
|
||||
haloPluginManager.getPluginStartingError(name));
|
||||
var e = new PluginRuntimeException("Plugin [" + name + "] state transition from ["
|
||||
throw new DoNotRetryException("Plugin [" + name + "] state transition from ["
|
||||
+ currentState + "] to [" + desiredState + "] failed");
|
||||
persistenceFailureStatus(name, e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -317,7 +344,7 @@ public class PluginReconciler implements Reconciler<Request> {
|
|||
private PluginWrapper getPluginWrapper(String name) {
|
||||
PluginWrapper pluginWrapper = haloPluginManager.getPlugin(name);
|
||||
if (pluginWrapper == null) {
|
||||
ensurePluginLoaded();
|
||||
ensurePluginLoaded(name);
|
||||
pluginWrapper = haloPluginManager.getPlugin(name);
|
||||
}
|
||||
|
||||
|
@ -336,7 +363,7 @@ public class PluginReconciler implements Reconciler<Request> {
|
|||
Plugin.PluginStatus.nullSafeConditions(status)
|
||||
.addAndEvictFIFO(condition);
|
||||
updateStatus(name, status);
|
||||
throw new PluginNotFoundException(errorMsg);
|
||||
throw new DoNotRetryException(errorMsg);
|
||||
}
|
||||
return pluginWrapper;
|
||||
}
|
||||
|
@ -348,6 +375,17 @@ public class PluginReconciler implements Reconciler<Request> {
|
|||
client.fetch(Plugin.class, name).ifPresent(plugin -> {
|
||||
Plugin.PluginStatus oldStatus = JsonUtils.deepCopy(plugin.statusNonNull());
|
||||
plugin.setStatus(status);
|
||||
URI loadLocation = status.getLoadLocation();
|
||||
if (loadLocation == null) {
|
||||
String pluginPath = nullSafeAnnotations(plugin).get(PLUGIN_PATH);
|
||||
if (StringUtils.isNotBlank(pluginPath)) {
|
||||
String absolutePath = buildPluginLocation(name, pluginPath);
|
||||
loadLocation = toUri(absolutePath);
|
||||
} else {
|
||||
loadLocation = getPluginWrapper(name).getPluginPath().toUri();
|
||||
}
|
||||
status.setLoadLocation(loadLocation);
|
||||
}
|
||||
if (!Objects.equals(oldStatus, status)) {
|
||||
client.update(plugin);
|
||||
}
|
||||
|
@ -465,11 +503,15 @@ public class PluginReconciler implements Reconciler<Request> {
|
|||
}
|
||||
|
||||
private void reconcilePluginState(String name) {
|
||||
if (haloPluginManager.getPlugin(name) == null) {
|
||||
ensurePluginLoaded();
|
||||
}
|
||||
|
||||
client.fetch(Plugin.class, name).ifPresent(plugin -> {
|
||||
// reload detection
|
||||
Map<String, String> annotations = nullSafeAnnotations(plugin);
|
||||
if (annotations.containsKey(RELOAD_ANNO)) {
|
||||
reload(plugin);
|
||||
// update will requeue to make next reconciliation
|
||||
return;
|
||||
}
|
||||
|
||||
// Transition plugin status if necessary
|
||||
if (shouldReconcileStartState(plugin)) {
|
||||
startAction(name);
|
||||
|
@ -481,20 +523,117 @@ public class PluginReconciler implements Reconciler<Request> {
|
|||
});
|
||||
}
|
||||
|
||||
private void ensurePluginLoaded() {
|
||||
// load plugin if exists in plugin root paths.
|
||||
List<PluginWrapper> loadedPlugins = haloPluginManager.getPlugins();
|
||||
Map<Path, PluginWrapper> loadedPluginWrapperMap = loadedPlugins.stream()
|
||||
.collect(Collectors.toMap(PluginWrapper::getPluginPath, item -> item));
|
||||
haloPluginManager.getPluginRepository()
|
||||
.getPluginPaths()
|
||||
.forEach(path -> {
|
||||
if (!loadedPluginWrapperMap.containsKey(path)) {
|
||||
haloPluginManager.loadPlugin(path);
|
||||
void reload(Plugin plugin) {
|
||||
String newPluginPath = nullSafeAnnotations(plugin).get(RELOAD_ANNO);
|
||||
if (StringUtils.isBlank(newPluginPath)) {
|
||||
return;
|
||||
}
|
||||
final String pluginName = plugin.getMetadata().getName();
|
||||
URI oldPluginLocation = plugin.statusNonNull().getLoadLocation();
|
||||
if (shouldDeleteFile(newPluginPath, oldPluginLocation)) {
|
||||
try {
|
||||
// delete old plugin jar file
|
||||
Files.deleteIfExists(Paths.get(oldPluginLocation.getPath()));
|
||||
} catch (IOException e) {
|
||||
throw new PluginRuntimeException(e);
|
||||
}
|
||||
}
|
||||
final var pluginFinder = new YamlPluginFinder();
|
||||
final var pluginInPath = pluginFinder.find(toPath(newPluginPath));
|
||||
client.fetch(Plugin.class, plugin.getMetadata().getName())
|
||||
.ifPresent(persisted -> {
|
||||
if (!persisted.getMetadata().getName()
|
||||
.equals(pluginInPath.getMetadata().getName())) {
|
||||
throw new DoNotRetryException("Plugin name is not match, skip reload.");
|
||||
}
|
||||
persisted.setSpec(pluginInPath.getSpec());
|
||||
// merge annotations and labels
|
||||
Map<String, String> newAnnotations = nullSafeAnnotations(persisted);
|
||||
newAnnotations.putAll(nullSafeAnnotations(pluginInPath));
|
||||
|
||||
newAnnotations.put(PLUGIN_PATH, resolvePluginPathAnnotation(newPluginPath));
|
||||
newAnnotations.remove(RELOAD_ANNO);
|
||||
nullSafeLabels(persisted).putAll(nullSafeLabels(pluginInPath));
|
||||
persisted.statusNonNull().setLoadLocation(toUri(newPluginPath));
|
||||
|
||||
// reload
|
||||
haloPluginManager.reloadPluginWithPath(pluginName, toPath(newPluginPath));
|
||||
// update plugin
|
||||
client.update(persisted);
|
||||
});
|
||||
}
|
||||
|
||||
String resolvePluginPathAnnotation(String pluginPathString) {
|
||||
Path pluginsRoot = toPath(haloPluginManager.getPluginsRoot().toString());
|
||||
Path pluginPath = toPath(pluginPathString);
|
||||
if (pluginPath.startsWith(pluginsRoot)) {
|
||||
return pluginsRoot.relativize(pluginPath).toString();
|
||||
}
|
||||
return pluginPath.toString();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns absolute plugin path.
|
||||
* if plugin path is absolute, use it directly in development mode.
|
||||
* otherwise, combine plugin path with plugin root path.
|
||||
* Note: plugin location without scheme
|
||||
*/
|
||||
String buildPluginLocation(String name, String pluginPathString) {
|
||||
Assert.notNull(name, "Plugin name must not be null");
|
||||
Assert.notNull(pluginPathString, "Plugin path must not be null");
|
||||
Path pluginsRoot = toPath(haloPluginManager.getPluginsRoot().toString());
|
||||
Path pluginPath = toPath(pluginPathString);
|
||||
// if plugin path is absolute, use it directly in development mode
|
||||
if (pluginPath.isAbsolute()) {
|
||||
if (!isDevelopmentMode(name) && !pluginPath.startsWith(pluginsRoot)) {
|
||||
throw new DoNotRetryException(
|
||||
"Plugin path must be relative path or relative to plugin root path.");
|
||||
}
|
||||
return pluginPath.toString();
|
||||
}
|
||||
return PathUtils.combinePath(pluginsRoot.toString(), pluginPath.toString());
|
||||
}
|
||||
|
||||
boolean shouldDeleteFile(String newPluginPath, URI oldPluginLocation) {
|
||||
if (oldPluginLocation == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (oldPluginLocation.equals(toUri(newPluginPath))) {
|
||||
return false;
|
||||
}
|
||||
return isJarFile(Paths.get(oldPluginLocation));
|
||||
}
|
||||
|
||||
private void ensurePluginLoaded(String name) {
|
||||
client.fetch(Plugin.class, name).ifPresent(plugin -> {
|
||||
PluginWrapper pluginWrapper = haloPluginManager.getPlugin(name);
|
||||
if (pluginWrapper != null) {
|
||||
return;
|
||||
}
|
||||
Path pluginLocation = determinePluginLocation(plugin);
|
||||
if (!Files.exists(pluginLocation)) {
|
||||
return;
|
||||
}
|
||||
haloPluginManager.loadPlugin(pluginLocation);
|
||||
});
|
||||
}
|
||||
|
||||
Path toPath(String pathString) {
|
||||
if (StringUtils.isBlank(pathString)) {
|
||||
return null;
|
||||
}
|
||||
return Paths.get(URI.create(pathString).getPath());
|
||||
}
|
||||
|
||||
URI toUri(String pathString) {
|
||||
if (StringUtils.isBlank(pathString)) {
|
||||
throw new IllegalArgumentException("Path string must not be blank");
|
||||
}
|
||||
return Paths.get(pathString).toUri();
|
||||
}
|
||||
|
||||
private boolean shouldReconcileStartState(Plugin plugin) {
|
||||
PluginWrapper pluginWrapper = getPluginWrapper(plugin.getMetadata().getName());
|
||||
return BooleanUtils.isTrue(plugin.getSpec().getEnabled())
|
||||
|
@ -574,8 +713,11 @@ public class PluginReconciler implements Reconciler<Request> {
|
|||
}
|
||||
|
||||
// delete plugin resources
|
||||
Path pluginPath = determinePluginLocation(plugin);
|
||||
if (pluginPath != null && !isDevelopmentMode(name) && isJarFile(pluginPath)) {
|
||||
Path pluginPath = Optional.ofNullable(plugin.statusNonNull().getLoadLocation())
|
||||
.map(URI::getPath)
|
||||
.map(Paths::get)
|
||||
.orElse(null);
|
||||
if (pluginPath != null && isJarFile(pluginPath)) {
|
||||
// delete plugin file
|
||||
try {
|
||||
Files.deleteIfExists(pluginPath);
|
||||
|
@ -585,19 +727,21 @@ public class PluginReconciler implements Reconciler<Request> {
|
|||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@NonNull
|
||||
Path determinePluginLocation(Plugin plugin) {
|
||||
final var name = plugin.getMetadata().getName();
|
||||
PluginWrapper pluginWrapper = haloPluginManager.getPlugin(name);
|
||||
return Optional.ofNullable(pluginWrapper)
|
||||
.map(PluginWrapper::getPluginPath)
|
||||
.orElseGet(() -> {
|
||||
var localtionUri = plugin.statusNonNull().getLoadLocation();
|
||||
if (localtionUri != null) {
|
||||
return Path.of(localtionUri);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
String pluginPath = nullSafeAnnotations(plugin).get(PLUGIN_PATH);
|
||||
String name = plugin.getMetadata().getName();
|
||||
if (StringUtils.isBlank(pluginPath)) {
|
||||
URI loadLocation = plugin.statusNonNull().getLoadLocation();
|
||||
if (loadLocation != null) {
|
||||
pluginPath = loadLocation.getPath();
|
||||
} else {
|
||||
throw new DoNotRetryException(
|
||||
"Cannot determine plugin path for plugin: " + name);
|
||||
}
|
||||
}
|
||||
String pluginLocation = buildPluginLocation(name, pluginPath);
|
||||
return Paths.get(pluginLocation);
|
||||
}
|
||||
|
||||
void createInitialReverseProxyIfNotPresent(Plugin plugin) {
|
||||
|
@ -629,15 +773,22 @@ public class PluginReconciler implements Reconciler<Request> {
|
|||
}, () -> client.create(reverseProxy));
|
||||
}
|
||||
|
||||
static class DoNotRetryException extends PluginRuntimeException {
|
||||
public DoNotRetryException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
static String initialReverseProxyName(String pluginName) {
|
||||
return pluginName + "-system-generated-reverse-proxy";
|
||||
}
|
||||
|
||||
private boolean isDevelopmentMode(String name) {
|
||||
PluginWrapper pluginWrapper = haloPluginManager.getPlugin(name);
|
||||
if (pluginWrapper == null) {
|
||||
return false;
|
||||
RuntimeMode runtimeMode = haloPluginManager.getRuntimeMode();
|
||||
if (pluginWrapper != null) {
|
||||
runtimeMode = pluginWrapper.getRuntimeMode();
|
||||
}
|
||||
return RuntimeMode.DEVELOPMENT.equals(pluginWrapper.getRuntimeMode());
|
||||
return RuntimeMode.DEVELOPMENT.equals(runtimeMode);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,25 +7,25 @@ import java.io.IOException;
|
|||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.Duration;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.Validate;
|
||||
import org.pf4j.PluginWrapper;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
|
||||
import org.springframework.retry.RetryException;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.server.ServerErrorException;
|
||||
import org.springframework.web.server.ServerWebInputException;
|
||||
import reactor.core.Exceptions;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.scheduler.Schedulers;
|
||||
import reactor.util.retry.Retry;
|
||||
import run.halo.app.core.extension.Plugin;
|
||||
import run.halo.app.core.extension.service.PluginService;
|
||||
import run.halo.app.extension.MetadataUtil;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
import run.halo.app.infra.SystemVersionSupplier;
|
||||
import run.halo.app.infra.exception.PluginAlreadyExistsException;
|
||||
|
@ -33,6 +33,7 @@ import run.halo.app.infra.exception.UnsatisfiedAttributeValueException;
|
|||
import run.halo.app.infra.utils.FileUtils;
|
||||
import run.halo.app.infra.utils.VersionUtils;
|
||||
import run.halo.app.plugin.HaloPluginManager;
|
||||
import run.halo.app.plugin.PluginConst;
|
||||
import run.halo.app.plugin.PluginProperties;
|
||||
import run.halo.app.plugin.YamlPluginFinder;
|
||||
|
||||
|
@ -96,29 +97,21 @@ public class PluginServiceImpl implements PluginService {
|
|||
// pre-check the plugin in the path
|
||||
final var pluginFinder = new YamlPluginFinder();
|
||||
final var pluginInPath = pluginFinder.find(path);
|
||||
Validate.notNull(pluginInPath.statusNonNull().getLoadLocation());
|
||||
satisfiesRequiresVersion(pluginInPath);
|
||||
if (!Objects.equals(name, pluginInPath.getMetadata().getName())) {
|
||||
return Mono.error(new ServerWebInputException(
|
||||
"The provided plugin " + pluginInPath.getMetadata().getName()
|
||||
+ " didn't match the given plugin " + name));
|
||||
+ " didn't match the given plugin " + name));
|
||||
}
|
||||
|
||||
// check if the plugin exists
|
||||
return client.fetch(Plugin.class, name)
|
||||
.switchIfEmpty(Mono.error(() -> new ServerWebInputException(
|
||||
"The given plugin with name " + name + " was not found.")))
|
||||
// delete the plugin and wait for the deletion
|
||||
.then(Mono.defer(() -> deletePluginAndWaitForComplete(name)))
|
||||
// copy plugin into plugin home
|
||||
.flatMap(prevPlugin -> copyToPluginHome(pluginInPath)
|
||||
.map(pluginFinder::find)
|
||||
// reset enabled spec
|
||||
.doOnNext(pluginToCreate -> {
|
||||
var enabled = prevPlugin.getSpec().getEnabled();
|
||||
pluginToCreate.getSpec().setEnabled(enabled);
|
||||
}))
|
||||
// create the plugin
|
||||
.flatMap(client::create);
|
||||
.flatMap(prevPlugin -> copyToPluginHome(pluginInPath))
|
||||
.flatMap(pluginPath -> updateReloadAnno(name, pluginPath));
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -129,15 +122,16 @@ public class PluginServiceImpl implements PluginService {
|
|||
return Mono.error(() -> new ServerWebInputException(
|
||||
"The given plugin with name " + name + " was not found."));
|
||||
}
|
||||
YamlPluginFinder yamlPluginFinder = new YamlPluginFinder();
|
||||
Plugin newPlugin = yamlPluginFinder.find(pluginWrapper.getPluginPath());
|
||||
// reload plugin
|
||||
pluginManager.reloadPlugin(name);
|
||||
return updateReloadAnno(name, pluginWrapper.getPluginPath());
|
||||
}
|
||||
|
||||
private Mono<Plugin> updateReloadAnno(String name, Path pluginPath) {
|
||||
return client.get(Plugin.class, name)
|
||||
.flatMap(plugin -> {
|
||||
newPlugin.getMetadata().setVersion(plugin.getMetadata().getVersion());
|
||||
newPlugin.getSpec().setEnabled(true);
|
||||
return client.update(newPlugin);
|
||||
// add reload annotation to flag the plugin to be reloaded
|
||||
Map<String, String> annotations = MetadataUtil.nullSafeAnnotations(plugin);
|
||||
annotations.put(PluginConst.RELOAD_ANNO, pluginPath.toString());
|
||||
return client.update(plugin);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -150,7 +144,7 @@ public class PluginServiceImpl implements PluginService {
|
|||
private Mono<Path> copyToPluginHome(Plugin plugin) {
|
||||
return Mono.fromCallable(
|
||||
() -> {
|
||||
var fileName = plugin.generateFileName();
|
||||
var fileName = generateFileName(plugin);
|
||||
var pluginRoot = Paths.get(pluginProperties.getPluginsRoot());
|
||||
try {
|
||||
Files.createDirectories(pluginRoot);
|
||||
|
@ -168,25 +162,17 @@ public class PluginServiceImpl implements PluginService {
|
|||
.subscribeOn(Schedulers.boundedElastic());
|
||||
}
|
||||
|
||||
private Mono<Plugin> deletePluginAndWaitForComplete(String pluginName) {
|
||||
return client.fetch(Plugin.class, pluginName)
|
||||
.flatMap(client::delete)
|
||||
.flatMap(plugin -> waitForDeleted(pluginName).thenReturn(plugin));
|
||||
static String generateFileName(Plugin plugin) {
|
||||
Assert.notNull(plugin, "The plugin must not be null.");
|
||||
Assert.notNull(plugin.getMetadata(), "The plugin metadata must not be null.");
|
||||
Assert.notNull(plugin.getSpec(), "The plugin spec must not be null.");
|
||||
String version = plugin.getSpec().getVersion();
|
||||
if (StringUtils.isBlank(version)) {
|
||||
throw new ServerWebInputException("The plugin version must not be blank.");
|
||||
}
|
||||
return String.format("%s-%s.jar", plugin.getMetadata().getName(), version);
|
||||
}
|
||||
|
||||
private Mono<Void> waitForDeleted(String pluginName) {
|
||||
return Mono.defer(() -> client.fetch(Plugin.class, pluginName)
|
||||
.flatMap(plugin -> Mono.error(
|
||||
new RetryException("Re-check if the plugin is deleted successfully"))))
|
||||
.retryWhen(Retry.fixedDelay(20, Duration.ofMillis(100))
|
||||
.filter(t -> t instanceof RetryException)
|
||||
)
|
||||
.onErrorMap(Exceptions::isRetryExhausted,
|
||||
t -> new ServerErrorException("Wait timeout for plugin deleted", t))
|
||||
.then();
|
||||
}
|
||||
|
||||
|
||||
private void satisfiesRequiresVersion(Plugin newPlugin) {
|
||||
Assert.notNull(newPlugin, "The plugin must not be null.");
|
||||
Version version = systemVersion.get();
|
||||
|
|
|
@ -11,6 +11,7 @@ import lombok.extern.slf4j.Slf4j;
|
|||
import org.pf4j.DefaultPluginManager;
|
||||
import org.pf4j.ExtensionFactory;
|
||||
import org.pf4j.ExtensionFinder;
|
||||
import org.pf4j.PluginAlreadyLoadedException;
|
||||
import org.pf4j.PluginDependency;
|
||||
import org.pf4j.PluginDescriptor;
|
||||
import org.pf4j.PluginDescriptorFinder;
|
||||
|
@ -378,6 +379,23 @@ public class HaloPluginManager extends DefaultPluginManager
|
|||
return getPlugin(pluginId).getPluginState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reload plugin by name and path.
|
||||
* Note: This method will ignore {@link PluginAlreadyLoadedException}.
|
||||
*
|
||||
* @param pluginName plugin name
|
||||
* @param pluginPath a new plugin path
|
||||
*/
|
||||
public void reloadPluginWithPath(String pluginName, Path pluginPath) {
|
||||
stopPlugin(pluginName, false);
|
||||
unloadPlugin(pluginName, false);
|
||||
try {
|
||||
loadPlugin(pluginPath);
|
||||
} catch (PluginAlreadyLoadedException ex) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Release plugin holding release on stop.
|
||||
*/
|
||||
|
|
|
@ -16,6 +16,10 @@ public interface PluginConst {
|
|||
|
||||
String SYSTEM_PLUGIN_NAME = "system";
|
||||
|
||||
String RELOAD_ANNO = "plugin.halo.run/reload";
|
||||
|
||||
String PLUGIN_PATH = "plugin.halo.run/plugin-path";
|
||||
|
||||
static String assertsRoutePrefix(String pluginName) {
|
||||
return "/plugins/" + pluginName + "/assets/";
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import org.pf4j.util.FileUtils;
|
|||
import org.springframework.core.io.FileSystemResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import run.halo.app.core.extension.Plugin;
|
||||
import run.halo.app.extension.MetadataUtil;
|
||||
import run.halo.app.extension.Unstructured;
|
||||
import run.halo.app.infra.utils.YamlUnstructuredLoader;
|
||||
|
||||
|
@ -68,6 +69,8 @@ public class YamlPluginFinder {
|
|||
pluginStatus.setLoadLocation(pluginPath.toUri());
|
||||
plugin.setStatus(pluginStatus);
|
||||
}
|
||||
MetadataUtil.nullSafeAnnotations(plugin)
|
||||
.put(PluginConst.PLUGIN_PATH, pluginPath.toString());
|
||||
return plugin;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,21 +1,33 @@
|
|||
package run.halo.app.core.extension.reconciler;
|
||||
|
||||
import static java.util.Objects.requireNonNull;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.isA;
|
||||
import static org.mockito.Mockito.doNothing;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static run.halo.app.core.extension.reconciler.PluginReconciler.initialReverseProxyName;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import org.json.JSONException;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
|
@ -35,8 +47,10 @@ import run.halo.app.core.extension.ReverseProxy;
|
|||
import run.halo.app.extension.ExtensionClient;
|
||||
import run.halo.app.extension.Metadata;
|
||||
import run.halo.app.extension.controller.Reconciler;
|
||||
import run.halo.app.infra.utils.FileUtils;
|
||||
import run.halo.app.infra.utils.JsonUtils;
|
||||
import run.halo.app.plugin.HaloPluginManager;
|
||||
import run.halo.app.plugin.PluginConst;
|
||||
import run.halo.app.plugin.PluginStartingError;
|
||||
|
||||
/**
|
||||
|
@ -84,7 +98,7 @@ class PluginReconcilerTest {
|
|||
});
|
||||
|
||||
ArgumentCaptor<Plugin> pluginCaptor = doReconcileWithoutRequeue();
|
||||
verify(extensionClient, times(2)).update(isA(Plugin.class));
|
||||
verify(extensionClient, times(3)).update(isA(Plugin.class));
|
||||
|
||||
Plugin updateArgs = pluginCaptor.getAllValues().get(1);
|
||||
assertThat(updateArgs).isNotNull();
|
||||
|
@ -144,7 +158,7 @@ class PluginReconcilerTest {
|
|||
when(pluginWrapper.getPluginState()).thenReturn(PluginState.STARTED);
|
||||
|
||||
ArgumentCaptor<Plugin> pluginCaptor = doReconcileWithoutRequeue();
|
||||
verify(extensionClient, times(2)).update(any(Plugin.class));
|
||||
verify(extensionClient, times(3)).update(any(Plugin.class));
|
||||
|
||||
Plugin updateArgs = pluginCaptor.getValue();
|
||||
assertThat(updateArgs).isNotNull();
|
||||
|
@ -169,7 +183,8 @@ class PluginReconcilerTest {
|
|||
"enabled": false
|
||||
},
|
||||
"status": {
|
||||
"phase": "STOPPED"
|
||||
"phase": "STOPPED",
|
||||
"loadLocation": "/tmp/plugins/apples.jar"
|
||||
}
|
||||
}
|
||||
""", Plugin.class);
|
||||
|
@ -182,7 +197,7 @@ class PluginReconcilerTest {
|
|||
when(pluginWrapper.getPluginState()).thenReturn(PluginState.STARTED);
|
||||
|
||||
ArgumentCaptor<Plugin> pluginCaptor = doReconcileWithoutRequeue();
|
||||
verify(extensionClient, times(2)).update(any(Plugin.class));
|
||||
verify(extensionClient, times(3)).update(any(Plugin.class));
|
||||
|
||||
Plugin updateArgs = pluginCaptor.getValue();
|
||||
assertThat(updateArgs).isNotNull();
|
||||
|
@ -360,6 +375,119 @@ class PluginReconcilerTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolvePluginPathAnnotation() {
|
||||
when(haloPluginManager.getPluginsRoot()).thenReturn(Paths.get("/tmp/plugins"));
|
||||
String path = pluginReconciler.resolvePluginPathAnnotation("/tmp/plugins/sitemap-1.0.jar");
|
||||
assertThat(path).isEqualTo("sitemap-1.0.jar");
|
||||
|
||||
path = pluginReconciler.resolvePluginPathAnnotation("/abc/plugins/sitemap-1.0.jar");
|
||||
assertThat(path).isEqualTo("/abc/plugins/sitemap-1.0.jar");
|
||||
}
|
||||
|
||||
@Nested
|
||||
class ReloadPluginTest {
|
||||
private static final String PLUGIN_NAME = "fake-plugin";
|
||||
private static final Path OLD_PLUGIN_PATH = Paths.get("/path/to/old/plugin.jar");
|
||||
|
||||
@Test
|
||||
void reload() throws IOException, URISyntaxException {
|
||||
var fakePluginUri = requireNonNull(
|
||||
getClass().getClassLoader().getResource("plugin/plugin-0.0.2")).toURI();
|
||||
Path tempDirectory = Files.createTempDirectory("halo-ut-plugin-service-impl-");
|
||||
final Path fakePluginPath = tempDirectory.resolve("plugin-0.0.2.jar");
|
||||
try {
|
||||
FileUtils.jar(Paths.get(fakePluginUri), tempDirectory.resolve("plugin-0.0.2.jar"));
|
||||
when(haloPluginManager.getPluginsRoot()).thenReturn(tempDirectory);
|
||||
// mock plugin
|
||||
Plugin plugin = mock(Plugin.class);
|
||||
Metadata metadata = new Metadata();
|
||||
metadata.setName(PLUGIN_NAME);
|
||||
when(plugin.getMetadata()).thenReturn(metadata);
|
||||
metadata.setAnnotations(new HashMap<>());
|
||||
metadata.getAnnotations()
|
||||
.put(PluginConst.RELOAD_ANNO, fakePluginPath.toString());
|
||||
Plugin.PluginStatus pluginStatus = mock(Plugin.PluginStatus.class);
|
||||
when(pluginStatus.getLoadLocation()).thenReturn(OLD_PLUGIN_PATH.toUri());
|
||||
when(plugin.statusNonNull()).thenReturn(pluginStatus);
|
||||
|
||||
when(extensionClient.fetch(Plugin.class, PLUGIN_NAME))
|
||||
.thenReturn(Optional.of(plugin));
|
||||
|
||||
// call reload method
|
||||
pluginReconciler.reload(plugin);
|
||||
|
||||
// verify that the plugin is updated with the new plugin's spec, annotations, and
|
||||
// labels
|
||||
verify(plugin).setSpec(any(Plugin.PluginSpec.class));
|
||||
verify(extensionClient).update(plugin);
|
||||
|
||||
// verify that the plugin's load location is updated to the new plugin path
|
||||
verify(pluginStatus).setLoadLocation(fakePluginPath.toUri());
|
||||
|
||||
// verify that the new plugin is reloaded
|
||||
verify(haloPluginManager).reloadPluginWithPath(PLUGIN_NAME, fakePluginPath);
|
||||
} finally {
|
||||
FileUtils.deleteRecursivelyAndSilently(tempDirectory);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldDeleteFile() throws IOException {
|
||||
String newPluginPath = "/path/to/new/plugin.jar";
|
||||
|
||||
// Case 1: oldPluginLocation is null
|
||||
assertFalse(pluginReconciler.shouldDeleteFile(newPluginPath, null));
|
||||
|
||||
// Case 2: oldPluginLocation is the same as newPluginPath
|
||||
assertFalse(pluginReconciler.shouldDeleteFile(newPluginPath,
|
||||
pluginReconciler.toUri(newPluginPath)));
|
||||
|
||||
Path tempDirectory = Files.createTempDirectory("halo-ut-plugin-service-impl-");
|
||||
try {
|
||||
Path oldPluginPath = tempDirectory.resolve("plugin.jar");
|
||||
final URI oldPluginLocation = oldPluginPath.toUri();
|
||||
Files.createFile(oldPluginPath);
|
||||
// Case 3: oldPluginLocation is different from newPluginPath and is a JAR file
|
||||
assertTrue(pluginReconciler.shouldDeleteFile(newPluginPath, oldPluginLocation));
|
||||
} finally {
|
||||
FileUtils.deleteRecursivelyAndSilently(tempDirectory);
|
||||
}
|
||||
|
||||
// Case 4: oldPluginLocation is different from newPluginPath and is not a JAR file
|
||||
assertFalse(pluginReconciler.shouldDeleteFile(newPluginPath,
|
||||
Paths.get("/path/to/old/plugin.txt").toUri()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void toPath() {
|
||||
assertThat(pluginReconciler.toPath(null)).isNull();
|
||||
assertThat(pluginReconciler.toPath("")).isNull();
|
||||
assertThat(pluginReconciler.toPath(" ")).isNull();
|
||||
|
||||
Path path = pluginReconciler.toPath("file:///path/to/file.txt");
|
||||
assertThat(path).isNotNull();
|
||||
assertThat(path.toString()).isEqualTo("/path/to/file.txt");
|
||||
}
|
||||
|
||||
@Test
|
||||
void toUri() {
|
||||
// Test with null pathString
|
||||
Assertions.assertThrows(IllegalArgumentException.class, () -> {
|
||||
pluginReconciler.toUri(null);
|
||||
});
|
||||
|
||||
// Test with empty pathString
|
||||
Assertions.assertThrows(IllegalArgumentException.class, () -> {
|
||||
pluginReconciler.toUri("");
|
||||
});
|
||||
|
||||
// Test with non-empty pathString
|
||||
URI uri = pluginReconciler.toUri("/path/to/file");
|
||||
Assertions.assertEquals("file:///path/to/file", uri.toString());
|
||||
}
|
||||
}
|
||||
|
||||
private ArgumentCaptor<Plugin> doReconcileNeedRequeue() {
|
||||
ArgumentCaptor<Plugin> pluginCaptor = ArgumentCaptor.forClass(Plugin.class);
|
||||
doNothing().when(extensionClient).update(pluginCaptor.capture());
|
||||
|
@ -397,7 +525,8 @@ class PluginReconcilerTest {
|
|||
"enabled": true
|
||||
},
|
||||
"status": {
|
||||
"phase": "STOPPED"
|
||||
"phase": "STOPPED",
|
||||
"loadLocation": "/tmp/plugins/apples.jar"
|
||||
}
|
||||
}
|
||||
""", Plugin.class);
|
||||
|
@ -416,7 +545,8 @@ class PluginReconcilerTest {
|
|||
"enabled": false
|
||||
},
|
||||
"status": {
|
||||
"phase": "STARTED"
|
||||
"phase": "STARTED",
|
||||
"loadLocation": "/tmp/plugins/apples.jar"
|
||||
}
|
||||
}
|
||||
""", Plugin.class);
|
||||
|
|
|
@ -2,11 +2,9 @@ package run.halo.app.core.extension.service.impl;
|
|||
|
||||
import static java.util.Objects.requireNonNull;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.argThat;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.isA;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
@ -26,13 +24,11 @@ import org.junit.jupiter.api.BeforeEach;
|
|||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.pf4j.PluginState;
|
||||
import org.pf4j.PluginWrapper;
|
||||
import org.springframework.web.server.ServerErrorException;
|
||||
import org.springframework.web.server.ServerWebInputException;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
|
@ -43,6 +39,7 @@ import run.halo.app.infra.SystemVersionSupplier;
|
|||
import run.halo.app.infra.exception.PluginAlreadyExistsException;
|
||||
import run.halo.app.infra.utils.FileUtils;
|
||||
import run.halo.app.plugin.HaloPluginManager;
|
||||
import run.halo.app.plugin.PluginConst;
|
||||
import run.halo.app.plugin.PluginProperties;
|
||||
import run.halo.app.plugin.YamlPluginFinder;
|
||||
|
||||
|
@ -167,30 +164,15 @@ class PluginServiceImplTest {
|
|||
verify(client).fetch(Plugin.class, "fake-plugin");
|
||||
}
|
||||
|
||||
@Test
|
||||
void upgradeWhenWaitingTimeoutForPluginDeletion() {
|
||||
var prevPlugin = mock(Plugin.class);
|
||||
when(client.fetch(Plugin.class, "fake-plugin"))
|
||||
.thenReturn(Mono.just(prevPlugin));
|
||||
when(client.delete(isA(Plugin.class))).thenReturn(Mono.just(prevPlugin));
|
||||
|
||||
var plugin = pluginService.upgrade("fake-plugin", fakePluginPath);
|
||||
StepVerifier.create(plugin)
|
||||
.consumeErrorWith(error -> {
|
||||
assertTrue(error instanceof ServerErrorException);
|
||||
assertEquals("Wait timeout for plugin deleted",
|
||||
((ServerErrorException) error).getReason());
|
||||
})
|
||||
.verify();
|
||||
|
||||
verify(client, times(23)).fetch(Plugin.class, "fake-plugin");
|
||||
verify(client).delete(isA(Plugin.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void upgradeNormally() {
|
||||
var prevPlugin = mock(Plugin.class);
|
||||
var spec = mock(Plugin.PluginSpec.class);
|
||||
final var prevPlugin = mock(Plugin.class);
|
||||
final var spec = mock(Plugin.PluginSpec.class);
|
||||
final var updatedPlugin = mock(Plugin.class);
|
||||
Metadata metadata = new Metadata();
|
||||
metadata.setName("fake-plugin");
|
||||
when(prevPlugin.getMetadata()).thenReturn(metadata);
|
||||
|
||||
when(prevPlugin.getSpec()).thenReturn(spec);
|
||||
when(spec.getEnabled()).thenReturn(true);
|
||||
|
||||
|
@ -198,61 +180,50 @@ class PluginServiceImplTest {
|
|||
.thenReturn(Mono.just(prevPlugin))
|
||||
.thenReturn(Mono.just(prevPlugin))
|
||||
.thenReturn(Mono.empty());
|
||||
when(client.delete(isA(Plugin.class))).thenReturn(Mono.just(prevPlugin));
|
||||
|
||||
var createdPlugin = mock(Plugin.class);
|
||||
when(client.create(isA(Plugin.class))).thenReturn(Mono.just(createdPlugin));
|
||||
when(client.get(Plugin.class, "fake-plugin"))
|
||||
.thenReturn(Mono.just(prevPlugin));
|
||||
|
||||
when(client.update(isA(Plugin.class))).thenReturn(Mono.just(updatedPlugin));
|
||||
|
||||
var plugin = pluginService.upgrade("fake-plugin", fakePluginPath);
|
||||
|
||||
StepVerifier.create(plugin)
|
||||
.expectNext(createdPlugin)
|
||||
.verifyComplete();
|
||||
.expectNext(updatedPlugin).verifyComplete();
|
||||
|
||||
verify(client, times(3)).fetch(Plugin.class, "fake-plugin");
|
||||
verify(client).delete(isA(Plugin.class));
|
||||
verify(client).<Plugin>create(argThat(p -> p.getSpec().getEnabled()));
|
||||
verify(client, times(1)).fetch(Plugin.class, "fake-plugin");
|
||||
verify(client, times(0)).delete(isA(Plugin.class));
|
||||
verify(client).<Plugin>update(argThat(p -> p.getSpec().getEnabled()));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void reload() throws IOException, URISyntaxException {
|
||||
var fakePluginUri = requireNonNull(
|
||||
getClass().getClassLoader().getResource("plugin/plugin-0.0.2")).toURI();
|
||||
Path tempDirectory = Files.createTempDirectory("halo-ut-plugin-service-impl-");
|
||||
Path fakePluginPath = tempDirectory.resolve("plugin-0.0.2.jar");
|
||||
try {
|
||||
FileUtils.jar(Paths.get(fakePluginUri), tempDirectory.resolve("plugin-0.0.2.jar"));
|
||||
void reload() {
|
||||
// given
|
||||
String pluginName = "test-plugin";
|
||||
PluginWrapper pluginWrapper = mock(PluginWrapper.class);
|
||||
when(pluginManager.getPlugin(pluginName)).thenReturn(pluginWrapper);
|
||||
when(pluginWrapper.getPluginPath())
|
||||
.thenReturn(Paths.get("/tmp/plugins/fake-plugin.jar"));
|
||||
Plugin plugin = new Plugin();
|
||||
plugin.setMetadata(new Metadata());
|
||||
plugin.getMetadata().setName(pluginName);
|
||||
plugin.setSpec(new Plugin.PluginSpec());
|
||||
when(client.get(Plugin.class, pluginName)).thenReturn(Mono.just(plugin));
|
||||
when(client.update(plugin)).thenReturn(Mono.just(plugin));
|
||||
|
||||
final String pluginName = "fake-plugin";
|
||||
PluginWrapper pluginWrapper = mock(PluginWrapper.class);
|
||||
when(pluginManager.getPlugin(eq(pluginName))).thenReturn(pluginWrapper);
|
||||
when(pluginWrapper.getPluginPath()).thenReturn(fakePluginPath);
|
||||
// when
|
||||
Mono<Plugin> result = pluginService.reload(pluginName);
|
||||
|
||||
Plugin plugin = new Plugin();
|
||||
plugin.setMetadata(new Metadata());
|
||||
plugin.getMetadata().setName(pluginName);
|
||||
plugin.setSpec(new Plugin.PluginSpec());
|
||||
plugin.getSpec().setEnabled(false);
|
||||
plugin.getSpec().setDisplayName("Fake Plugin");
|
||||
|
||||
when(client.get(eq(Plugin.class), eq(pluginName))).thenReturn(Mono.just(plugin));
|
||||
when(client.update(any(Plugin.class))).thenReturn(Mono.empty());
|
||||
|
||||
pluginService.reload(pluginName).block();
|
||||
|
||||
verify(pluginManager).reloadPlugin(eq(pluginName));
|
||||
verify(client).get(eq(Plugin.class), eq(pluginName));
|
||||
|
||||
ArgumentCaptor<Plugin> captor = ArgumentCaptor.forClass(Plugin.class);
|
||||
verify(client).update(captor.capture());
|
||||
|
||||
Plugin updatedPlugin = captor.getValue();
|
||||
assertThat(updatedPlugin.getSpec().getEnabled()).isTrue();
|
||||
assertThat(updatedPlugin.getSpec().getDisplayName()).isEqualTo("Fake Display Name");
|
||||
assertThat(updatedPlugin.getSpec().getDescription()).isEqualTo("Fake description");
|
||||
} finally {
|
||||
FileUtils.deleteRecursivelyAndSilently(tempDirectory);
|
||||
}
|
||||
// then
|
||||
assertDoesNotThrow(() -> result.block());
|
||||
verify(client, times(1)).update(
|
||||
argThat(p -> {
|
||||
String reloadPath = p.getMetadata().getAnnotations().get(PluginConst.RELOAD_ANNO);
|
||||
assertThat(reloadPath).isEqualTo("/tmp/plugins/fake-plugin.jar");
|
||||
return true;
|
||||
})
|
||||
);
|
||||
verify(pluginWrapper, times(1)).getPluginPath();
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue